diff --git a/.gitignore b/.gitignore
index 33ba5fe..a56a938 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,3 @@
.godot/
/android/
/builds/
-
-# Blender backup files
-*.blend1
diff --git a/ASSET-LICENSE.md b/ASSET-LICENSE.md
deleted file mode 100644
index 9519836..0000000
--- a/ASSET-LICENSE.md
+++ /dev/null
@@ -1,189 +0,0 @@
-# straywild Asset License 1.1
-
-Copyright © 2026 Woofmeow. All rights reserved except for the permissions
-expressly granted below.
-
-## 1. Scope
-
-This license applies to original straywild artwork, graphics, textures,
-models, animations, music, sound recordings, narrative content, visual
-designs, characters, and other creative assets owned by the straywild owners
-or licensed to them with authority to sublicense, publishing as Woofmeow (the
-**“straywild Assets”**).
-
-It does not apply to:
-
-- software source code identified as GPL-3.0-or-later;
-- third-party material identified in the source repository's
- `docs/ATTRIBUTION.md` or the packaged `ATTRIBUTION.md` notice;
-- material accompanied by a different license; or
-- the straywild and Woofmeow names and logos, which are addressed in
- `TRADEMARKS.md`.
-
-Repository location alone does not change third-party ownership or licensing.
-
-## 2. Definitions
-
-**Official straywild Software** means:
-
-- an unmodified binary release published by the straywild owners through an
- official straywild distribution channel; or
-- a build made from an unmodified official straywild source release or tagged
- revision.
-
-**Mod** means a non-standalone add-on created to operate with Official
-straywild Software. A Mod may add, replace, or alter content loaded by the
-Official straywild Software, but it may not redistribute a replacement copy
-of the game or represent a fork, clone, sequel, or separate game.
-
-**Contribution Fork** means a source-control fork or clone of an official
-straywild source repository that is created and maintained in good faith
-primarily to prepare, test, review, or submit changes for possible inclusion
-in Official straywild Software. It does not include a fork presented or
-developed as an independent game, community edition, replacement distribution,
-or ongoing product separate from the official straywild project.
-
-## 3. Permission to use the official game
-
-You may reproduce straywild Assets as technically necessary to install, run,
-back up, and privately modify a lawfully obtained copy of Official straywild
-Software.
-
-You may capture, publish, and monetize screenshots, gameplay videos,
-livestreams, reviews, tutorials, and similar commentary showing a lawfully
-obtained copy of straywild. This permission does not authorize redistribution
-of the source asset files or imply endorsement.
-
-## 4. Community Mod license
-
-Subject to this license, you may create, modify, and distribute straywild
-Assets solely as part of a Mod for Official straywild Software.
-
-This permission is worldwide, nonexclusive, royalty-free, and available for
-as long as you comply with this license. It permits only the copies of
-straywild Assets reasonably necessary for the Mod to function. It does not
-permit distributing the original game, a substantially complete collection of
-its assets, or a general-purpose asset pack.
-
-A distributed Mod must:
-
-1. require a separately obtained copy of Official straywild Software;
-2. be presented as unofficial community content;
-3. include the following notice in its documentation or distribution page:
-
- > Contains or adapts straywild assets © 2026 Woofmeow, used under the
- > straywild Asset License 1.1. This Mod is unofficial and is not endorsed
- > by Woofmeow.
-
-4. identify material created by the Mod author separately from straywild
- Assets; and
-5. comply with `TRADEMARKS.md`.
-
-Mods may be shared without charge and may accept voluntary donations that do
-not purchase access, features, or preferential treatment. Selling a Mod,
-charging for access to it, licensing it commercially, or using straywild
-Assets in paid products requires separate written permission from both
-straywild owners.
-
-## 5. Contribution Fork license
-
-Subject to this license, you may reproduce, modify, and distribute straywild
-Assets within a Contribution Fork. You may also make the nonpublic builds
-reasonably necessary to develop, test, and review its proposed contribution.
-
-This permission is worldwide, nonexclusive, royalty-free, and available for
-as long as you comply with this license. Public source-control hosting of the
-Contribution Fork and the repository history technically necessary for that
-fork are permitted distributions.
-
-A Contribution Fork must:
-
-1. remain primarily directed toward preparing changes for possible inclusion
- in the official straywild project;
-2. preserve this license and the official straywild project's applicable
- attribution, third-party license, and copyright notices;
-3. remain visibly linked to or identified as a fork of the official source
- repository and not be presented as official or endorsed;
-4. limit test-build access to the owners, maintainers, contributors, and
- testers directly participating in development or review of the proposed
- contribution;
-5. not publish general-audience binaries, releases, packages, or downloads,
- operate a persistent public alternative service, or monetize access;
-6. not distribute the straywild Assets as a standalone extraction, archive,
- or general-purpose asset pack; and
-7. comply with `TRADEMARKS.md`.
-
-Temporary access-controlled servers may be operated when reasonably necessary
-to test a proposed contribution. They may not be promoted or maintained as a
-public alternative to Official straywild Software or its services.
-
-Submitting work from a Contribution Fork is governed separately by
-`CONTRIBUTOR-TERMS.md` and does not require the straywild owners to accept the
-work. Declining or closing a proposed contribution does not retroactively
-invalidate good-faith use under this section. A compliant Contribution Fork
-may remain publicly accessible as an archival development record, but it may
-not be repurposed for continued development or distribution of a separate
-product containing straywild Assets.
-
-## 6. Uses not permitted
-
-Except as expressly permitted by Sections 3, 4, and 5 or by separate written
-permission from both straywild owners, you may not use or distribute
-straywild Assets:
-
-- in a fork, clone, remake, sequel, or standalone game that is not a qualifying
- Contribution Fork;
-- with a modified distribution of the GPL-covered straywild code that is not
- Official straywild Software, except for nonpublic contribution-testing
- builds permitted by Section 5;
-- in another game, application, asset library, template, or merchandise;
-- as a substantially complete asset extraction or archive;
-- in a way that implies sponsorship, endorsement, or official status;
-- to register a confusingly similar trademark, domain, account, or product
- identity; or
-- under terms that claim ownership of the straywild Assets themselves.
-
-The GPL permits forks and redistribution of the GPL-covered code. Except for a
-qualifying Contribution Fork under Section 5, this asset license does not
-permit those forks to carry straywild Assets. A code fork that is not a
-Contribution Fork must remove or replace the straywild Assets and reserved
-branding unless both owners grant separate written permission.
-
-## 7. Ownership of Mods and contribution work
-
-You retain ownership of your original Mod material and original material
-created in a Contribution Fork. The straywild owners retain ownership of the
-straywild Assets and adaptations of those assets to the extent provided by
-applicable law.
-
-This license does not require you to assign or submit your original material.
-It also does not grant you ownership of the underlying straywild Assets. If
-you intentionally submit material for inclusion in straywild,
-`CONTRIBUTOR-TERMS.md` applies separately.
-
-## 8. Termination and cure
-
-Your permissions under this license terminate automatically if you violate
-its terms. They are reinstated provisionally when you stop the violation and
-permanently if both straywild owners do not notify you of the violation
-within sixty days after it has been cured.
-
-Termination does not limit remedies available for activity outside this
-license.
-
-## 9. No warranty
-
-THE straywild ASSETS ARE PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
-PARTICULAR PURPOSE, TITLE, OR NON-INFRINGEMENT. TO THE MAXIMUM EXTENT
-PERMITTED BY LAW, THE OWNERS WILL NOT BE LIABLE FOR ANY CLAIM, DAMAGES, OR
-OTHER LIABILITY ARISING FROM THIS LICENSE OR USE OF THE straywild ASSETS.
-
-## 10. Additional permission
-
-Uses not granted here require written permission from both straywild owners.
-A permission granted to one person or project does not amend this license for
-others.
-
-This is a custom asset license, not an open-source software license or a
-Creative Commons license.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index 2dc5724..0000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,82 +0,0 @@
-# Contribution policy
-
-straywild is an owner-directed Woofmeow project and is not accepting
-unsolicited contributions. Please do not submit pull requests, patches, code,
-artwork, models, audio, writing, documentation, translations, designs, or
-other implementation work. Pull requests are disabled for the official
-repositories.
-
-Issues remain available for reproducible bug reports and player feedback. An
-Issue is not a request for contributed implementation, and attaching or
-linking unsolicited work does not make it an accepted Project contribution.
-
-The owners may occasionally invite specific work in writing before it is
-created or submitted. [`CONTRIBUTOR-TERMS.md`](CONTRIBUTOR-TERMS.md) is retained
-to document previously accepted contributions and govern expressly invited
-work; it is not a standing invitation to contribute.
-
-## Contribution forks
-
-The [straywild Asset License](ASSET-LICENSE.md) contains limited permissions
-for qualifying Contribution Forks. Those permissions do not promise review or
-acceptance and do not override the policy above. Keep the hosting platform's
-fork relationship visible, preserve all license and attribution notices, and
-do not publish general-audience builds or use the fork as an independent game
-or distribution.
-
-Private test builds and temporary access-controlled test servers may be shared
-only as allowed by that license. Permission to maintain a Contribution Fork
-does not authorize unsolicited submission or obligate the owners to review it.
-
-## Change discipline
-
-- Keep a change focused; avoid unrelated cleanup.
-- Inspect the working tree before editing and preserve existing work.
-- Do not change protocol, save, settings, identity, or portable-data versions
- as release-label housekeeping.
-- Treat the host as authoritative for catches, purchases, sales, jobs, mail,
- and other shared state.
-- Keep presentation-only systems out of save files and network messages.
-- Store runtime assets under repository-owned paths; never reference a
- workstation sync or temporary directory.
-- Retain source, permission, and license evidence for newly imported assets.
- Update [`docs/ATTRIBUTION.md`](docs/ATTRIBUTION.md) when the accepted work
- adds or changes a distributed credit or third-party notice; do not add a
- file-by-file inventory of ordinary project-owned assets.
-- Do not assume the GPL code license applies to project assets; observe
- [`ASSET-LICENSE.md`](ASSET-LICENSE.md) and third-party terms.
-- Do not commit `.godot/`, test data, logs, captures, or build outputs.
-
-## Before committing a maintainer change
-
-```sh
-git diff --check
-scripts/run_validations.sh quick
-git status --short
-```
-
-Choose additional focused tests from the validation section of
-[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md#validation).
-Changes involving networking should run the network suite and a real
-two-process check. Visual changes still require graphical review; headless
-tests are supporting evidence, not a replacement.
-
-## Commits
-
-- Stage audited paths explicitly.
-- Use a concise imperative subject that describes the outcome.
-- Do not mix generated build artifacts with source changes.
-- Do not rewrite published release tags.
-- Describe behavior, compatibility impact, validation, and asset provenance in
- the internal review or release record.
-
-## Style
-
-Follow the conventions already present in the surrounding GDScript, scene, and
-resource files. Prefer typed values, named constants, shared resources, and
-small domain-specific services. Avoid duplicating protocol definitions or
-using display strings as persistent identifiers.
-
-New standalone source files should use `SPDX-License-Identifier:
-GPL-3.0-or-later` where the file format permits comments. Do not add that
-identifier to asset files governed by the straywild Asset License.
diff --git a/CONTRIBUTOR-TERMS.md b/CONTRIBUTOR-TERMS.md
deleted file mode 100644
index 261de60..0000000
--- a/CONTRIBUTOR-TERMS.md
+++ /dev/null
@@ -1,100 +0,0 @@
-# straywild contributor terms
-
-straywild does not accept unsolicited contributions. These terms are retained
-as the governing record for contributions previously accepted after their
-publication and for specific future work that both Owners expressly invite in
-writing before submission. They are not a request or standing invitation for
-patches, pull requests, assets, or other material.
-
-These terms do not retroactively change ownership of earlier work or replace a
-separately signed agreement.
-
-“Owners” means the two straywild co-owners who publish the Project as
-Woofmeow and are publicly credited as Voyager and Endeavour. “Project” means
-the official straywild game, its servers, tools, documentation, websites,
-ports, updates, and promotional materials.
-
-## 1. Your ownership and authority
-
-You retain copyright in your original contribution. By submitting it, you
-represent that:
-
-- you created it or otherwise have authority to grant these permissions;
-- its submission does not knowingly violate another party’s copyright,
- patent, trademark, privacy, contract, or other rights;
-- you have disclosed relevant third-party material and license terms; and
-- if an employer or another organization controls the contribution, you have
- obtained its permission before submitting.
-
-Do not submit confidential information, unlawfully obtained material, or
-content whose origin and permitted use cannot be documented.
-
-## 2. Code contributions
-
-You license an accepted software-code contribution under the GNU General
-Public License, version 3 or, at the recipient’s option, any later version
-(GPL-3.0-or-later).
-
-You also grant each recipient a perpetual, worldwide, nonexclusive,
-royalty-free patent license under patent claims you can license that are
-necessarily infringed by your contribution alone or by its combination with
-the Project as submitted. This patent license does not cover later
-combinations that are unrelated to your submitted contribution.
-
-## 3. Asset, writing, and documentation contributions
-
-For an accepted contribution consisting of artwork, models, animation, audio,
-music, writing, level content, branding material, or other non-code creative
-work, you grant the Owners a perpetual, worldwide, nonexclusive,
-royalty-free, transferable, and sublicensable license to:
-
-- use, reproduce, modify, adapt, translate, and create derivative works;
-- distribute, publicly display, publicly perform, transmit, and make
- available the contribution;
-- include it in free and commercial Project releases, ports, updates,
- merchandise, marketing, and archival materials; and
-- license it to Project users under `ASSET-LICENSE.md` or a later Project asset
- license jointly approved by the Owners.
-
-This grant is limited to straywild, its promotion, and official successor or
-derived straywild products. It does not transfer your copyright or authorize
-the Owners to sell your contribution as an unrelated standalone asset.
-
-## 4. Documentation accompanying code
-
-Documentation submitted as an ordinary part of a code contribution may be
-distributed under GPL-3.0-or-later with that code. Project credits and
-provenance records may be reproduced in any medium needed to document the
-Project and its releases.
-
-## 5. Attribution
-
-Provide the name or pseudonym you want used for credit. The Owners will make a
-reasonable effort to preserve material attribution, subject to formatting,
-privacy, platform, and practical limitations.
-
-Attribution does not imply that a contributor endorses every Project change
-or release.
-
-## 6. No automatic ownership, employment, or payment
-
-Submitting a contribution does not provide ownership of straywild, create an
-employment, agency, partnership, or fiduciary relationship, or create a right
-to compensation. Any payment, ownership, or employment arrangement requires a
-separate written agreement signed by the appropriate parties.
-
-## 7. Acceptance
-
-A contribution is not accepted merely because it was posted, uploaded, linked,
-emailed, or discussed. Unsolicited material will not be reviewed or accepted.
-The Owners or their authorized maintainers may modify, decline, or later
-remove previously accepted or expressly invited work.
-
-For work expressly invited in writing, intentionally delivering the requested
-material for Project inclusion after receiving notice of these terms records
-agreement to the applicable grants above. Maintainers must retain both the
-written invitation and the contributor's affirmative agreement in a durable
-Project record.
-
-Do not send unsolicited work. An owner invitation may instead specify a
-separate written arrangement signed by the appropriate parties.
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index f6cdd22..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,232 +0,0 @@
-GNU GENERAL PUBLIC LICENSE
-Version 3, 29 June 2007
-
-Copyright © 2007 Free Software Foundation, Inc.
-
-Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
-
-Preamble
-
-The GNU General Public License is a free, copyleft license for software and other kinds of works.
-
-The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too.
-
-When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
-
-To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others.
-
-For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
-
-Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it.
-
-For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions.
-
-Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users.
-
-Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free.
-
-The precise terms and conditions for copying, distribution and modification follow.
-
-TERMS AND CONDITIONS
-
-0. Definitions.
-
-“This License” refers to version 3 of the GNU General Public License.
-
-“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
-
-“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations.
-
-To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work.
-
-A “covered work” means either the unmodified Program or a work based on the Program.
-
-To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
-
-To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
-
-An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
-
-1. Source Code.
-The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work.
-
-A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
-
-The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
-
-The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work.
-
-The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
-
-The Corresponding Source for a work in source code form is that same work.
-
-2. Basic Permissions.
-All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
-
-You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
-
-Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
-
-3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
-
-When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
-
-4. Conveying Verbatim Copies.
-You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
-
-You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
-
-5. Conveying Modified Source Versions.
-You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”.
-
- c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
-
-A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
-
-6. Conveying Non-Source Forms.
-You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
-
- d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
-
-A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
-
-A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
-
-“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
-
-If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
-
-The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
-
-Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
-
-7. Additional Terms.
-“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
-
-When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
-
-Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
-
-All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
-
-If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
-
-Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
-
-8. Termination.
-You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
-
-However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
-
-Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
-
-Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
-
-9. Acceptance Not Required for Having Copies.
-You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
-
-10. Automatic Licensing of Downstream Recipients.
-Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
-
-An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
-
-You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
-
-11. Patents.
-A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”.
-
-A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
-
-Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
-
-In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
-
-If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
-
-If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
-
-A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
-
-Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
-
-12. No Surrender of Others' Freedom.
-If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
-
-13. Use with the GNU Affero General Public License.
-Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such.
-
-14. Revised Versions of this License.
-The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
-
-Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation.
-
-If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
-
-Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
-
-15. Disclaimer of Warranty.
-THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-16. Limitation of Liability.
-IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
-
-17. Interpretation of Sections 15 and 16.
-If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
-
-END OF TERMS AND CONDITIONS
-
-How to Apply These Terms to Your New Programs
-
-If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
-
-To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
-
-
- Copyright (C)
-
- This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with this program. If not, see .
-
-Also add information on how to contact you by electronic and paper mail.
-
-If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
-
- Copyright (C)
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.
-
-You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see .
-
-The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read .
diff --git a/README.md b/README.md
deleted file mode 100644
index e228e7b..0000000
--- a/README.md
+++ /dev/null
@@ -1,127 +0,0 @@
-# straywild
-
-straywild is a multiplayer-first fishing game built with Godot 4.7. The
-current pre-alpha includes host-authoritative fishing and economy flows,
-freshwater and saltwater habitats, persistent player data, social features,
-customization, jobs, and a controller-aware interface.
-
-The project is under active development. Interfaces, balance, content, and
-save migrations may continue to change before a stable release.
-
-straywild is developed and published by **Woofmeow**, the independent team of
-co-owners publicly credited as Voyager and Endeavour.
-
-## Links
-Patreon: https://www.patreon.com/cw/woofmeowgames
-
-Steam: https://store.steampowered.com/app/5068950/straywild/
-
-Itch.io: https://sol6-vi.itch.io/straywild
-
-Discord: https://discord.gg/5gP22447kc
-
-Matrix: https://matrix.to/#/#straywild:matrix.makearmy.io
-
-Repo: https://forge.makearmy.io/woofmeow/straywild
-
-Releases: https://forge.makearmy.io/woofmeow/straywild/releases
-
-
-## Installing release builds
-
-Download and extract the archive for your platform from the official release
-page, then run straywild from the extracted platform folder.
-
-### macOS
-
-Current macOS builds are not signed or notarized. macOS may block the first
-launch because it cannot verify the developer. Control-click the straywild
-application, choose **Open**, then confirm **Open**. If that option is not
-available, attempt to open the application once, then use **System Settings →
-Privacy & Security → Open Anyway**.
-
-Only bypass this warning for a straywild build downloaded from an official
-project release.
-
-## Requirements
-
-- Godot 4.7.x with the GL Compatibility renderer
-- Linux, Windows, or an Android development environment supported by Godot
-- Bash for the repository helper scripts
-
-Open `project.godot` in Godot, or start the editor from the repository root:
-
-```sh
-godot --editor --path .
-```
-
-Run the game directly with:
-
-```sh
-godot --path .
-```
-
-## Validation
-
-The validation scripts isolate Godot's settings and application-data roots.
-Run the fast, deterministic suite before committing a change:
-
-```sh
-scripts/run_validations.sh quick
-```
-
-Additional suites and individual commands are documented in the
-[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md#validation) validation section.
-
-## Building
-
-Export presets are maintained for Linux, Windows, and Android. Repository
-scripts expect the matching Godot export templates and platform toolchains to
-already be installed:
-
-```sh
-scripts/build_playtest.sh
-scripts/build_android_debug.sh
-```
-
-Build outputs belong under the ignored `builds/` directory. Release builds
-must follow the project's separate tagged-release checklist; these convenience
-scripts do not create or publish tags.
-
-## Project map
-
-- `main/`: composition root and application orchestration
-- `world/`: map, environment, water, and world presentation
-- `player/`, `fishing/`, `fish/`: player and fishing domain
-- `network/`: transport, identity, and host-authoritative services
-- `inventory/`, `items/`, `economy/`, `progression/`, `jobs/`: game state
-- `save/`, `settings/`: versioned persistence
-- `ui/`: gameplay and menu presentation
-- `tests/`: focused Godot validation scripts
-- `scripts/`: build, bake, and validation entry points
-
-All maintained project guidance starts at the
-[`docs/` documentation index](docs/README.md). Architecture, content policy,
-world and UI authoring, and testing are consolidated in
-[`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md).
-
-## Licensing
-
-- Project-owned software code is licensed under
- [GPL-3.0-or-later](LICENSE).
-- Project-owned artwork, models, music, and other creative assets are covered
- by the [straywild Asset License](ASSET-LICENSE.md). It permits community
- Mods for official straywild software and narrowly scoped contribution forks,
- but not independent forks, clones, or unrelated products.
-- The straywild and Woofmeow names and branding are reserved as described in
- [TRADEMARKS.md](TRADEMARKS.md).
-- Third-party materials retain their own terms; see the consolidated
- [credits, notices, and provenance record](docs/ATTRIBUTION.md).
-- Unsolicited contributions are not accepted. See
- [CONTRIBUTING.md](CONTRIBUTING.md). Historical and expressly invited work is
- governed by [CONTRIBUTOR-TERMS.md](CONTRIBUTOR-TERMS.md).
-
-Material boundaries and binary-release requirements are recorded in that same
-[`docs/ATTRIBUTION.md`](docs/ATTRIBUTION.md) reference.
-
-Copyright © 2026 Woofmeow.
diff --git a/TRADEMARKS.md b/TRADEMARKS.md
deleted file mode 100644
index fbf49d5..0000000
--- a/TRADEMARKS.md
+++ /dev/null
@@ -1,56 +0,0 @@
-# straywild and Woofmeow branding
-
-The straywild and Woofmeow names, logos, icons, trade dress, and other source
-identifiers are reserved by the straywild owners, publishing as Woofmeow.
-Their presence in a GPL-licensed repository does not grant trademark rights or
-imply permission to represent another product as official.
-
-No trademark registration is asserted by this notice. Applicable rights may
-exist through use and other law.
-
-## Permitted references
-
-You may use the names straywild and Woofmeow truthfully to:
-
-- refer to the official game or development team;
-- state that software interoperates with or is a Mod for straywild;
-- link to an official repository or distribution page; and
-- discuss, review, criticize, or report on the Project.
-
-Use only the amount of branding reasonably necessary to identify the Project.
-Do not imply sponsorship, certification, endorsement, or official status.
-
-An unofficial Mod may use wording such as “Example Mod for straywild” if its
-name and presentation remain clearly distinct and it displays an unofficial
-notice.
-
-## Contribution forks
-
-A qualifying Contribution Fork under `ASSET-LICENSE.md` may retain the
-straywild and Woofmeow names, logos, icons, and trade dress already present in
-the official source repository solely as necessary to reproduce, modify, and
-test the Project while preparing a proposed contribution. The source-control
-host's visible fork relationship or an equivalent repository notice must make
-its unofficial contribution purpose clear.
-
-This permission does not allow a Contribution Fork to use straywild or
-Woofmeow branding to advertise a public build, release, service, community
-edition, or independent product. It does not imply acceptance, sponsorship,
-endorsement, or official status.
-
-## Other forks and redistributed code
-
-The GNU GPL permits forks and redistribution of covered code. A fork or
-modified distribution that is not a qualifying Contribution Fork must:
-
-- use a distinct product name;
-- remove straywild and Woofmeow logos, icons, and official trade dress;
-- avoid confusingly similar names, domains, package identifiers, and account
- names; and
-- clearly identify itself as independent and unofficial.
-
-These branding terms do not limit rights granted by the GPL to the software
-code or lawful nominative, descriptive, comparative, or fair use.
-
-Permission for broader branding use must be obtained in writing from both
-straywild owners.
diff --git a/addons/straywild_shoreline_baker/plugin.cfg b/addons/straywild_shoreline_baker/plugin.cfg
deleted file mode 100644
index c3f4e47..0000000
--- a/addons/straywild_shoreline_baker/plugin.cfg
+++ /dev/null
@@ -1,7 +0,0 @@
-[plugin]
-
-name="straywild Shoreline Auto-Baker"
-description="Rebuilds configured static shoreline ribbons after authored terrain reimports."
-author="woofmeow"
-version="1.0.0"
-script="shoreline_auto_baker.gd"
diff --git a/addons/straywild_shoreline_baker/shoreline_auto_baker.gd b/addons/straywild_shoreline_baker/shoreline_auto_baker.gd
deleted file mode 100644
index 18da4af..0000000
--- a/addons/straywild_shoreline_baker/shoreline_auto_baker.gd
+++ /dev/null
@@ -1,66 +0,0 @@
-@tool
-extends EditorPlugin
-
-const TERRAIN_RESOURCE := (
- "res://art/exported/environment/terrain/starter_island.glb"
-)
-const REGION_SCENE := "res://world/regions/starter_island_region.tscn"
-
-var _bake_pending := false
-
-
-func _enter_tree() -> void:
- var filesystem := get_editor_interface().get_resource_filesystem()
- if not filesystem.resources_reimported.is_connected(_on_resources_reimported):
- filesystem.resources_reimported.connect(_on_resources_reimported)
-
-
-func _exit_tree() -> void:
- var filesystem := get_editor_interface().get_resource_filesystem()
- if filesystem.resources_reimported.is_connected(_on_resources_reimported):
- filesystem.resources_reimported.disconnect(_on_resources_reimported)
-
-
-func _on_resources_reimported(resources: PackedStringArray) -> void:
- if not resources.has(TERRAIN_RESOURCE) or _bake_pending:
- return
- _bake_pending = true
- call_deferred("_bake_configured_shorelines")
-
-
-func _bake_configured_shorelines() -> void:
- _bake_pending = false
- var packed_region := ResourceLoader.load(
- REGION_SCENE,
- "PackedScene",
- ResourceLoader.CACHE_MODE_REPLACE,
- ) as PackedScene
- if packed_region == null:
- push_error("Shoreline auto-bake could not load %s." % REGION_SCENE)
- return
- var region := packed_region.instantiate() as Node3D
- add_child(region)
- var baker := region.get_node_or_null(
- "ShorelineRibbonBaker"
- ) as ShorelineRibbonBaker
- if baker == null:
- push_error("Shoreline auto-bake found no configured baker.")
- region.queue_free()
- return
- var results := baker.rebuild_all()
- for result: Dictionary in results:
- if result.get("skipped", false):
- continue
- var output_path: String = result.get("output_path", "")
- if not output_path.is_empty():
- ResourceLoader.load(
- output_path,
- "ArrayMesh",
- ResourceLoader.CACHE_MODE_REPLACE,
- )
- print(
- "Shoreline auto-bake: %s, %d loops, %d triangles"
- % [output_path, result.loop_count, result.triangle_count]
- )
- region.queue_free()
- get_editor_interface().get_resource_filesystem().scan()
diff --git a/addons/straywild_shoreline_baker/shoreline_auto_baker.gd.uid b/addons/straywild_shoreline_baker/shoreline_auto_baker.gd.uid
deleted file mode 100644
index a143fcb..0000000
--- a/addons/straywild_shoreline_baker/shoreline_auto_baker.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cbk4xpalei5v3
diff --git a/art/exported/characters/base/straywild_base_character.glb b/art/exported/characters/base/straywild_base_character.glb
deleted file mode 100644
index 58eb7cd..0000000
Binary files a/art/exported/characters/base/straywild_base_character.glb and /dev/null differ
diff --git a/art/exported/characters/base/straywild_base_character.glb.import b/art/exported/characters/base/straywild_base_character.glb.import
deleted file mode 100644
index 96925de..0000000
--- a/art/exported/characters/base/straywild_base_character.glb.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://w7n4gjqq1juc"
-path="res://.godot/imported/straywild_base_character.glb-65021653e51a906d3b4a3494f170317f.scn"
-
-[deps]
-
-source_file="res://art/exported/characters/base/straywild_base_character.glb"
-dest_files=["res://.godot/imported/straywild_base_character.glb-65021653e51a906d3b4a3494f170317f.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-gltf/naming_version=2
-gltf/embedded_image_handling=1
-gltf/texture_map_mode=1
diff --git a/art/exported/characters/base/straywild_base_character_eyes_simple_shine.png b/art/exported/characters/base/straywild_base_character_eyes_simple_shine.png
deleted file mode 100644
index 36e6859..0000000
Binary files a/art/exported/characters/base/straywild_base_character_eyes_simple_shine.png and /dev/null differ
diff --git a/art/exported/characters/base/straywild_base_character_eyes_simple_shine.png.import b/art/exported/characters/base/straywild_base_character_eyes_simple_shine.png.import
deleted file mode 100644
index ddf8bb7..0000000
--- a/art/exported/characters/base/straywild_base_character_eyes_simple_shine.png.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dyjwgkgw0syo8"
-path.s3tc="res://.godot/imported/straywild_base_character_eyes_simple_shine.png-200a946ddace2078bbe44addb4e4524c.s3tc.ctex"
-path.etc2="res://.godot/imported/straywild_base_character_eyes_simple_shine.png-200a946ddace2078bbe44addb4e4524c.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-generator_parameters={
-"md5": "c2f7c29aaaa95809e140eb308d9bb132"
-}
-
-[deps]
-
-source_file="res://art/exported/characters/base/straywild_base_character_eyes_simple_shine.png"
-dest_files=["res://.godot/imported/straywild_base_character_eyes_simple_shine.png-200a946ddace2078bbe44addb4e4524c.s3tc.ctex", "res://.godot/imported/straywild_base_character_eyes_simple_shine.png-200a946ddace2078bbe44addb4e4524c.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/characters/base/straywild_base_character_mouth_three.png b/art/exported/characters/base/straywild_base_character_mouth_three.png
deleted file mode 100644
index 527f300..0000000
Binary files a/art/exported/characters/base/straywild_base_character_mouth_three.png and /dev/null differ
diff --git a/art/exported/characters/base/straywild_base_character_mouth_three.png.import b/art/exported/characters/base/straywild_base_character_mouth_three.png.import
deleted file mode 100644
index ba0ec2c..0000000
--- a/art/exported/characters/base/straywild_base_character_mouth_three.png.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dt5gitdtjdvcd"
-path.s3tc="res://.godot/imported/straywild_base_character_mouth_three.png-5bcf329b31a11eff496760a63e62d0e2.s3tc.ctex"
-path.etc2="res://.godot/imported/straywild_base_character_mouth_three.png-5bcf329b31a11eff496760a63e62d0e2.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-generator_parameters={
-"md5": "f8bf25a1006c5cc2ab5323f4a758eec7"
-}
-
-[deps]
-
-source_file="res://art/exported/characters/base/straywild_base_character_mouth_three.png"
-dest_files=["res://.godot/imported/straywild_base_character_mouth_three.png-5bcf329b31a11eff496760a63e62d0e2.s3tc.ctex", "res://.godot/imported/straywild_base_character_mouth_three.png-5bcf329b31a11eff496760a63e62d0e2.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/characters/base/straywild_base_character_nose_dog_round.png b/art/exported/characters/base/straywild_base_character_nose_dog_round.png
deleted file mode 100644
index 18a4590..0000000
Binary files a/art/exported/characters/base/straywild_base_character_nose_dog_round.png and /dev/null differ
diff --git a/art/exported/characters/base/straywild_base_character_nose_dog_round.png.import b/art/exported/characters/base/straywild_base_character_nose_dog_round.png.import
deleted file mode 100644
index cb2c573..0000000
--- a/art/exported/characters/base/straywild_base_character_nose_dog_round.png.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bmho1ryst3smt"
-path.s3tc="res://.godot/imported/straywild_base_character_nose_dog_round.png-e6909a2c139cf4a2748c2b79d1e12d92.s3tc.ctex"
-path.etc2="res://.godot/imported/straywild_base_character_nose_dog_round.png-e6909a2c139cf4a2748c2b79d1e12d92.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-generator_parameters={
-"md5": "3134f170152f9fa1512b41b61ecb01fc"
-}
-
-[deps]
-
-source_file="res://art/exported/characters/base/straywild_base_character_nose_dog_round.png"
-dest_files=["res://.godot/imported/straywild_base_character_nose_dog_round.png-e6909a2c139cf4a2748c2b79d1e12d92.s3tc.ctex", "res://.godot/imported/straywild_base_character_nose_dog_round.png-e6909a2c139cf4a2748c2b79d1e12d92.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_alligator_extra.png b/art/exported/characters/faces/eyes/eyes_alligator_extra.png
deleted file mode 100644
index e8c7be1..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_alligator_extra.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_alligator_extra.png.import b/art/exported/characters/faces/eyes/eyes_alligator_extra.png.import
deleted file mode 100644
index 6be8860..0000000
--- a/art/exported/characters/faces/eyes/eyes_alligator_extra.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c4rj1r1doawb4"
-path.s3tc="res://.godot/imported/eyes_alligator_extra.png-30730001eeef37c24fac0158f6613bd7.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_alligator_extra.png-30730001eeef37c24fac0158f6613bd7.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_alligator_extra.png"
-dest_files=["res://.godot/imported/eyes_alligator_extra.png-30730001eeef37c24fac0158f6613bd7.s3tc.ctex", "res://.godot/imported/eyes_alligator_extra.png-30730001eeef37c24fac0158f6613bd7.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_alligator_eyes.png b/art/exported/characters/faces/eyes/eyes_alligator_eyes.png
deleted file mode 100644
index 74736eb..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_alligator_eyes.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_alligator_eyes.png.import b/art/exported/characters/faces/eyes/eyes_alligator_eyes.png.import
deleted file mode 100644
index 410a5ef..0000000
--- a/art/exported/characters/faces/eyes/eyes_alligator_eyes.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://br2h5sfqmool8"
-path.s3tc="res://.godot/imported/eyes_alligator_eyes.png-843d36964858cdf7ffafe1972ffed681.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_alligator_eyes.png-843d36964858cdf7ffafe1972ffed681.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_alligator_eyes.png"
-dest_files=["res://.godot/imported/eyes_alligator_eyes.png-843d36964858cdf7ffafe1972ffed681.s3tc.ctex", "res://.godot/imported/eyes_alligator_eyes.png-843d36964858cdf7ffafe1972ffed681.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_angry.png b/art/exported/characters/faces/eyes/eyes_angry.png
deleted file mode 100644
index f88220e..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_angry.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_angry.png.import b/art/exported/characters/faces/eyes/eyes_angry.png.import
deleted file mode 100644
index ee3d3df..0000000
--- a/art/exported/characters/faces/eyes/eyes_angry.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b1ieqa8qos1vk"
-path.s3tc="res://.godot/imported/eyes_angry.png-a114dbeb3a8a14b8d1d86dd5a614b228.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_angry.png-a114dbeb3a8a14b8d1d86dd5a614b228.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_angry.png"
-dest_files=["res://.godot/imported/eyes_angry.png-a114dbeb3a8a14b8d1d86dd5a614b228.s3tc.ctex", "res://.godot/imported/eyes_angry.png-a114dbeb3a8a14b8d1d86dd5a614b228.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_anime_blue.png b/art/exported/characters/faces/eyes/eyes_anime_blue.png
deleted file mode 100644
index 39a6b86..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_anime_blue.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_anime_blue.png.import b/art/exported/characters/faces/eyes/eyes_anime_blue.png.import
deleted file mode 100644
index 6baa979..0000000
--- a/art/exported/characters/faces/eyes/eyes_anime_blue.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://2ecbidhb33vy"
-path.s3tc="res://.godot/imported/eyes_anime_blue.png-a86f72e375fc30cac2cd8320813c7c6c.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_anime_blue.png-a86f72e375fc30cac2cd8320813c7c6c.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_anime_blue.png"
-dest_files=["res://.godot/imported/eyes_anime_blue.png-a86f72e375fc30cac2cd8320813c7c6c.s3tc.ctex", "res://.godot/imported/eyes_anime_blue.png-a86f72e375fc30cac2cd8320813c7c6c.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_anime_green.png b/art/exported/characters/faces/eyes/eyes_anime_green.png
deleted file mode 100644
index e273c6e..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_anime_green.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_anime_green.png.import b/art/exported/characters/faces/eyes/eyes_anime_green.png.import
deleted file mode 100644
index 9c2a60c..0000000
--- a/art/exported/characters/faces/eyes/eyes_anime_green.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bwrxrtiv375ne"
-path.s3tc="res://.godot/imported/eyes_anime_green.png-a789a8f21c23e3085bca8992b3a3fc25.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_anime_green.png-a789a8f21c23e3085bca8992b3a3fc25.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_anime_green.png"
-dest_files=["res://.godot/imported/eyes_anime_green.png-a789a8f21c23e3085bca8992b3a3fc25.s3tc.ctex", "res://.godot/imported/eyes_anime_green.png-a789a8f21c23e3085bca8992b3a3fc25.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_beady.png b/art/exported/characters/faces/eyes/eyes_beady.png
deleted file mode 100644
index cdc5c2a..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_beady.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_beady.png.import b/art/exported/characters/faces/eyes/eyes_beady.png.import
deleted file mode 100644
index ce4ddea..0000000
--- a/art/exported/characters/faces/eyes/eyes_beady.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bahypju2dturm"
-path.s3tc="res://.godot/imported/eyes_beady.png-18d3b3ff0818d80f773d4f72d0b0061e.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_beady.png-18d3b3ff0818d80f773d4f72d0b0061e.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_beady.png"
-dest_files=["res://.godot/imported/eyes_beady.png-18d3b3ff0818d80f773d4f72d0b0061e.s3tc.ctex", "res://.godot/imported/eyes_beady.png-18d3b3ff0818d80f773d4f72d0b0061e.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_beady_extra.png b/art/exported/characters/faces/eyes/eyes_beady_extra.png
deleted file mode 100644
index 437d483..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_beady_extra.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_beady_extra.png.import b/art/exported/characters/faces/eyes/eyes_beady_extra.png.import
deleted file mode 100644
index 29babb1..0000000
--- a/art/exported/characters/faces/eyes/eyes_beady_extra.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bqgdrdc0hpi7s"
-path.s3tc="res://.godot/imported/eyes_beady_extra.png-d07d79345b1d22e55a1cf140f7682248.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_beady_extra.png-d07d79345b1d22e55a1cf140f7682248.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_beady_extra.png"
-dest_files=["res://.godot/imported/eyes_beady_extra.png-d07d79345b1d22e55a1cf140f7682248.s3tc.ctex", "res://.godot/imported/eyes_beady_extra.png-d07d79345b1d22e55a1cf140f7682248.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_beady_extra_shine.png b/art/exported/characters/faces/eyes/eyes_beady_extra_shine.png
deleted file mode 100644
index 6c2a02b..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_beady_extra_shine.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_beady_extra_shine.png.import b/art/exported/characters/faces/eyes/eyes_beady_extra_shine.png.import
deleted file mode 100644
index 047869d..0000000
--- a/art/exported/characters/faces/eyes/eyes_beady_extra_shine.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://mvjcdk84ko6f"
-path.s3tc="res://.godot/imported/eyes_beady_extra_shine.png-c72a35f216a8c1a06122f7bd743ca6a4.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_beady_extra_shine.png-c72a35f216a8c1a06122f7bd743ca6a4.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_beady_extra_shine.png"
-dest_files=["res://.godot/imported/eyes_beady_extra_shine.png-c72a35f216a8c1a06122f7bd743ca6a4.s3tc.ctex", "res://.godot/imported/eyes_beady_extra_shine.png-c72a35f216a8c1a06122f7bd743ca6a4.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_beady_eyebrows.png b/art/exported/characters/faces/eyes/eyes_beady_eyebrows.png
deleted file mode 100644
index 80b1077..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_beady_eyebrows.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_beady_eyebrows.png.import b/art/exported/characters/faces/eyes/eyes_beady_eyebrows.png.import
deleted file mode 100644
index 63a4b2f..0000000
--- a/art/exported/characters/faces/eyes/eyes_beady_eyebrows.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dp4sm67vn52um"
-path.s3tc="res://.godot/imported/eyes_beady_eyebrows.png-4f63e1a7ca603e9daf2d37a1d182e93f.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_beady_eyebrows.png-4f63e1a7ca603e9daf2d37a1d182e93f.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_beady_eyebrows.png"
-dest_files=["res://.godot/imported/eyes_beady_eyebrows.png-4f63e1a7ca603e9daf2d37a1d182e93f.s3tc.ctex", "res://.godot/imported/eyes_beady_eyebrows.png-4f63e1a7ca603e9daf2d37a1d182e93f.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_beady_whites.png b/art/exported/characters/faces/eyes/eyes_beady_whites.png
deleted file mode 100644
index 960fe50..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_beady_whites.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_beady_whites.png.import b/art/exported/characters/faces/eyes/eyes_beady_whites.png.import
deleted file mode 100644
index 8503f0e..0000000
--- a/art/exported/characters/faces/eyes/eyes_beady_whites.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cw34wh51s5sxi"
-path.s3tc="res://.godot/imported/eyes_beady_whites.png-87ce5dcbdfca9ca43a6f7c5ddca77bfa.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_beady_whites.png-87ce5dcbdfca9ca43a6f7c5ddca77bfa.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_beady_whites.png"
-dest_files=["res://.godot/imported/eyes_beady_whites.png-87ce5dcbdfca9ca43a6f7c5ddca77bfa.s3tc.ctex", "res://.godot/imported/eyes_beady_whites.png-87ce5dcbdfca9ca43a6f7c5ddca77bfa.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_carrot_eyes.png b/art/exported/characters/faces/eyes/eyes_carrot_eyes.png
deleted file mode 100644
index 5a11a4b..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_carrot_eyes.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_carrot_eyes.png.import b/art/exported/characters/faces/eyes/eyes_carrot_eyes.png.import
deleted file mode 100644
index 6444d02..0000000
--- a/art/exported/characters/faces/eyes/eyes_carrot_eyes.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://vn7mkjkx80ix"
-path.s3tc="res://.godot/imported/eyes_carrot_eyes.png-4d4af86c340a0dd0633449a9c6cc1bae.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_carrot_eyes.png-4d4af86c340a0dd0633449a9c6cc1bae.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_carrot_eyes.png"
-dest_files=["res://.godot/imported/eyes_carrot_eyes.png-4d4af86c340a0dd0633449a9c6cc1bae.s3tc.ctex", "res://.godot/imported/eyes_carrot_eyes.png-4d4af86c340a0dd0633449a9c6cc1bae.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_cat_real.png b/art/exported/characters/faces/eyes/eyes_cat_real.png
deleted file mode 100644
index 365dd61..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_cat_real.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_cat_real.png.import b/art/exported/characters/faces/eyes/eyes_cat_real.png.import
deleted file mode 100644
index 25675c9..0000000
--- a/art/exported/characters/faces/eyes/eyes_cat_real.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://sbxopn55femq"
-path.s3tc="res://.godot/imported/eyes_cat_real.png-13c9b6d280ac1000d177e74cbd33f07f.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_cat_real.png-13c9b6d280ac1000d177e74cbd33f07f.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_cat_real.png"
-dest_files=["res://.godot/imported/eyes_cat_real.png-13c9b6d280ac1000d177e74cbd33f07f.s3tc.ctex", "res://.godot/imported/eyes_cat_real.png-13c9b6d280ac1000d177e74cbd33f07f.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_cat_real_hunter.png b/art/exported/characters/faces/eyes/eyes_cat_real_hunter.png
deleted file mode 100644
index 17e6c94..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_cat_real_hunter.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_cat_real_hunter.png.import b/art/exported/characters/faces/eyes/eyes_cat_real_hunter.png.import
deleted file mode 100644
index ab1c3f5..0000000
--- a/art/exported/characters/faces/eyes/eyes_cat_real_hunter.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cg1l01m0ae2oa"
-path.s3tc="res://.godot/imported/eyes_cat_real_hunter.png-6eb486fc423d7e3fa84f71aa30ec04f3.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_cat_real_hunter.png-6eb486fc423d7e3fa84f71aa30ec04f3.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_cat_real_hunter.png"
-dest_files=["res://.godot/imported/eyes_cat_real_hunter.png-6eb486fc423d7e3fa84f71aa30ec04f3.s3tc.ctex", "res://.godot/imported/eyes_cat_real_hunter.png-6eb486fc423d7e3fa84f71aa30ec04f3.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_close.png b/art/exported/characters/faces/eyes/eyes_close.png
deleted file mode 100644
index 1e0123f..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_close.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_close.png.import b/art/exported/characters/faces/eyes/eyes_close.png.import
deleted file mode 100644
index e40b902..0000000
--- a/art/exported/characters/faces/eyes/eyes_close.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://kbigjk76ejx8"
-path.s3tc="res://.godot/imported/eyes_close.png-13a7da90295acf57ea489edcf26fc555.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_close.png-13a7da90295acf57ea489edcf26fc555.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_close.png"
-dest_files=["res://.godot/imported/eyes_close.png-13a7da90295acf57ea489edcf26fc555.s3tc.ctex", "res://.godot/imported/eyes_close.png-13a7da90295acf57ea489edcf26fc555.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_closed.png b/art/exported/characters/faces/eyes/eyes_closed.png
deleted file mode 100644
index d37f0a7..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_closed.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_closed.png.import b/art/exported/characters/faces/eyes/eyes_closed.png.import
deleted file mode 100644
index 325eb74..0000000
--- a/art/exported/characters/faces/eyes/eyes_closed.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b2ufsni8mnpmq"
-path.s3tc="res://.godot/imported/eyes_closed.png-80789498475c2cfbcb6e3ac14e067a76.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_closed.png-80789498475c2cfbcb6e3ac14e067a76.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_closed.png"
-dest_files=["res://.godot/imported/eyes_closed.png-80789498475c2cfbcb6e3ac14e067a76.s3tc.ctex", "res://.godot/imported/eyes_closed.png-80789498475c2cfbcb6e3ac14e067a76.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_cute.png b/art/exported/characters/faces/eyes/eyes_cute.png
deleted file mode 100644
index 1e078b9..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_cute.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_cute.png.import b/art/exported/characters/faces/eyes/eyes_cute.png.import
deleted file mode 100644
index b19f9ef..0000000
--- a/art/exported/characters/faces/eyes/eyes_cute.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b5455dbwse02f"
-path.s3tc="res://.godot/imported/eyes_cute.png-e2eabbdf5281c15927e85f1e2f22b127.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_cute.png-e2eabbdf5281c15927e85f1e2f22b127.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_cute.png"
-dest_files=["res://.godot/imported/eyes_cute.png-e2eabbdf5281c15927e85f1e2f22b127.s3tc.ctex", "res://.godot/imported/eyes_cute.png-e2eabbdf5281c15927e85f1e2f22b127.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_cute_close.png b/art/exported/characters/faces/eyes/eyes_cute_close.png
deleted file mode 100644
index c49f1c5..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_cute_close.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_cute_close.png.import b/art/exported/characters/faces/eyes/eyes_cute_close.png.import
deleted file mode 100644
index 9a3c9b2..0000000
--- a/art/exported/characters/faces/eyes/eyes_cute_close.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://lp21yuhfqbc1"
-path.s3tc="res://.godot/imported/eyes_cute_close.png-7a8a1b4e10864cfef604b9ced31a3f0c.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_cute_close.png-7a8a1b4e10864cfef604b9ced31a3f0c.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_cute_close.png"
-dest_files=["res://.godot/imported/eyes_cute_close.png-7a8a1b4e10864cfef604b9ced31a3f0c.s3tc.ctex", "res://.godot/imported/eyes_cute_close.png-7a8a1b4e10864cfef604b9ced31a3f0c.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_huge.png b/art/exported/characters/faces/eyes/eyes_huge.png
deleted file mode 100644
index 5c1026f..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_huge.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_huge.png.import b/art/exported/characters/faces/eyes/eyes_huge.png.import
deleted file mode 100644
index 7b55837..0000000
--- a/art/exported/characters/faces/eyes/eyes_huge.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://vb1eaupu8fpm"
-path.s3tc="res://.godot/imported/eyes_huge.png-9747926f369b1134a94639907057a08d.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_huge.png-9747926f369b1134a94639907057a08d.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_huge.png"
-dest_files=["res://.godot/imported/eyes_huge.png-9747926f369b1134a94639907057a08d.s3tc.ctex", "res://.godot/imported/eyes_huge.png-9747926f369b1134a94639907057a08d.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_interested.png b/art/exported/characters/faces/eyes/eyes_interested.png
deleted file mode 100644
index 4e1239e..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_interested.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_interested.png.import b/art/exported/characters/faces/eyes/eyes_interested.png.import
deleted file mode 100644
index 7f3a996..0000000
--- a/art/exported/characters/faces/eyes/eyes_interested.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b3481384do6m4"
-path.s3tc="res://.godot/imported/eyes_interested.png-5e8a34c0afaf7b4543f1b892af2227c7.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_interested.png-5e8a34c0afaf7b4543f1b892af2227c7.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_interested.png"
-dest_files=["res://.godot/imported/eyes_interested.png-5e8a34c0afaf7b4543f1b892af2227c7.s3tc.ctex", "res://.godot/imported/eyes_interested.png-5e8a34c0afaf7b4543f1b892af2227c7.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_jackolantern.png b/art/exported/characters/faces/eyes/eyes_jackolantern.png
deleted file mode 100644
index 98a0492..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_jackolantern.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_jackolantern.png.import b/art/exported/characters/faces/eyes/eyes_jackolantern.png.import
deleted file mode 100644
index 0515c3f..0000000
--- a/art/exported/characters/faces/eyes/eyes_jackolantern.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cdqqfoss11muq"
-path.s3tc="res://.godot/imported/eyes_jackolantern.png-16dc8591f3bd827988f8fcb90bbced59.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_jackolantern.png-16dc8591f3bd827988f8fcb90bbced59.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_jackolantern.png"
-dest_files=["res://.godot/imported/eyes_jackolantern.png-16dc8591f3bd827988f8fcb90bbced59.s3tc.ctex", "res://.godot/imported/eyes_jackolantern.png-16dc8591f3bd827988f8fcb90bbced59.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_jackolantern_small.png b/art/exported/characters/faces/eyes/eyes_jackolantern_small.png
deleted file mode 100644
index 1bb27fe..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_jackolantern_small.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_jackolantern_small.png.import b/art/exported/characters/faces/eyes/eyes_jackolantern_small.png.import
deleted file mode 100644
index 3c8fb9b..0000000
--- a/art/exported/characters/faces/eyes/eyes_jackolantern_small.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://6n6riqe60s2f"
-path.s3tc="res://.godot/imported/eyes_jackolantern_small.png-7608ab74472a3edb79fcddb1cafbcc2f.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_jackolantern_small.png-7608ab74472a3edb79fcddb1cafbcc2f.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_jackolantern_small.png"
-dest_files=["res://.godot/imported/eyes_jackolantern_small.png-7608ab74472a3edb79fcddb1cafbcc2f.s3tc.ctex", "res://.godot/imported/eyes_jackolantern_small.png-7608ab74472a3edb79fcddb1cafbcc2f.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_punk_purple.png b/art/exported/characters/faces/eyes/eyes_punk_purple.png
deleted file mode 100644
index 2ab8a17..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_punk_purple.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_punk_purple.png.import b/art/exported/characters/faces/eyes/eyes_punk_purple.png.import
deleted file mode 100644
index 5e58771..0000000
--- a/art/exported/characters/faces/eyes/eyes_punk_purple.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b2c784my766ag"
-path.s3tc="res://.godot/imported/eyes_punk_purple.png-fc0ccb3a5f1d2c23f2f367bb306d1ada.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_punk_purple.png-fc0ccb3a5f1d2c23f2f367bb306d1ada.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_punk_purple.png"
-dest_files=["res://.godot/imported/eyes_punk_purple.png-fc0ccb3a5f1d2c23f2f367bb306d1ada.s3tc.ctex", "res://.godot/imported/eyes_punk_purple.png-fc0ccb3a5f1d2c23f2f367bb306d1ada.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_sexy_blue.png b/art/exported/characters/faces/eyes/eyes_sexy_blue.png
deleted file mode 100644
index 3dffd6c..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_sexy_blue.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_sexy_blue.png.import b/art/exported/characters/faces/eyes/eyes_sexy_blue.png.import
deleted file mode 100644
index dc29314..0000000
--- a/art/exported/characters/faces/eyes/eyes_sexy_blue.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://drajn1tk8gq4a"
-path.s3tc="res://.godot/imported/eyes_sexy_blue.png-f99ecb6d89cb6c453161f6ad9fd5f802.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_sexy_blue.png-f99ecb6d89cb6c453161f6ad9fd5f802.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_sexy_blue.png"
-dest_files=["res://.godot/imported/eyes_sexy_blue.png-f99ecb6d89cb6c453161f6ad9fd5f802.s3tc.ctex", "res://.godot/imported/eyes_sexy_blue.png-f99ecb6d89cb6c453161f6ad9fd5f802.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_sexy_brown.png b/art/exported/characters/faces/eyes/eyes_sexy_brown.png
deleted file mode 100644
index bc53f8f..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_sexy_brown.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_sexy_brown.png.import b/art/exported/characters/faces/eyes/eyes_sexy_brown.png.import
deleted file mode 100644
index 3abd188..0000000
--- a/art/exported/characters/faces/eyes/eyes_sexy_brown.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c38u5tdksowpw"
-path.s3tc="res://.godot/imported/eyes_sexy_brown.png-e77cd183e70234d3ee7772ed2837cf34.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_sexy_brown.png-e77cd183e70234d3ee7772ed2837cf34.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_sexy_brown.png"
-dest_files=["res://.godot/imported/eyes_sexy_brown.png-e77cd183e70234d3ee7772ed2837cf34.s3tc.ctex", "res://.godot/imported/eyes_sexy_brown.png-e77cd183e70234d3ee7772ed2837cf34.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_spider.png b/art/exported/characters/faces/eyes/eyes_spider.png
deleted file mode 100644
index 239707f..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_spider.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_spider.png.import b/art/exported/characters/faces/eyes/eyes_spider.png.import
deleted file mode 100644
index b274e66..0000000
--- a/art/exported/characters/faces/eyes/eyes_spider.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bevrdbag3eixb"
-path.s3tc="res://.godot/imported/eyes_spider.png-4cbe85913f69207b18d4b23095aca596.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_spider.png-4cbe85913f69207b18d4b23095aca596.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_spider.png"
-dest_files=["res://.godot/imported/eyes_spider.png-4cbe85913f69207b18d4b23095aca596.s3tc.ctex", "res://.godot/imported/eyes_spider.png-4cbe85913f69207b18d4b23095aca596.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_surprised.png b/art/exported/characters/faces/eyes/eyes_surprised.png
deleted file mode 100644
index be78ec8..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_surprised.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_surprised.png.import b/art/exported/characters/faces/eyes/eyes_surprised.png.import
deleted file mode 100644
index d83d6c5..0000000
--- a/art/exported/characters/faces/eyes/eyes_surprised.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://chvod4aqeqn10"
-path.s3tc="res://.godot/imported/eyes_surprised.png-21858dc3f7520b1423e3d0aff59b6896.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_surprised.png-21858dc3f7520b1423e3d0aff59b6896.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_surprised.png"
-dest_files=["res://.godot/imported/eyes_surprised.png-21858dc3f7520b1423e3d0aff59b6896.s3tc.ctex", "res://.godot/imported/eyes_surprised.png-21858dc3f7520b1423e3d0aff59b6896.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_tired.png b/art/exported/characters/faces/eyes/eyes_tired.png
deleted file mode 100644
index 061b3e4..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_tired.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_tired.png.import b/art/exported/characters/faces/eyes/eyes_tired.png.import
deleted file mode 100644
index 70a45ab..0000000
--- a/art/exported/characters/faces/eyes/eyes_tired.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bimnlu17t6u6v"
-path.s3tc="res://.godot/imported/eyes_tired.png-752efd150fd50cc078ee25c8bbbeeb36.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_tired.png-752efd150fd50cc078ee25c8bbbeeb36.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_tired.png"
-dest_files=["res://.godot/imported/eyes_tired.png-752efd150fd50cc078ee25c8bbbeeb36.s3tc.ctex", "res://.godot/imported/eyes_tired.png-752efd150fd50cc078ee25c8bbbeeb36.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_tired_flat.png b/art/exported/characters/faces/eyes/eyes_tired_flat.png
deleted file mode 100644
index faf123c..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_tired_flat.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_tired_flat.png.import b/art/exported/characters/faces/eyes/eyes_tired_flat.png.import
deleted file mode 100644
index 831ff6d..0000000
--- a/art/exported/characters/faces/eyes/eyes_tired_flat.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b4hmrvoblu3cl"
-path.s3tc="res://.godot/imported/eyes_tired_flat.png-c441cfd39c6147e6a13989a4dd86be05.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_tired_flat.png-c441cfd39c6147e6a13989a4dd86be05.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_tired_flat.png"
-dest_files=["res://.godot/imported/eyes_tired_flat.png-c441cfd39c6147e6a13989a4dd86be05.s3tc.ctex", "res://.godot/imported/eyes_tired_flat.png-c441cfd39c6147e6a13989a4dd86be05.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/eyes/eyes_upset.png b/art/exported/characters/faces/eyes/eyes_upset.png
deleted file mode 100644
index 1e570a3..0000000
Binary files a/art/exported/characters/faces/eyes/eyes_upset.png and /dev/null differ
diff --git a/art/exported/characters/faces/eyes/eyes_upset.png.import b/art/exported/characters/faces/eyes/eyes_upset.png.import
deleted file mode 100644
index a0b51d5..0000000
--- a/art/exported/characters/faces/eyes/eyes_upset.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://blm1i6xyjclp5"
-path.s3tc="res://.godot/imported/eyes_upset.png-a8509f19cb25f577e84ebe86d28a31b1.s3tc.ctex"
-path.etc2="res://.godot/imported/eyes_upset.png-a8509f19cb25f577e84ebe86d28a31b1.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/eyes/eyes_upset.png"
-dest_files=["res://.godot/imported/eyes_upset.png-a8509f19cb25f577e84ebe86d28a31b1.s3tc.ctex", "res://.godot/imported/eyes_upset.png-a8509f19cb25f577e84ebe86d28a31b1.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_3.png b/art/exported/characters/faces/mouth/mouth_3.png
deleted file mode 100644
index dcdb30c..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_3.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_3.png.import b/art/exported/characters/faces/mouth/mouth_3.png.import
deleted file mode 100644
index 3da8cbf..0000000
--- a/art/exported/characters/faces/mouth/mouth_3.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b5wnxcm61q4ou"
-path.s3tc="res://.godot/imported/mouth_3.png-9fd9edc312c3b2c33df59da33d62bb30.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_3.png-9fd9edc312c3b2c33df59da33d62bb30.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_3.png"
-dest_files=["res://.godot/imported/mouth_3.png-9fd9edc312c3b2c33df59da33d62bb30.s3tc.ctex", "res://.godot/imported/mouth_3.png-9fd9edc312c3b2c33df59da33d62bb30.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_3_cheeky.png b/art/exported/characters/faces/mouth/mouth_3_cheeky.png
deleted file mode 100644
index b5f9797..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_3_cheeky.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_3_cheeky.png.import b/art/exported/characters/faces/mouth/mouth_3_cheeky.png.import
deleted file mode 100644
index f212e57..0000000
--- a/art/exported/characters/faces/mouth/mouth_3_cheeky.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dhaec7illng60"
-path.s3tc="res://.godot/imported/mouth_3_cheeky.png-1c9ae1bcb18ff29fe44e13b55b692f57.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_3_cheeky.png-1c9ae1bcb18ff29fe44e13b55b692f57.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_3_cheeky.png"
-dest_files=["res://.godot/imported/mouth_3_cheeky.png-1c9ae1bcb18ff29fe44e13b55b692f57.s3tc.ctex", "res://.godot/imported/mouth_3_cheeky.png-1c9ae1bcb18ff29fe44e13b55b692f57.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_buck teeth.png b/art/exported/characters/faces/mouth/mouth_buck teeth.png
deleted file mode 100644
index 9fde3c3..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_buck teeth.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_buck teeth.png.import b/art/exported/characters/faces/mouth/mouth_buck teeth.png.import
deleted file mode 100644
index 6e837c8..0000000
--- a/art/exported/characters/faces/mouth/mouth_buck teeth.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dmvxpg0jbiyqw"
-path.s3tc="res://.godot/imported/mouth_buck teeth.png-84be0f368b98b8640d5cc51d8a9b956f.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_buck teeth.png-84be0f368b98b8640d5cc51d8a9b956f.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_buck teeth.png"
-dest_files=["res://.godot/imported/mouth_buck teeth.png-84be0f368b98b8640d5cc51d8a9b956f.s3tc.ctex", "res://.godot/imported/mouth_buck teeth.png-84be0f368b98b8640d5cc51d8a9b956f.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_cheeky_smile.png b/art/exported/characters/faces/mouth/mouth_cheeky_smile.png
deleted file mode 100644
index addb335..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_cheeky_smile.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_cheeky_smile.png.import b/art/exported/characters/faces/mouth/mouth_cheeky_smile.png.import
deleted file mode 100644
index dce960a..0000000
--- a/art/exported/characters/faces/mouth/mouth_cheeky_smile.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://8tdtfnv3bgft"
-path.s3tc="res://.godot/imported/mouth_cheeky_smile.png-d55ec6f5fd7e73fd8793d524475f9f1f.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_cheeky_smile.png-d55ec6f5fd7e73fd8793d524475f9f1f.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_cheeky_smile.png"
-dest_files=["res://.godot/imported/mouth_cheeky_smile.png-d55ec6f5fd7e73fd8793d524475f9f1f.s3tc.ctex", "res://.godot/imported/mouth_cheeky_smile.png-d55ec6f5fd7e73fd8793d524475f9f1f.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_cheeky_smile_onesided.png b/art/exported/characters/faces/mouth/mouth_cheeky_smile_onesided.png
deleted file mode 100644
index cef99aa..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_cheeky_smile_onesided.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_cheeky_smile_onesided.png.import b/art/exported/characters/faces/mouth/mouth_cheeky_smile_onesided.png.import
deleted file mode 100644
index f500e42..0000000
--- a/art/exported/characters/faces/mouth/mouth_cheeky_smile_onesided.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://8xyawmp5gru8"
-path.s3tc="res://.godot/imported/mouth_cheeky_smile_onesided.png-366c346f7be47b1521677b84666a672b.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_cheeky_smile_onesided.png-366c346f7be47b1521677b84666a672b.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_cheeky_smile_onesided.png"
-dest_files=["res://.godot/imported/mouth_cheeky_smile_onesided.png-366c346f7be47b1521677b84666a672b.s3tc.ctex", "res://.godot/imported/mouth_cheeky_smile_onesided.png-366c346f7be47b1521677b84666a672b.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_jackolantern.png b/art/exported/characters/faces/mouth/mouth_jackolantern.png
deleted file mode 100644
index 80b5ebe..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_jackolantern.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_jackolantern.png.import b/art/exported/characters/faces/mouth/mouth_jackolantern.png.import
deleted file mode 100644
index 6c0861a..0000000
--- a/art/exported/characters/faces/mouth/mouth_jackolantern.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://ck4bbdnpf6ekd"
-path.s3tc="res://.godot/imported/mouth_jackolantern.png-7f9373a2b59e617edd25d92ef63041f0.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_jackolantern.png-7f9373a2b59e617edd25d92ef63041f0.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_jackolantern.png"
-dest_files=["res://.godot/imported/mouth_jackolantern.png-7f9373a2b59e617edd25d92ef63041f0.s3tc.ctex", "res://.godot/imported/mouth_jackolantern.png-7f9373a2b59e617edd25d92ef63041f0.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_jowls.png b/art/exported/characters/faces/mouth/mouth_jowls.png
deleted file mode 100644
index 05ecbd1..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_jowls.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_jowls.png.import b/art/exported/characters/faces/mouth/mouth_jowls.png.import
deleted file mode 100644
index 7f5823b..0000000
--- a/art/exported/characters/faces/mouth/mouth_jowls.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dyynmci0hf3vq"
-path.s3tc="res://.godot/imported/mouth_jowls.png-88cd6159c02a5b4eee74b98dc8248dcd.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_jowls.png-88cd6159c02a5b4eee74b98dc8248dcd.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_jowls.png"
-dest_files=["res://.godot/imported/mouth_jowls.png-88cd6159c02a5b4eee74b98dc8248dcd.s3tc.ctex", "res://.godot/imported/mouth_jowls.png-88cd6159c02a5b4eee74b98dc8248dcd.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_jowls_brock.png b/art/exported/characters/faces/mouth/mouth_jowls_brock.png
deleted file mode 100644
index 3b3d4a3..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_jowls_brock.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_jowls_brock.png.import b/art/exported/characters/faces/mouth/mouth_jowls_brock.png.import
deleted file mode 100644
index 0461534..0000000
--- a/art/exported/characters/faces/mouth/mouth_jowls_brock.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://duajvlqrieray"
-path.s3tc="res://.godot/imported/mouth_jowls_brock.png-de76cffdce72eb30289c44bcb2e68ce6.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_jowls_brock.png-de76cffdce72eb30289c44bcb2e68ce6.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_jowls_brock.png"
-dest_files=["res://.godot/imported/mouth_jowls_brock.png-de76cffdce72eb30289c44bcb2e68ce6.s3tc.ctex", "res://.godot/imported/mouth_jowls_brock.png-de76cffdce72eb30289c44bcb2e68ce6.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_jowls_simple.png b/art/exported/characters/faces/mouth/mouth_jowls_simple.png
deleted file mode 100644
index f96856e..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_jowls_simple.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_jowls_simple.png.import b/art/exported/characters/faces/mouth/mouth_jowls_simple.png.import
deleted file mode 100644
index fc1ad41..0000000
--- a/art/exported/characters/faces/mouth/mouth_jowls_simple.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c37gty8itl5oe"
-path.s3tc="res://.godot/imported/mouth_jowls_simple.png-0586400ebfd9ef98c9067f0ebbe730be.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_jowls_simple.png-0586400ebfd9ef98c9067f0ebbe730be.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_jowls_simple.png"
-dest_files=["res://.godot/imported/mouth_jowls_simple.png-0586400ebfd9ef98c9067f0ebbe730be.s3tc.ctex", "res://.godot/imported/mouth_jowls_simple.png-0586400ebfd9ef98c9067f0ebbe730be.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_kissy.png b/art/exported/characters/faces/mouth/mouth_kissy.png
deleted file mode 100644
index 65af5d9..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_kissy.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_kissy.png.import b/art/exported/characters/faces/mouth/mouth_kissy.png.import
deleted file mode 100644
index 1281fa1..0000000
--- a/art/exported/characters/faces/mouth/mouth_kissy.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://druncaq416lvu"
-path.s3tc="res://.godot/imported/mouth_kissy.png-52ec7a06301d8506497442f0ea0054a0.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_kissy.png-52ec7a06301d8506497442f0ea0054a0.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_kissy.png"
-dest_files=["res://.godot/imported/mouth_kissy.png-52ec7a06301d8506497442f0ea0054a0.s3tc.ctex", "res://.godot/imported/mouth_kissy.png-52ec7a06301d8506497442f0ea0054a0.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_kissy_lips.png b/art/exported/characters/faces/mouth/mouth_kissy_lips.png
deleted file mode 100644
index b20c0d1..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_kissy_lips.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_kissy_lips.png.import b/art/exported/characters/faces/mouth/mouth_kissy_lips.png.import
deleted file mode 100644
index 3778c31..0000000
--- a/art/exported/characters/faces/mouth/mouth_kissy_lips.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://babmei7rq8uaf"
-path.s3tc="res://.godot/imported/mouth_kissy_lips.png-a90d7b9649dbba8cc17232f59291a2c8.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_kissy_lips.png-a90d7b9649dbba8cc17232f59291a2c8.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_kissy_lips.png"
-dest_files=["res://.godot/imported/mouth_kissy_lips.png-a90d7b9649dbba8cc17232f59291a2c8.s3tc.ctex", "res://.godot/imported/mouth_kissy_lips.png-a90d7b9649dbba8cc17232f59291a2c8.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_lil_c.png b/art/exported/characters/faces/mouth/mouth_lil_c.png
deleted file mode 100644
index bd1219a..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_lil_c.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_lil_c.png.import b/art/exported/characters/faces/mouth/mouth_lil_c.png.import
deleted file mode 100644
index 06518f1..0000000
--- a/art/exported/characters/faces/mouth/mouth_lil_c.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bfafrjmy6liy6"
-path.s3tc="res://.godot/imported/mouth_lil_c.png-e65a240e587f99537a3a9f1d3f241323.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_lil_c.png-e65a240e587f99537a3a9f1d3f241323.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_lil_c.png"
-dest_files=["res://.godot/imported/mouth_lil_c.png-e65a240e587f99537a3a9f1d3f241323.s3tc.ctex", "res://.godot/imported/mouth_lil_c.png-e65a240e587f99537a3a9f1d3f241323.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_open_ah.png b/art/exported/characters/faces/mouth/mouth_open_ah.png
deleted file mode 100644
index b028f06..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_open_ah.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_open_ah.png.import b/art/exported/characters/faces/mouth/mouth_open_ah.png.import
deleted file mode 100644
index f37ae3a..0000000
--- a/art/exported/characters/faces/mouth/mouth_open_ah.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://44cqu18imydc"
-path.s3tc="res://.godot/imported/mouth_open_ah.png-2710d397264f57dbe8f9bac82dd4b0de.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_open_ah.png-2710d397264f57dbe8f9bac82dd4b0de.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_open_ah.png"
-dest_files=["res://.godot/imported/mouth_open_ah.png-2710d397264f57dbe8f9bac82dd4b0de.s3tc.ctex", "res://.godot/imported/mouth_open_ah.png-2710d397264f57dbe8f9bac82dd4b0de.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_open_oh.png b/art/exported/characters/faces/mouth/mouth_open_oh.png
deleted file mode 100644
index eba37ab..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_open_oh.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_open_oh.png.import b/art/exported/characters/faces/mouth/mouth_open_oh.png.import
deleted file mode 100644
index b5e7a1d..0000000
--- a/art/exported/characters/faces/mouth/mouth_open_oh.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c78hwv0t3ph47"
-path.s3tc="res://.godot/imported/mouth_open_oh.png-10fc59160b782879c415074831421088.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_open_oh.png-10fc59160b782879c415074831421088.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_open_oh.png"
-dest_files=["res://.godot/imported/mouth_open_oh.png-10fc59160b782879c415074831421088.s3tc.ctex", "res://.godot/imported/mouth_open_oh.png-10fc59160b782879c415074831421088.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_open_small.png b/art/exported/characters/faces/mouth/mouth_open_small.png
deleted file mode 100644
index 97c1755..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_open_small.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_open_small.png.import b/art/exported/characters/faces/mouth/mouth_open_small.png.import
deleted file mode 100644
index 5a83a6e..0000000
--- a/art/exported/characters/faces/mouth/mouth_open_small.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bn68lfabdo7ga"
-path.s3tc="res://.godot/imported/mouth_open_small.png-30e9081c075d21159d6563a981b15378.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_open_small.png-30e9081c075d21159d6563a981b15378.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_open_small.png"
-dest_files=["res://.godot/imported/mouth_open_small.png-30e9081c075d21159d6563a981b15378.s3tc.ctex", "res://.godot/imported/mouth_open_small.png-30e9081c075d21159d6563a981b15378.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_open_smile.png b/art/exported/characters/faces/mouth/mouth_open_smile.png
deleted file mode 100644
index 57675f8..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_open_smile.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_open_smile.png.import b/art/exported/characters/faces/mouth/mouth_open_smile.png.import
deleted file mode 100644
index 46b3100..0000000
--- a/art/exported/characters/faces/mouth/mouth_open_smile.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b12jrc4ao3r16"
-path.s3tc="res://.godot/imported/mouth_open_smile.png-2561ffea934645e2eac40ab3a0a6395a.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_open_smile.png-2561ffea934645e2eac40ab3a0a6395a.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_open_smile.png"
-dest_files=["res://.godot/imported/mouth_open_smile.png-2561ffea934645e2eac40ab3a0a6395a.s3tc.ctex", "res://.godot/imported/mouth_open_smile.png-2561ffea934645e2eac40ab3a0a6395a.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_p_tounge.png b/art/exported/characters/faces/mouth/mouth_p_tounge.png
deleted file mode 100644
index 7e71399..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_p_tounge.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_p_tounge.png.import b/art/exported/characters/faces/mouth/mouth_p_tounge.png.import
deleted file mode 100644
index a49c3ae..0000000
--- a/art/exported/characters/faces/mouth/mouth_p_tounge.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://nt1q6gpij6y3"
-path.s3tc="res://.godot/imported/mouth_p_tounge.png-f2e1b9018a58fa3c500b05c19941a1f3.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_p_tounge.png-f2e1b9018a58fa3c500b05c19941a1f3.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_p_tounge.png"
-dest_files=["res://.godot/imported/mouth_p_tounge.png-f2e1b9018a58fa3c500b05c19941a1f3.s3tc.ctex", "res://.godot/imported/mouth_p_tounge.png-f2e1b9018a58fa3c500b05c19941a1f3.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_sad.png b/art/exported/characters/faces/mouth/mouth_sad.png
deleted file mode 100644
index 86687e0..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_sad.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_sad.png.import b/art/exported/characters/faces/mouth/mouth_sad.png.import
deleted file mode 100644
index a0cce86..0000000
--- a/art/exported/characters/faces/mouth/mouth_sad.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://camwvowsgxnlw"
-path.s3tc="res://.godot/imported/mouth_sad.png-ced7361b391677e61acbb1877a8e7156.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_sad.png-ced7361b391677e61acbb1877a8e7156.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_sad.png"
-dest_files=["res://.godot/imported/mouth_sad.png-ced7361b391677e61acbb1877a8e7156.s3tc.ctex", "res://.godot/imported/mouth_sad.png-ced7361b391677e61acbb1877a8e7156.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_sigh.png b/art/exported/characters/faces/mouth/mouth_sigh.png
deleted file mode 100644
index 875da79..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_sigh.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_sigh.png.import b/art/exported/characters/faces/mouth/mouth_sigh.png.import
deleted file mode 100644
index 3199031..0000000
--- a/art/exported/characters/faces/mouth/mouth_sigh.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b0842jxg3m1ca"
-path.s3tc="res://.godot/imported/mouth_sigh.png-f58b2992f2709483fde720834a5e8820.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_sigh.png-f58b2992f2709483fde720834a5e8820.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_sigh.png"
-dest_files=["res://.godot/imported/mouth_sigh.png-f58b2992f2709483fde720834a5e8820.s3tc.ctex", "res://.godot/imported/mouth_sigh.png-f58b2992f2709483fde720834a5e8820.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_simple_smile.png b/art/exported/characters/faces/mouth/mouth_simple_smile.png
deleted file mode 100644
index 9f8d430..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_simple_smile.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_simple_smile.png.import b/art/exported/characters/faces/mouth/mouth_simple_smile.png.import
deleted file mode 100644
index c71a778..0000000
--- a/art/exported/characters/faces/mouth/mouth_simple_smile.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://nafybekpya31"
-path.s3tc="res://.godot/imported/mouth_simple_smile.png-029050835f365c4976c1b4e62edbc7c8.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_simple_smile.png-029050835f365c4976c1b4e62edbc7c8.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_simple_smile.png"
-dest_files=["res://.godot/imported/mouth_simple_smile.png-029050835f365c4976c1b4e62edbc7c8.s3tc.ctex", "res://.godot/imported/mouth_simple_smile.png-029050835f365c4976c1b4e62edbc7c8.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_slanted.png b/art/exported/characters/faces/mouth/mouth_slanted.png
deleted file mode 100644
index 568eb4f..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_slanted.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_slanted.png.import b/art/exported/characters/faces/mouth/mouth_slanted.png.import
deleted file mode 100644
index 80611ac..0000000
--- a/art/exported/characters/faces/mouth/mouth_slanted.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://nuj6ednwjawn"
-path.s3tc="res://.godot/imported/mouth_slanted.png-cb0a217a5bb11b80e53b1a6e3e94f43d.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_slanted.png-cb0a217a5bb11b80e53b1a6e3e94f43d.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_slanted.png"
-dest_files=["res://.godot/imported/mouth_slanted.png-cb0a217a5bb11b80e53b1a6e3e94f43d.s3tc.ctex", "res://.godot/imported/mouth_slanted.png-cb0a217a5bb11b80e53b1a6e3e94f43d.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_squigle.png b/art/exported/characters/faces/mouth/mouth_squigle.png
deleted file mode 100644
index cffb2e8..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_squigle.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_squigle.png.import b/art/exported/characters/faces/mouth/mouth_squigle.png.import
deleted file mode 100644
index 22c7de7..0000000
--- a/art/exported/characters/faces/mouth/mouth_squigle.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://calw27urbe3hk"
-path.s3tc="res://.godot/imported/mouth_squigle.png-263a4acb09e9f29aff38899ef1aac53f.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_squigle.png-263a4acb09e9f29aff38899ef1aac53f.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_squigle.png"
-dest_files=["res://.godot/imported/mouth_squigle.png-263a4acb09e9f29aff38899ef1aac53f.s3tc.ctex", "res://.godot/imported/mouth_squigle.png-263a4acb09e9f29aff38899ef1aac53f.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_vampy.png b/art/exported/characters/faces/mouth/mouth_vampy.png
deleted file mode 100644
index ee941f2..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_vampy.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_vampy.png.import b/art/exported/characters/faces/mouth/mouth_vampy.png.import
deleted file mode 100644
index cd3d5c0..0000000
--- a/art/exported/characters/faces/mouth/mouth_vampy.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bdpe0lrftqjkq"
-path.s3tc="res://.godot/imported/mouth_vampy.png-0f16a1970484a70927f4063fa1bab422.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_vampy.png-0f16a1970484a70927f4063fa1bab422.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_vampy.png"
-dest_files=["res://.godot/imported/mouth_vampy.png-0f16a1970484a70927f4063fa1bab422.s3tc.ctex", "res://.godot/imported/mouth_vampy.png-0f16a1970484a70927f4063fa1bab422.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_whiskers.png b/art/exported/characters/faces/mouth/mouth_whiskers.png
deleted file mode 100644
index 414634f..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_whiskers.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_whiskers.png.import b/art/exported/characters/faces/mouth/mouth_whiskers.png.import
deleted file mode 100644
index 799b0f1..0000000
--- a/art/exported/characters/faces/mouth/mouth_whiskers.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cpxwladf5vobh"
-path.s3tc="res://.godot/imported/mouth_whiskers.png-27b3d6495cd4a57de394a2ad8eb67d39.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_whiskers.png-27b3d6495cd4a57de394a2ad8eb67d39.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_whiskers.png"
-dest_files=["res://.godot/imported/mouth_whiskers.png-27b3d6495cd4a57de394a2ad8eb67d39.s3tc.ctex", "res://.godot/imported/mouth_whiskers.png-27b3d6495cd4a57de394a2ad8eb67d39.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_y.png b/art/exported/characters/faces/mouth/mouth_y.png
deleted file mode 100644
index 600cd8c..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_y.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_y.png.import b/art/exported/characters/faces/mouth/mouth_y.png.import
deleted file mode 100644
index 916f91f..0000000
--- a/art/exported/characters/faces/mouth/mouth_y.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://6w65l4rjbgq"
-path.s3tc="res://.godot/imported/mouth_y.png-08338d9ca7fd2d139dd64add4385096d.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_y.png-08338d9ca7fd2d139dd64add4385096d.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_y.png"
-dest_files=["res://.godot/imported/mouth_y.png-08338d9ca7fd2d139dd64add4385096d.s3tc.ctex", "res://.godot/imported/mouth_y.png-08338d9ca7fd2d139dd64add4385096d.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_y_small.png b/art/exported/characters/faces/mouth/mouth_y_small.png
deleted file mode 100644
index f66357b..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_y_small.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_y_small.png.import b/art/exported/characters/faces/mouth/mouth_y_small.png.import
deleted file mode 100644
index 80dedf9..0000000
--- a/art/exported/characters/faces/mouth/mouth_y_small.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cchv8fvnqbtm2"
-path.s3tc="res://.godot/imported/mouth_y_small.png-493215adfb2e980f01f69152d24a8ef4.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_y_small.png-493215adfb2e980f01f69152d24a8ef4.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_y_small.png"
-dest_files=["res://.godot/imported/mouth_y_small.png-493215adfb2e980f01f69152d24a8ef4.s3tc.ctex", "res://.godot/imported/mouth_y_small.png-493215adfb2e980f01f69152d24a8ef4.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/mouth/mouth_y_small_pink.png b/art/exported/characters/faces/mouth/mouth_y_small_pink.png
deleted file mode 100644
index 7f0af4a..0000000
Binary files a/art/exported/characters/faces/mouth/mouth_y_small_pink.png and /dev/null differ
diff --git a/art/exported/characters/faces/mouth/mouth_y_small_pink.png.import b/art/exported/characters/faces/mouth/mouth_y_small_pink.png.import
deleted file mode 100644
index efe6ac8..0000000
--- a/art/exported/characters/faces/mouth/mouth_y_small_pink.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dvqdmhhiet5ix"
-path.s3tc="res://.godot/imported/mouth_y_small_pink.png-febc93e05453be68458eff8bab3d21c1.s3tc.ctex"
-path.etc2="res://.godot/imported/mouth_y_small_pink.png-febc93e05453be68458eff8bab3d21c1.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/mouth/mouth_y_small_pink.png"
-dest_files=["res://.godot/imported/mouth_y_small_pink.png-febc93e05453be68458eff8bab3d21c1.s3tc.ctex", "res://.godot/imported/mouth_y_small_pink.png-febc93e05453be68458eff8bab3d21c1.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_cat_real.png b/art/exported/characters/faces/noses/nose_cat_real.png
deleted file mode 100644
index 0765f2e..0000000
Binary files a/art/exported/characters/faces/noses/nose_cat_real.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_cat_real.png.import b/art/exported/characters/faces/noses/nose_cat_real.png.import
deleted file mode 100644
index 8e1a496..0000000
--- a/art/exported/characters/faces/noses/nose_cat_real.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://5n0kihgof38n"
-path.s3tc="res://.godot/imported/nose_cat_real.png-25c7e8464e384043f0969f191e2c2950.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_cat_real.png-25c7e8464e384043f0969f191e2c2950.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_cat_real.png"
-dest_files=["res://.godot/imported/nose_cat_real.png-25c7e8464e384043f0969f191e2c2950.s3tc.ctex", "res://.godot/imported/nose_cat_real.png-25c7e8464e384043f0969f191e2c2950.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_clown.png b/art/exported/characters/faces/noses/nose_clown.png
deleted file mode 100644
index cb9fb14..0000000
Binary files a/art/exported/characters/faces/noses/nose_clown.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_clown.png.import b/art/exported/characters/faces/noses/nose_clown.png.import
deleted file mode 100644
index 1a642e3..0000000
--- a/art/exported/characters/faces/noses/nose_clown.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cc0nb34t7helb"
-path.s3tc="res://.godot/imported/nose_clown.png-bfa33a86cf06540d45ba440868a39bb3.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_clown.png-bfa33a86cf06540d45ba440868a39bb3.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_clown.png"
-dest_files=["res://.godot/imported/nose_clown.png-bfa33a86cf06540d45ba440868a39bb3.s3tc.ctex", "res://.godot/imported/nose_clown.png-bfa33a86cf06540d45ba440868a39bb3.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_dog_real.png b/art/exported/characters/faces/noses/nose_dog_real.png
deleted file mode 100644
index 01b3a7e..0000000
Binary files a/art/exported/characters/faces/noses/nose_dog_real.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_dog_real.png.import b/art/exported/characters/faces/noses/nose_dog_real.png.import
deleted file mode 100644
index 316c80e..0000000
--- a/art/exported/characters/faces/noses/nose_dog_real.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b8jwky86fl3ky"
-path.s3tc="res://.godot/imported/nose_dog_real.png-f065da766aead62076c01fa08fbffb2a.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_dog_real.png-f065da766aead62076c01fa08fbffb2a.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_dog_real.png"
-dest_files=["res://.godot/imported/nose_dog_real.png-f065da766aead62076c01fa08fbffb2a.s3tc.ctex", "res://.godot/imported/nose_dog_real.png-f065da766aead62076c01fa08fbffb2a.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_dog_simple.png b/art/exported/characters/faces/noses/nose_dog_simple.png
deleted file mode 100644
index d3743c7..0000000
Binary files a/art/exported/characters/faces/noses/nose_dog_simple.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_dog_simple.png.import b/art/exported/characters/faces/noses/nose_dog_simple.png.import
deleted file mode 100644
index 344af2e..0000000
--- a/art/exported/characters/faces/noses/nose_dog_simple.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b6d4ubqjijjux"
-path.s3tc="res://.godot/imported/nose_dog_simple.png-fdc0a92e3e2c82271db9ea87256058a7.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_dog_simple.png-fdc0a92e3e2c82271db9ea87256058a7.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_dog_simple.png"
-dest_files=["res://.godot/imported/nose_dog_simple.png-fdc0a92e3e2c82271db9ea87256058a7.s3tc.ctex", "res://.godot/imported/nose_dog_simple.png-fdc0a92e3e2c82271db9ea87256058a7.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_dragon.png b/art/exported/characters/faces/noses/nose_dragon.png
deleted file mode 100644
index 157a28d..0000000
Binary files a/art/exported/characters/faces/noses/nose_dragon.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_dragon.png.import b/art/exported/characters/faces/noses/nose_dragon.png.import
deleted file mode 100644
index 92d98eb..0000000
--- a/art/exported/characters/faces/noses/nose_dragon.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://8n1bhbyt5id6"
-path.s3tc="res://.godot/imported/nose_dragon.png-6c98cf6f5692ec43ffe43650115cd010.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_dragon.png-6c98cf6f5692ec43ffe43650115cd010.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_dragon.png"
-dest_files=["res://.godot/imported/nose_dragon.png-6c98cf6f5692ec43ffe43650115cd010.s3tc.ctex", "res://.godot/imported/nose_dragon.png-6c98cf6f5692ec43ffe43650115cd010.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_hippo.png b/art/exported/characters/faces/noses/nose_hippo.png
deleted file mode 100644
index 7ceabb2..0000000
Binary files a/art/exported/characters/faces/noses/nose_hippo.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_hippo.png.import b/art/exported/characters/faces/noses/nose_hippo.png.import
deleted file mode 100644
index 3e5736f..0000000
--- a/art/exported/characters/faces/noses/nose_hippo.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bmfa44uf6u4or"
-path.s3tc="res://.godot/imported/nose_hippo.png-059f63ab00597454d369b0d51b5712a6.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_hippo.png-059f63ab00597454d369b0d51b5712a6.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_hippo.png"
-dest_files=["res://.godot/imported/nose_hippo.png-059f63ab00597454d369b0d51b5712a6.s3tc.ctex", "res://.godot/imported/nose_hippo.png-059f63ab00597454d369b0d51b5712a6.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_human.png b/art/exported/characters/faces/noses/nose_human.png
deleted file mode 100644
index e0e7846..0000000
Binary files a/art/exported/characters/faces/noses/nose_human.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_human.png.import b/art/exported/characters/faces/noses/nose_human.png.import
deleted file mode 100644
index 486fa00..0000000
--- a/art/exported/characters/faces/noses/nose_human.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cuthoacwggro3"
-path.s3tc="res://.godot/imported/nose_human.png-337d9ba4f724d6241a6fa9a307a30008.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_human.png-337d9ba4f724d6241a6fa9a307a30008.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_human.png"
-dest_files=["res://.godot/imported/nose_human.png-337d9ba4f724d6241a6fa9a307a30008.s3tc.ctex", "res://.godot/imported/nose_human.png-337d9ba4f724d6241a6fa9a307a30008.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_nostrils.png b/art/exported/characters/faces/noses/nose_nostrils.png
deleted file mode 100644
index 11fb0a3..0000000
Binary files a/art/exported/characters/faces/noses/nose_nostrils.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_nostrils.png.import b/art/exported/characters/faces/noses/nose_nostrils.png.import
deleted file mode 100644
index 68ae74a..0000000
--- a/art/exported/characters/faces/noses/nose_nostrils.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bvu2lox5px3ab"
-path.s3tc="res://.godot/imported/nose_nostrils.png-9e1c2b5c8e7adb019d72a39b1e805781.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_nostrils.png-9e1c2b5c8e7adb019d72a39b1e805781.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_nostrils.png"
-dest_files=["res://.godot/imported/nose_nostrils.png-9e1c2b5c8e7adb019d72a39b1e805781.s3tc.ctex", "res://.godot/imported/nose_nostrils.png-9e1c2b5c8e7adb019d72a39b1e805781.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_piercing.png b/art/exported/characters/faces/noses/nose_piercing.png
deleted file mode 100644
index f35b3ac..0000000
Binary files a/art/exported/characters/faces/noses/nose_piercing.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_piercing.png.import b/art/exported/characters/faces/noses/nose_piercing.png.import
deleted file mode 100644
index d50d3e0..0000000
--- a/art/exported/characters/faces/noses/nose_piercing.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://3w37sblwbd8b"
-path.s3tc="res://.godot/imported/nose_piercing.png-0c3316905c9f379988cc52353c88569c.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_piercing.png-0c3316905c9f379988cc52353c88569c.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_piercing.png"
-dest_files=["res://.godot/imported/nose_piercing.png-0c3316905c9f379988cc52353c88569c.s3tc.ctex", "res://.godot/imported/nose_piercing.png-0c3316905c9f379988cc52353c88569c.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_pig_snout.png b/art/exported/characters/faces/noses/nose_pig_snout.png
deleted file mode 100644
index 0e403cf..0000000
Binary files a/art/exported/characters/faces/noses/nose_pig_snout.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_pig_snout.png.import b/art/exported/characters/faces/noses/nose_pig_snout.png.import
deleted file mode 100644
index 9ead70a..0000000
--- a/art/exported/characters/faces/noses/nose_pig_snout.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bvran6dr0u22l"
-path.s3tc="res://.godot/imported/nose_pig_snout.png-40e08884db61f222d4371c6c6a798bca.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_pig_snout.png-40e08884db61f222d4371c6c6a798bca.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_pig_snout.png"
-dest_files=["res://.godot/imported/nose_pig_snout.png-40e08884db61f222d4371c6c6a798bca.s3tc.ctex", "res://.godot/imported/nose_pig_snout.png-40e08884db61f222d4371c6c6a798bca.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_pig_snout_pink.png b/art/exported/characters/faces/noses/nose_pig_snout_pink.png
deleted file mode 100644
index 6fe994f..0000000
Binary files a/art/exported/characters/faces/noses/nose_pig_snout_pink.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_pig_snout_pink.png.import b/art/exported/characters/faces/noses/nose_pig_snout_pink.png.import
deleted file mode 100644
index c66209b..0000000
--- a/art/exported/characters/faces/noses/nose_pig_snout_pink.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cyigqco0dp8ph"
-path.s3tc="res://.godot/imported/nose_pig_snout_pink.png-da6fcfc06a8aa02d6ad664e2ed669558.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_pig_snout_pink.png-da6fcfc06a8aa02d6ad664e2ed669558.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_pig_snout_pink.png"
-dest_files=["res://.godot/imported/nose_pig_snout_pink.png-da6fcfc06a8aa02d6ad664e2ed669558.s3tc.ctex", "res://.godot/imported/nose_pig_snout_pink.png-da6fcfc06a8aa02d6ad664e2ed669558.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_squidward.png b/art/exported/characters/faces/noses/nose_squidward.png
deleted file mode 100644
index a878daa..0000000
Binary files a/art/exported/characters/faces/noses/nose_squidward.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_squidward.png.import b/art/exported/characters/faces/noses/nose_squidward.png.import
deleted file mode 100644
index ab07b93..0000000
--- a/art/exported/characters/faces/noses/nose_squidward.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://ilvb5vre4kv7"
-path.s3tc="res://.godot/imported/nose_squidward.png-579e15cf6acfb402abda29a8a555d504.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_squidward.png-579e15cf6acfb402abda29a8a555d504.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_squidward.png"
-dest_files=["res://.godot/imported/nose_squidward.png-579e15cf6acfb402abda29a8a555d504.s3tc.ctex", "res://.godot/imported/nose_squidward.png-579e15cf6acfb402abda29a8a555d504.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_squidward_wide.png b/art/exported/characters/faces/noses/nose_squidward_wide.png
deleted file mode 100644
index 1f55a69..0000000
Binary files a/art/exported/characters/faces/noses/nose_squidward_wide.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_squidward_wide.png.import b/art/exported/characters/faces/noses/nose_squidward_wide.png.import
deleted file mode 100644
index cf307ce..0000000
--- a/art/exported/characters/faces/noses/nose_squidward_wide.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dtisqywq2rv14"
-path.s3tc="res://.godot/imported/nose_squidward_wide.png-fca231f913f03b97c273c870770f7948.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_squidward_wide.png-fca231f913f03b97c273c870770f7948.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_squidward_wide.png"
-dest_files=["res://.godot/imported/nose_squidward_wide.png-fca231f913f03b97c273c870770f7948.s3tc.ctex", "res://.godot/imported/nose_squidward_wide.png-fca231f913f03b97c273c870770f7948.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_triangle.png b/art/exported/characters/faces/noses/nose_triangle.png
deleted file mode 100644
index 5f15f3a..0000000
Binary files a/art/exported/characters/faces/noses/nose_triangle.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_triangle.png.import b/art/exported/characters/faces/noses/nose_triangle.png.import
deleted file mode 100644
index ba494a2..0000000
--- a/art/exported/characters/faces/noses/nose_triangle.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bitins3gtewe"
-path.s3tc="res://.godot/imported/nose_triangle.png-018974a6bbf68652b8c5533c2675cc2f.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_triangle.png-018974a6bbf68652b8c5533c2675cc2f.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_triangle.png"
-dest_files=["res://.godot/imported/nose_triangle.png-018974a6bbf68652b8c5533c2675cc2f.s3tc.ctex", "res://.godot/imported/nose_triangle.png-018974a6bbf68652b8c5533c2675cc2f.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_triangle_pink.png b/art/exported/characters/faces/noses/nose_triangle_pink.png
deleted file mode 100644
index 2915456..0000000
Binary files a/art/exported/characters/faces/noses/nose_triangle_pink.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_triangle_pink.png.import b/art/exported/characters/faces/noses/nose_triangle_pink.png.import
deleted file mode 100644
index eff0bb1..0000000
--- a/art/exported/characters/faces/noses/nose_triangle_pink.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://oi8qhxbtijmk"
-path.s3tc="res://.godot/imported/nose_triangle_pink.png-051ce6df5e7fd5932f19fd353123d7d1.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_triangle_pink.png-051ce6df5e7fd5932f19fd353123d7d1.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_triangle_pink.png"
-dest_files=["res://.godot/imported/nose_triangle_pink.png-051ce6df5e7fd5932f19fd353123d7d1.s3tc.ctex", "res://.godot/imported/nose_triangle_pink.png-051ce6df5e7fd5932f19fd353123d7d1.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_triangle_small.png b/art/exported/characters/faces/noses/nose_triangle_small.png
deleted file mode 100644
index 719e67b..0000000
Binary files a/art/exported/characters/faces/noses/nose_triangle_small.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_triangle_small.png.import b/art/exported/characters/faces/noses/nose_triangle_small.png.import
deleted file mode 100644
index e704938..0000000
--- a/art/exported/characters/faces/noses/nose_triangle_small.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cedgrca26ponk"
-path.s3tc="res://.godot/imported/nose_triangle_small.png-64129c5fc69bf7095f684c2b7de2855b.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_triangle_small.png-64129c5fc69bf7095f684c2b7de2855b.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_triangle_small.png"
-dest_files=["res://.godot/imported/nose_triangle_small.png-64129c5fc69bf7095f684c2b7de2855b.s3tc.ctex", "res://.godot/imported/nose_triangle_small.png-64129c5fc69bf7095f684c2b7de2855b.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_triangle_small_pink.png b/art/exported/characters/faces/noses/nose_triangle_small_pink.png
deleted file mode 100644
index 3a4bf19..0000000
Binary files a/art/exported/characters/faces/noses/nose_triangle_small_pink.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_triangle_small_pink.png.import b/art/exported/characters/faces/noses/nose_triangle_small_pink.png.import
deleted file mode 100644
index b3060e6..0000000
--- a/art/exported/characters/faces/noses/nose_triangle_small_pink.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://0kird82ka1ug"
-path.s3tc="res://.godot/imported/nose_triangle_small_pink.png-fba736a5f1806c1286f1d3040592933b.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_triangle_small_pink.png-fba736a5f1806c1286f1d3040592933b.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_triangle_small_pink.png"
-dest_files=["res://.godot/imported/nose_triangle_small_pink.png-fba736a5f1806c1286f1d3040592933b.s3tc.ctex", "res://.godot/imported/nose_triangle_small_pink.png-fba736a5f1806c1286f1d3040592933b.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/faces/noses/nose_under.png b/art/exported/characters/faces/noses/nose_under.png
deleted file mode 100644
index d7c4d7b..0000000
Binary files a/art/exported/characters/faces/noses/nose_under.png and /dev/null differ
diff --git a/art/exported/characters/faces/noses/nose_under.png.import b/art/exported/characters/faces/noses/nose_under.png.import
deleted file mode 100644
index d0c87ea..0000000
--- a/art/exported/characters/faces/noses/nose_under.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://u0ih35v73j5h"
-path.s3tc="res://.godot/imported/nose_under.png-269bae96966b3c6367d43ac9bd5b2a3f.s3tc.ctex"
-path.etc2="res://.godot/imported/nose_under.png-269bae96966b3c6367d43ac9bd5b2a3f.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/characters/faces/noses/nose_under.png"
-dest_files=["res://.godot/imported/nose_under.png-269bae96966b3c6367d43ac9bd5b2a3f.s3tc.ctex", "res://.godot/imported/nose_under.png-269bae96966b3c6367d43ac9bd5b2a3f.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=512
-detect_3d/compress_to=0
diff --git a/art/exported/characters/patterns/bengal/body_arms_bengal.png b/art/exported/characters/patterns/bengal/body_arms_bengal.png
deleted file mode 100644
index dd4c735..0000000
Binary files a/art/exported/characters/patterns/bengal/body_arms_bengal.png and /dev/null differ
diff --git a/art/exported/characters/patterns/bengal/body_main_bengal.png b/art/exported/characters/patterns/bengal/body_main_bengal.png
deleted file mode 100644
index c6eae5b..0000000
Binary files a/art/exported/characters/patterns/bengal/body_main_bengal.png and /dev/null differ
diff --git a/art/exported/characters/patterns/bengal/head_round_bengal.png b/art/exported/characters/patterns/bengal/head_round_bengal.png
deleted file mode 100644
index d5326c4..0000000
Binary files a/art/exported/characters/patterns/bengal/head_round_bengal.png and /dev/null differ
diff --git a/art/exported/characters/patterns/calico/body_main_calico.png b/art/exported/characters/patterns/calico/body_main_calico.png
deleted file mode 100644
index ef2acad..0000000
Binary files a/art/exported/characters/patterns/calico/body_main_calico.png and /dev/null differ
diff --git a/art/exported/characters/patterns/fox/body_arms_fox.png b/art/exported/characters/patterns/fox/body_arms_fox.png
deleted file mode 100644
index aab265a..0000000
Binary files a/art/exported/characters/patterns/fox/body_arms_fox.png and /dev/null differ
diff --git a/art/exported/characters/patterns/fox/body_arms_fox.png.import b/art/exported/characters/patterns/fox/body_arms_fox.png.import
deleted file mode 100644
index af10b42..0000000
--- a/art/exported/characters/patterns/fox/body_arms_fox.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://p5cg2bwrxf7v"
-path="res://.godot/imported/body_arms_fox.png-91ab21ce544fdc29b8a0ebf3e54ef7cd.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://art/exported/characters/patterns/fox/body_arms_fox.png"
-dest_files=["res://.godot/imported/body_arms_fox.png-91ab21ce544fdc29b8a0ebf3e54ef7cd.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/art/exported/characters/patterns/fox/body_main_fox.png b/art/exported/characters/patterns/fox/body_main_fox.png
deleted file mode 100644
index 5c87bc5..0000000
Binary files a/art/exported/characters/patterns/fox/body_main_fox.png and /dev/null differ
diff --git a/art/exported/characters/patterns/fox/body_main_fox.png.import b/art/exported/characters/patterns/fox/body_main_fox.png.import
deleted file mode 100644
index 467c2b8..0000000
--- a/art/exported/characters/patterns/fox/body_main_fox.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://wcsadkjjpaei"
-path="res://.godot/imported/body_main_fox.png-25e4fa03a48053f6b37b3bbadb1f01ba.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://art/exported/characters/patterns/fox/body_main_fox.png"
-dest_files=["res://.godot/imported/body_main_fox.png-25e4fa03a48053f6b37b3bbadb1f01ba.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/art/exported/characters/patterns/fox/ears_pointy_short_fox.png b/art/exported/characters/patterns/fox/ears_pointy_short_fox.png
deleted file mode 100644
index 03720dd..0000000
Binary files a/art/exported/characters/patterns/fox/ears_pointy_short_fox.png and /dev/null differ
diff --git a/art/exported/characters/patterns/fox/head_pointy_fox.png b/art/exported/characters/patterns/fox/head_pointy_fox.png
deleted file mode 100644
index 71be5c3..0000000
Binary files a/art/exported/characters/patterns/fox/head_pointy_fox.png and /dev/null differ
diff --git a/art/exported/characters/patterns/fox/head_pointy_fox.png.import b/art/exported/characters/patterns/fox/head_pointy_fox.png.import
deleted file mode 100644
index e3fa7e7..0000000
--- a/art/exported/characters/patterns/fox/head_pointy_fox.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c4vlbrp7qevvo"
-path="res://.godot/imported/head_pointy_fox.png-ef3535a6c05a49aae36fe5514844a3df.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://art/exported/characters/patterns/fox/head_pointy_fox.png"
-dest_files=["res://.godot/imported/head_pointy_fox.png-ef3535a6c05a49aae36fe5514844a3df.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/art/exported/characters/patterns/fox/head_round_fox.png b/art/exported/characters/patterns/fox/head_round_fox.png
deleted file mode 100644
index 427650f..0000000
Binary files a/art/exported/characters/patterns/fox/head_round_fox.png and /dev/null differ
diff --git a/art/exported/characters/patterns/fox/head_round_fox.png.import b/art/exported/characters/patterns/fox/head_round_fox.png.import
deleted file mode 100644
index 6d958dd..0000000
--- a/art/exported/characters/patterns/fox/head_round_fox.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bwiwt76kyyevw"
-path="res://.godot/imported/head_round_fox.png-27dc2125a4e5b85a622291762225a303.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://art/exported/characters/patterns/fox/head_round_fox.png"
-dest_files=["res://.godot/imported/head_round_fox.png-27dc2125a4e5b85a622291762225a303.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/art/exported/characters/patterns/fox/tails_fox_fox.png b/art/exported/characters/patterns/fox/tails_fox_fox.png
deleted file mode 100644
index 7283c2c..0000000
Binary files a/art/exported/characters/patterns/fox/tails_fox_fox.png and /dev/null differ
diff --git a/art/exported/characters/patterns/fox/tails_fox_fox.png.import b/art/exported/characters/patterns/fox/tails_fox_fox.png.import
deleted file mode 100644
index 46f6e60..0000000
--- a/art/exported/characters/patterns/fox/tails_fox_fox.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cyr0wcavb1cmi"
-path="res://.godot/imported/tails_fox_fox.png-061a224c36e90f10b33d37e616ff7e94.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://art/exported/characters/patterns/fox/tails_fox_fox.png"
-dest_files=["res://.godot/imported/tails_fox_fox.png-061a224c36e90f10b33d37e616ff7e94.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/art/exported/characters/patterns/paws/body_arms_paws.png b/art/exported/characters/patterns/paws/body_arms_paws.png
deleted file mode 100644
index 4b3c2d7..0000000
Binary files a/art/exported/characters/patterns/paws/body_arms_paws.png and /dev/null differ
diff --git a/art/exported/characters/patterns/paws/body_arms_paws.png.import b/art/exported/characters/patterns/paws/body_arms_paws.png.import
deleted file mode 100644
index 877788b..0000000
--- a/art/exported/characters/patterns/paws/body_arms_paws.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dmqpy81ves1g8"
-path="res://.godot/imported/body_arms_paws.png-06ae28eef9d8a116856aac8b0c511466.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://art/exported/characters/patterns/paws/body_arms_paws.png"
-dest_files=["res://.godot/imported/body_arms_paws.png-06ae28eef9d8a116856aac8b0c511466.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/art/exported/characters/patterns/paws/body_main_paws.png b/art/exported/characters/patterns/paws/body_main_paws.png
deleted file mode 100644
index bd738b7..0000000
Binary files a/art/exported/characters/patterns/paws/body_main_paws.png and /dev/null differ
diff --git a/art/exported/characters/patterns/paws/body_main_paws.png.import b/art/exported/characters/patterns/paws/body_main_paws.png.import
deleted file mode 100644
index 4db3aa8..0000000
--- a/art/exported/characters/patterns/paws/body_main_paws.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://nl50my4whybb"
-path="res://.godot/imported/body_main_paws.png-7262be81b419a66af0dc6fa26e01a7f8.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://art/exported/characters/patterns/paws/body_main_paws.png"
-dest_files=["res://.godot/imported/body_main_paws.png-7262be81b419a66af0dc6fa26e01a7f8.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/art/exported/characters/patterns/stripes/body_main_stripes.png b/art/exported/characters/patterns/stripes/body_main_stripes.png
deleted file mode 100644
index 1eac4f2..0000000
Binary files a/art/exported/characters/patterns/stripes/body_main_stripes.png and /dev/null differ
diff --git a/art/exported/characters/patterns/stripes/body_main_stripes.png.import b/art/exported/characters/patterns/stripes/body_main_stripes.png.import
deleted file mode 100644
index 28192d5..0000000
--- a/art/exported/characters/patterns/stripes/body_main_stripes.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cmmemafdvgw5y"
-path="res://.godot/imported/body_main_stripes.png-2ff942bebaea90b5e13ff6f122820689.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://art/exported/characters/patterns/stripes/body_main_stripes.png"
-dest_files=["res://.godot/imported/body_main_stripes.png-2ff942bebaea90b5e13ff6f122820689.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/art/exported/characters/patterns/tiger/body_main_tiger.png b/art/exported/characters/patterns/tiger/body_main_tiger.png
deleted file mode 100644
index e61d7ba..0000000
Binary files a/art/exported/characters/patterns/tiger/body_main_tiger.png and /dev/null differ
diff --git a/art/exported/characters/patterns/tiger/body_main_tiger.png.import b/art/exported/characters/patterns/tiger/body_main_tiger.png.import
deleted file mode 100644
index 28f3b3d..0000000
--- a/art/exported/characters/patterns/tiger/body_main_tiger.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dbakek50irjdp"
-path="res://.godot/imported/body_main_tiger.png-30e81e0d87ce0d9af9d678c10fd2e5bd.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://art/exported/characters/patterns/tiger/body_main_tiger.png"
-dest_files=["res://.godot/imported/body_main_tiger.png-30e81e0d87ce0d9af9d678c10fd2e5bd.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/art/exported/characters/patterns/tiger/head_round_tiger.png b/art/exported/characters/patterns/tiger/head_round_tiger.png
deleted file mode 100644
index e89fb56..0000000
Binary files a/art/exported/characters/patterns/tiger/head_round_tiger.png and /dev/null differ
diff --git a/art/exported/characters/patterns/tiger/head_round_tiger.png.import b/art/exported/characters/patterns/tiger/head_round_tiger.png.import
deleted file mode 100644
index 1b0c5e1..0000000
--- a/art/exported/characters/patterns/tiger/head_round_tiger.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cnpjuifjm0h4i"
-path="res://.godot/imported/head_round_tiger.png-fe9ec3d30703aa477bff0311b12fc252.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://art/exported/characters/patterns/tiger/head_round_tiger.png"
-dest_files=["res://.godot/imported/head_round_tiger.png-fe9ec3d30703aa477bff0311b12fc252.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/art/exported/characters/patterns/tummy/body_main_tummy.png b/art/exported/characters/patterns/tummy/body_main_tummy.png
deleted file mode 100644
index 241c2a8..0000000
Binary files a/art/exported/characters/patterns/tummy/body_main_tummy.png and /dev/null differ
diff --git a/art/exported/characters/patterns/tummy/body_main_tummy.png.import b/art/exported/characters/patterns/tummy/body_main_tummy.png.import
deleted file mode 100644
index 044bee3..0000000
--- a/art/exported/characters/patterns/tummy/body_main_tummy.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dv1bran6gqbd5"
-path="res://.godot/imported/body_main_tummy.png-74bd92fbb462e4dbe1c1bf841db71eb0.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://art/exported/characters/patterns/tummy/body_main_tummy.png"
-dest_files=["res://.godot/imported/body_main_tummy.png-74bd92fbb462e4dbe1c1bf841db71eb0.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/art/exported/creatures/101.png b/art/exported/creatures/101.png
deleted file mode 100644
index e6e431c..0000000
Binary files a/art/exported/creatures/101.png and /dev/null differ
diff --git a/art/exported/creatures/101.png.import b/art/exported/creatures/101.png.import
deleted file mode 100644
index 3dc8865..0000000
--- a/art/exported/creatures/101.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://x5e744k11k70"
-path.s3tc="res://.godot/imported/101.png-81f7f90e29292575c1a9727aa6060fa0.s3tc.ctex"
-path.etc2="res://.godot/imported/101.png-81f7f90e29292575c1a9727aa6060fa0.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/101.png"
-dest_files=["res://.godot/imported/101.png-81f7f90e29292575c1a9727aa6060fa0.s3tc.ctex", "res://.godot/imported/101.png-81f7f90e29292575c1a9727aa6060fa0.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/102.png b/art/exported/creatures/102.png
deleted file mode 100644
index b1b66a6..0000000
Binary files a/art/exported/creatures/102.png and /dev/null differ
diff --git a/art/exported/creatures/102.png.import b/art/exported/creatures/102.png.import
deleted file mode 100644
index c958e5f..0000000
--- a/art/exported/creatures/102.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://1kla4jewitot"
-path.s3tc="res://.godot/imported/102.png-2ab8ac9408a482b1042b6ba3cd3bd718.s3tc.ctex"
-path.etc2="res://.godot/imported/102.png-2ab8ac9408a482b1042b6ba3cd3bd718.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/102.png"
-dest_files=["res://.godot/imported/102.png-2ab8ac9408a482b1042b6ba3cd3bd718.s3tc.ctex", "res://.godot/imported/102.png-2ab8ac9408a482b1042b6ba3cd3bd718.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/103.png b/art/exported/creatures/103.png
deleted file mode 100644
index 98dd618..0000000
Binary files a/art/exported/creatures/103.png and /dev/null differ
diff --git a/art/exported/creatures/103.png.import b/art/exported/creatures/103.png.import
deleted file mode 100644
index 2972cde..0000000
--- a/art/exported/creatures/103.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cfdsg4tevv0jm"
-path.s3tc="res://.godot/imported/103.png-f8985f10e04b19499cd6f5ffbadc83c9.s3tc.ctex"
-path.etc2="res://.godot/imported/103.png-f8985f10e04b19499cd6f5ffbadc83c9.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/103.png"
-dest_files=["res://.godot/imported/103.png-f8985f10e04b19499cd6f5ffbadc83c9.s3tc.ctex", "res://.godot/imported/103.png-f8985f10e04b19499cd6f5ffbadc83c9.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/104.png b/art/exported/creatures/104.png
deleted file mode 100644
index 09eec3c..0000000
Binary files a/art/exported/creatures/104.png and /dev/null differ
diff --git a/art/exported/creatures/104.png.import b/art/exported/creatures/104.png.import
deleted file mode 100644
index 01a9393..0000000
--- a/art/exported/creatures/104.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://pgm364g5qlgs"
-path.s3tc="res://.godot/imported/104.png-e164f67032648af2cc0a545d2a861c7d.s3tc.ctex"
-path.etc2="res://.godot/imported/104.png-e164f67032648af2cc0a545d2a861c7d.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/104.png"
-dest_files=["res://.godot/imported/104.png-e164f67032648af2cc0a545d2a861c7d.s3tc.ctex", "res://.godot/imported/104.png-e164f67032648af2cc0a545d2a861c7d.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/105.png b/art/exported/creatures/105.png
deleted file mode 100644
index 3ba1000..0000000
Binary files a/art/exported/creatures/105.png and /dev/null differ
diff --git a/art/exported/creatures/105.png.import b/art/exported/creatures/105.png.import
deleted file mode 100644
index de797a7..0000000
--- a/art/exported/creatures/105.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dwanip2j561n7"
-path.s3tc="res://.godot/imported/105.png-a2b440912e59b3375c6e61e17d08287b.s3tc.ctex"
-path.etc2="res://.godot/imported/105.png-a2b440912e59b3375c6e61e17d08287b.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/105.png"
-dest_files=["res://.godot/imported/105.png-a2b440912e59b3375c6e61e17d08287b.s3tc.ctex", "res://.godot/imported/105.png-a2b440912e59b3375c6e61e17d08287b.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/106.png b/art/exported/creatures/106.png
deleted file mode 100644
index 4227b44..0000000
Binary files a/art/exported/creatures/106.png and /dev/null differ
diff --git a/art/exported/creatures/106.png.import b/art/exported/creatures/106.png.import
deleted file mode 100644
index 3032c38..0000000
--- a/art/exported/creatures/106.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cr7tc11ievso5"
-path.s3tc="res://.godot/imported/106.png-306ce449448588714e5a0e883ae441ac.s3tc.ctex"
-path.etc2="res://.godot/imported/106.png-306ce449448588714e5a0e883ae441ac.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/106.png"
-dest_files=["res://.godot/imported/106.png-306ce449448588714e5a0e883ae441ac.s3tc.ctex", "res://.godot/imported/106.png-306ce449448588714e5a0e883ae441ac.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/107.png b/art/exported/creatures/107.png
deleted file mode 100644
index c843bbe..0000000
Binary files a/art/exported/creatures/107.png and /dev/null differ
diff --git a/art/exported/creatures/107.png.import b/art/exported/creatures/107.png.import
deleted file mode 100644
index c4d28e8..0000000
--- a/art/exported/creatures/107.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c8rf4tnxx8mov"
-path.s3tc="res://.godot/imported/107.png-062740d0bdad6e3ac4ecc1cf92ee6b9d.s3tc.ctex"
-path.etc2="res://.godot/imported/107.png-062740d0bdad6e3ac4ecc1cf92ee6b9d.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/107.png"
-dest_files=["res://.godot/imported/107.png-062740d0bdad6e3ac4ecc1cf92ee6b9d.s3tc.ctex", "res://.godot/imported/107.png-062740d0bdad6e3ac4ecc1cf92ee6b9d.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/201.png b/art/exported/creatures/201.png
deleted file mode 100644
index 3994912..0000000
Binary files a/art/exported/creatures/201.png and /dev/null differ
diff --git a/art/exported/creatures/201.png.import b/art/exported/creatures/201.png.import
deleted file mode 100644
index 9d5decf..0000000
--- a/art/exported/creatures/201.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c0q8o1sc8hlv0"
-path.s3tc="res://.godot/imported/201.png-1b2063c63e42271e8f3910da8e3f1277.s3tc.ctex"
-path.etc2="res://.godot/imported/201.png-1b2063c63e42271e8f3910da8e3f1277.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/201.png"
-dest_files=["res://.godot/imported/201.png-1b2063c63e42271e8f3910da8e3f1277.s3tc.ctex", "res://.godot/imported/201.png-1b2063c63e42271e8f3910da8e3f1277.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/3906.png b/art/exported/creatures/3906.png
deleted file mode 100644
index 6cc1d1b..0000000
Binary files a/art/exported/creatures/3906.png and /dev/null differ
diff --git a/art/exported/creatures/3906.png.import b/art/exported/creatures/3906.png.import
deleted file mode 100644
index 9bce5e4..0000000
--- a/art/exported/creatures/3906.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bdmm1jfod7gq0"
-path.s3tc="res://.godot/imported/3906.png-39cc349a6d3e716822bc9e12bd0ac7a2.s3tc.ctex"
-path.etc2="res://.godot/imported/3906.png-39cc349a6d3e716822bc9e12bd0ac7a2.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/3906.png"
-dest_files=["res://.godot/imported/3906.png-39cc349a6d3e716822bc9e12bd0ac7a2.s3tc.ctex", "res://.godot/imported/3906.png-39cc349a6d3e716822bc9e12bd0ac7a2.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/403.png b/art/exported/creatures/403.png
deleted file mode 100644
index a1eb283..0000000
Binary files a/art/exported/creatures/403.png and /dev/null differ
diff --git a/art/exported/creatures/403.png.import b/art/exported/creatures/403.png.import
deleted file mode 100644
index da790e8..0000000
--- a/art/exported/creatures/403.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bypagdb0ollh0"
-path.s3tc="res://.godot/imported/403.png-57b9dd64545d97bbd63b86e7f3d61562.s3tc.ctex"
-path.etc2="res://.godot/imported/403.png-57b9dd64545d97bbd63b86e7f3d61562.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/403.png"
-dest_files=["res://.godot/imported/403.png-57b9dd64545d97bbd63b86e7f3d61562.s3tc.ctex", "res://.godot/imported/403.png-57b9dd64545d97bbd63b86e7f3d61562.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/4401.png b/art/exported/creatures/4401.png
deleted file mode 100644
index d043f4c..0000000
Binary files a/art/exported/creatures/4401.png and /dev/null differ
diff --git a/art/exported/creatures/4401.png.import b/art/exported/creatures/4401.png.import
deleted file mode 100644
index 809a146..0000000
--- a/art/exported/creatures/4401.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://7dbcbicxwvlo"
-path.s3tc="res://.godot/imported/4401.png-0fd242cb090b49614af9b4d07503b2db.s3tc.ctex"
-path.etc2="res://.godot/imported/4401.png-0fd242cb090b49614af9b4d07503b2db.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/4401.png"
-dest_files=["res://.godot/imported/4401.png-0fd242cb090b49614af9b4d07503b2db.s3tc.ctex", "res://.godot/imported/4401.png-0fd242cb090b49614af9b4d07503b2db.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/4701.png b/art/exported/creatures/4701.png
deleted file mode 100644
index afd6a85..0000000
Binary files a/art/exported/creatures/4701.png and /dev/null differ
diff --git a/art/exported/creatures/4701.png.import b/art/exported/creatures/4701.png.import
deleted file mode 100644
index 5f4ed32..0000000
--- a/art/exported/creatures/4701.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://do6hcv3omivv5"
-path.s3tc="res://.godot/imported/4701.png-dcb44d4ce6dfdc812a2e185e7f9511be.s3tc.ctex"
-path.etc2="res://.godot/imported/4701.png-dcb44d4ce6dfdc812a2e185e7f9511be.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/4701.png"
-dest_files=["res://.godot/imported/4701.png-dcb44d4ce6dfdc812a2e185e7f9511be.s3tc.ctex", "res://.godot/imported/4701.png-dcb44d4ce6dfdc812a2e185e7f9511be.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/5702.png b/art/exported/creatures/5702.png
deleted file mode 100644
index aba6b80..0000000
Binary files a/art/exported/creatures/5702.png and /dev/null differ
diff --git a/art/exported/creatures/5702.png.import b/art/exported/creatures/5702.png.import
deleted file mode 100644
index 527c367..0000000
--- a/art/exported/creatures/5702.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dxwr7yv5h30s"
-path.s3tc="res://.godot/imported/5702.png-b0cead42bd9a5851c3f6775a7d0e469e.s3tc.ctex"
-path.etc2="res://.godot/imported/5702.png-b0cead42bd9a5851c3f6775a7d0e469e.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/5702.png"
-dest_files=["res://.godot/imported/5702.png-b0cead42bd9a5851c3f6775a7d0e469e.s3tc.ctex", "res://.godot/imported/5702.png-b0cead42bd9a5851c3f6775a7d0e469e.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/5901.png b/art/exported/creatures/5901.png
deleted file mode 100644
index a75039f..0000000
Binary files a/art/exported/creatures/5901.png and /dev/null differ
diff --git a/art/exported/creatures/5901.png.import b/art/exported/creatures/5901.png.import
deleted file mode 100644
index e52ad8c..0000000
--- a/art/exported/creatures/5901.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dovitajskavmi"
-path.s3tc="res://.godot/imported/5901.png-10c9e77992ad31ce54d34b95a6db6f61.s3tc.ctex"
-path.etc2="res://.godot/imported/5901.png-10c9e77992ad31ce54d34b95a6db6f61.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/5901.png"
-dest_files=["res://.godot/imported/5901.png-10c9e77992ad31ce54d34b95a6db6f61.s3tc.ctex", "res://.godot/imported/5901.png-10c9e77992ad31ce54d34b95a6db6f61.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/6103.png b/art/exported/creatures/6103.png
deleted file mode 100644
index f45e607..0000000
Binary files a/art/exported/creatures/6103.png and /dev/null differ
diff --git a/art/exported/creatures/6103.png.import b/art/exported/creatures/6103.png.import
deleted file mode 100644
index ac97dac..0000000
--- a/art/exported/creatures/6103.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://k3ser2dy6wj7"
-path.s3tc="res://.godot/imported/6103.png-5fc95bf908f801d5397114bde5081f55.s3tc.ctex"
-path.etc2="res://.godot/imported/6103.png-5fc95bf908f801d5397114bde5081f55.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/6103.png"
-dest_files=["res://.godot/imported/6103.png-5fc95bf908f801d5397114bde5081f55.s3tc.ctex", "res://.godot/imported/6103.png-5fc95bf908f801d5397114bde5081f55.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/6804.png b/art/exported/creatures/6804.png
deleted file mode 100644
index 14cc738..0000000
Binary files a/art/exported/creatures/6804.png and /dev/null differ
diff --git a/art/exported/creatures/6804.png.import b/art/exported/creatures/6804.png.import
deleted file mode 100644
index 12be7af..0000000
--- a/art/exported/creatures/6804.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b5ejex5wtfoun"
-path.s3tc="res://.godot/imported/6804.png-28bcc10060d656f6d7f080ea81e2d1d3.s3tc.ctex"
-path.etc2="res://.godot/imported/6804.png-28bcc10060d656f6d7f080ea81e2d1d3.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/6804.png"
-dest_files=["res://.godot/imported/6804.png-28bcc10060d656f6d7f080ea81e2d1d3.s3tc.ctex", "res://.godot/imported/6804.png-28bcc10060d656f6d7f080ea81e2d1d3.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/6905.png b/art/exported/creatures/6905.png
deleted file mode 100644
index 296848e..0000000
Binary files a/art/exported/creatures/6905.png and /dev/null differ
diff --git a/art/exported/creatures/6905.png.import b/art/exported/creatures/6905.png.import
deleted file mode 100644
index f267dc2..0000000
--- a/art/exported/creatures/6905.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c4d8x6qyhahvn"
-path.s3tc="res://.godot/imported/6905.png-a52c1647421059618698e17d7a3f8f98.s3tc.ctex"
-path.etc2="res://.godot/imported/6905.png-a52c1647421059618698e17d7a3f8f98.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/6905.png"
-dest_files=["res://.godot/imported/6905.png-a52c1647421059618698e17d7a3f8f98.s3tc.ctex", "res://.godot/imported/6905.png-a52c1647421059618698e17d7a3f8f98.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/701.png b/art/exported/creatures/701.png
deleted file mode 100644
index 4f7d8f8..0000000
Binary files a/art/exported/creatures/701.png and /dev/null differ
diff --git a/art/exported/creatures/701.png.import b/art/exported/creatures/701.png.import
deleted file mode 100644
index 7598ee8..0000000
--- a/art/exported/creatures/701.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://jw5xt0cfjn1d"
-path.s3tc="res://.godot/imported/701.png-8a66c28a9f49fa465ce3b5342fa08dbc.s3tc.ctex"
-path.etc2="res://.godot/imported/701.png-8a66c28a9f49fa465ce3b5342fa08dbc.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/701.png"
-dest_files=["res://.godot/imported/701.png-8a66c28a9f49fa465ce3b5342fa08dbc.s3tc.ctex", "res://.godot/imported/701.png-8a66c28a9f49fa465ce3b5342fa08dbc.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/704.png b/art/exported/creatures/704.png
deleted file mode 100644
index 526d090..0000000
Binary files a/art/exported/creatures/704.png and /dev/null differ
diff --git a/art/exported/creatures/704.png.import b/art/exported/creatures/704.png.import
deleted file mode 100644
index e80e19e..0000000
--- a/art/exported/creatures/704.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bonpxkp6uwxq6"
-path.s3tc="res://.godot/imported/704.png-649669246217a14c9609a7e929841827.s3tc.ctex"
-path.etc2="res://.godot/imported/704.png-649669246217a14c9609a7e929841827.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/704.png"
-dest_files=["res://.godot/imported/704.png-649669246217a14c9609a7e929841827.s3tc.ctex", "res://.godot/imported/704.png-649669246217a14c9609a7e929841827.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/7101.png b/art/exported/creatures/7101.png
deleted file mode 100644
index 93a13af..0000000
Binary files a/art/exported/creatures/7101.png and /dev/null differ
diff --git a/art/exported/creatures/7101.png.import b/art/exported/creatures/7101.png.import
deleted file mode 100644
index 2d312b3..0000000
--- a/art/exported/creatures/7101.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dd5lpjei8ix4d"
-path.s3tc="res://.godot/imported/7101.png-9be787a05b48075222e923e556de87ac.s3tc.ctex"
-path.etc2="res://.godot/imported/7101.png-9be787a05b48075222e923e556de87ac.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/7101.png"
-dest_files=["res://.godot/imported/7101.png-9be787a05b48075222e923e556de87ac.s3tc.ctex", "res://.godot/imported/7101.png-9be787a05b48075222e923e556de87ac.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/7403.png b/art/exported/creatures/7403.png
deleted file mode 100644
index ad73c17..0000000
Binary files a/art/exported/creatures/7403.png and /dev/null differ
diff --git a/art/exported/creatures/7403.png.import b/art/exported/creatures/7403.png.import
deleted file mode 100644
index bef32c4..0000000
--- a/art/exported/creatures/7403.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bl7ru6gm5xnpb"
-path.s3tc="res://.godot/imported/7403.png-54b51089a6ff5c2091372d659ba4bdc7.s3tc.ctex"
-path.etc2="res://.godot/imported/7403.png-54b51089a6ff5c2091372d659ba4bdc7.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/7403.png"
-dest_files=["res://.godot/imported/7403.png-54b51089a6ff5c2091372d659ba4bdc7.s3tc.ctex", "res://.godot/imported/7403.png-54b51089a6ff5c2091372d659ba4bdc7.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/7506.png b/art/exported/creatures/7506.png
deleted file mode 100644
index 7e8c5b2..0000000
Binary files a/art/exported/creatures/7506.png and /dev/null differ
diff --git a/art/exported/creatures/7506.png.import b/art/exported/creatures/7506.png.import
deleted file mode 100644
index c2c99a0..0000000
--- a/art/exported/creatures/7506.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c6qm3sujp8xvg"
-path.s3tc="res://.godot/imported/7506.png-5c0f871c623e61a890015626ea315de9.s3tc.ctex"
-path.etc2="res://.godot/imported/7506.png-5c0f871c623e61a890015626ea315de9.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/7506.png"
-dest_files=["res://.godot/imported/7506.png-5c0f871c623e61a890015626ea315de9.s3tc.ctex", "res://.godot/imported/7506.png-5c0f871c623e61a890015626ea315de9.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/7508.png b/art/exported/creatures/7508.png
deleted file mode 100644
index 27f1a59..0000000
Binary files a/art/exported/creatures/7508.png and /dev/null differ
diff --git a/art/exported/creatures/7508.png.import b/art/exported/creatures/7508.png.import
deleted file mode 100644
index a14d42f..0000000
--- a/art/exported/creatures/7508.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bnw0dw14iyxbh"
-path.s3tc="res://.godot/imported/7508.png-555eff33a2cb66622ccca9d7f063522e.s3tc.ctex"
-path.etc2="res://.godot/imported/7508.png-555eff33a2cb66622ccca9d7f063522e.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/7508.png"
-dest_files=["res://.godot/imported/7508.png-555eff33a2cb66622ccca9d7f063522e.s3tc.ctex", "res://.godot/imported/7508.png-555eff33a2cb66622ccca9d7f063522e.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/801.png b/art/exported/creatures/801.png
deleted file mode 100644
index f2e6390..0000000
Binary files a/art/exported/creatures/801.png and /dev/null differ
diff --git a/art/exported/creatures/801.png.import b/art/exported/creatures/801.png.import
deleted file mode 100644
index 3721440..0000000
--- a/art/exported/creatures/801.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dkocdkoyy2udh"
-path.s3tc="res://.godot/imported/801.png-b0324ef8c10934464b8be69cb9eca981.s3tc.ctex"
-path.etc2="res://.godot/imported/801.png-b0324ef8c10934464b8be69cb9eca981.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/801.png"
-dest_files=["res://.godot/imported/801.png-b0324ef8c10934464b8be69cb9eca981.s3tc.ctex", "res://.godot/imported/801.png-b0324ef8c10934464b8be69cb9eca981.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/802.png b/art/exported/creatures/802.png
deleted file mode 100644
index f786031..0000000
Binary files a/art/exported/creatures/802.png and /dev/null differ
diff --git a/art/exported/creatures/802.png.import b/art/exported/creatures/802.png.import
deleted file mode 100644
index f7e4cc6..0000000
--- a/art/exported/creatures/802.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b6pkx1koopl7p"
-path.s3tc="res://.godot/imported/802.png-1e79f95b56c444a9c7183dada8a14dc3.s3tc.ctex"
-path.etc2="res://.godot/imported/802.png-1e79f95b56c444a9c7183dada8a14dc3.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/802.png"
-dest_files=["res://.godot/imported/802.png-1e79f95b56c444a9c7183dada8a14dc3.s3tc.ctex", "res://.godot/imported/802.png-1e79f95b56c444a9c7183dada8a14dc3.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/805.png b/art/exported/creatures/805.png
deleted file mode 100644
index 727793c..0000000
Binary files a/art/exported/creatures/805.png and /dev/null differ
diff --git a/art/exported/creatures/805.png.import b/art/exported/creatures/805.png.import
deleted file mode 100644
index db359af..0000000
--- a/art/exported/creatures/805.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cyo0ihvopej4"
-path.s3tc="res://.godot/imported/805.png-f508211dd3f15d8b9090675eef9939f7.s3tc.ctex"
-path.etc2="res://.godot/imported/805.png-f508211dd3f15d8b9090675eef9939f7.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/805.png"
-dest_files=["res://.godot/imported/805.png-f508211dd3f15d8b9090675eef9939f7.s3tc.ctex", "res://.godot/imported/805.png-f508211dd3f15d8b9090675eef9939f7.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/806.png b/art/exported/creatures/806.png
deleted file mode 100644
index a8acdb1..0000000
Binary files a/art/exported/creatures/806.png and /dev/null differ
diff --git a/art/exported/creatures/806.png.import b/art/exported/creatures/806.png.import
deleted file mode 100644
index 8078cb6..0000000
--- a/art/exported/creatures/806.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c31dw2wqjat40"
-path.s3tc="res://.godot/imported/806.png-640099ab90a2d2daf3c4e7229634452c.s3tc.ctex"
-path.etc2="res://.godot/imported/806.png-640099ab90a2d2daf3c4e7229634452c.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/806.png"
-dest_files=["res://.godot/imported/806.png-640099ab90a2d2daf3c4e7229634452c.s3tc.ctex", "res://.godot/imported/806.png-640099ab90a2d2daf3c4e7229634452c.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/808.png b/art/exported/creatures/808.png
deleted file mode 100644
index e218b8e..0000000
Binary files a/art/exported/creatures/808.png and /dev/null differ
diff --git a/art/exported/creatures/808.png.import b/art/exported/creatures/808.png.import
deleted file mode 100644
index 1126876..0000000
--- a/art/exported/creatures/808.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://x2d4nglo0rlk"
-path.s3tc="res://.godot/imported/808.png-bc683347c2e3b627379b7eb047e12a89.s3tc.ctex"
-path.etc2="res://.godot/imported/808.png-bc683347c2e3b627379b7eb047e12a89.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/808.png"
-dest_files=["res://.godot/imported/808.png-bc683347c2e3b627379b7eb047e12a89.s3tc.ctex", "res://.godot/imported/808.png-bc683347c2e3b627379b7eb047e12a89.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/creatures/fish_placeholder.png b/art/exported/creatures/fish_placeholder.png
deleted file mode 100644
index de69afb..0000000
Binary files a/art/exported/creatures/fish_placeholder.png and /dev/null differ
diff --git a/art/exported/creatures/fish_placeholder.png.import b/art/exported/creatures/fish_placeholder.png.import
deleted file mode 100644
index 87111f4..0000000
--- a/art/exported/creatures/fish_placeholder.png.import
+++ /dev/null
@@ -1,42 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://d08ce7q8vmlfs"
-path.s3tc="res://.godot/imported/fish_placeholder.png-41ecfc07b87417338ce888ac1e2c2362.s3tc.ctex"
-path.etc2="res://.godot/imported/fish_placeholder.png-41ecfc07b87417338ce888ac1e2c2362.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://art/exported/creatures/fish_placeholder.png"
-dest_files=["res://.godot/imported/fish_placeholder.png-41ecfc07b87417338ce888ac1e2c2362.s3tc.ctex", "res://.godot/imported/fish_placeholder.png-41ecfc07b87417338ce888ac1e2c2362.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/environment/terrain/starter_island.glb b/art/exported/environment/terrain/starter_island.glb
index e812b19..9612f8d 100644
Binary files a/art/exported/environment/terrain/starter_island.glb and b/art/exported/environment/terrain/starter_island.glb differ
diff --git a/art/exported/environment/terrain/starter_island.glb.import b/art/exported/environment/terrain/starter_island.glb.import
index 18e4e12..bedce7c 100644
--- a/art/exported/environment/terrain/starter_island.glb.import
+++ b/art/exported/environment/terrain/starter_island.glb.import
@@ -41,5 +41,5 @@ materials/extract_format=0
materials/extract_path=""
_subresources={}
gltf/naming_version=2
-gltf/embedded_image_handling=3
+gltf/embedded_image_handling=1
gltf/texture_map_mode=1
diff --git a/art/exported/environment/textures/grass_olive_tile.png b/art/exported/environment/textures/grass_olive_tile.png
new file mode 100644
index 0000000..affb5dc
Binary files /dev/null and b/art/exported/environment/textures/grass_olive_tile.png differ
diff --git a/art/exported/environment/textures/grass_olive_tile.png.import b/art/exported/environment/textures/grass_olive_tile.png.import
new file mode 100644
index 0000000..bd6ee69
--- /dev/null
+++ b/art/exported/environment/textures/grass_olive_tile.png.import
@@ -0,0 +1,41 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://btxhq3rdlum2"
+path.s3tc="res://.godot/imported/grass_olive_tile.png-72072a382d0948818df97773afa5493f.s3tc.ctex"
+metadata={
+"imported_formats": ["s3tc_bptc"],
+"vram_texture": true
+}
+
+[deps]
+
+source_file="res://art/exported/environment/textures/grass_olive_tile.png"
+dest_files=["res://.godot/imported/grass_olive_tile.png-72072a382d0948818df97773afa5493f.s3tc.ctex"]
+
+[params]
+
+compress/mode=2
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=true
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=0
diff --git a/art/exported/items/equipment/net.glb b/art/exported/items/equipment/net.glb
deleted file mode 100644
index db4fa34..0000000
Binary files a/art/exported/items/equipment/net.glb and /dev/null differ
diff --git a/art/exported/items/equipment/net.glb.import b/art/exported/items/equipment/net.glb.import
deleted file mode 100644
index 28667d6..0000000
--- a/art/exported/items/equipment/net.glb.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://dsa6yss1sxfcy"
-path="res://.godot/imported/net.glb-cbbc53905aec6a50c4a3322ca7a93b1e.scn"
-
-[deps]
-
-source_file="res://art/exported/items/equipment/net.glb"
-dest_files=["res://.godot/imported/net.glb-cbbc53905aec6a50c4a3322ca7a93b1e.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-gltf/naming_version=2
-gltf/embedded_image_handling=1
-gltf/texture_map_mode=1
diff --git a/art/exported/items/equipment/net_Material.png b/art/exported/items/equipment/net_Material.png
deleted file mode 100644
index bd5eff2..0000000
Binary files a/art/exported/items/equipment/net_Material.png and /dev/null differ
diff --git a/art/exported/items/equipment/net_Material.png.import b/art/exported/items/equipment/net_Material.png.import
deleted file mode 100644
index 0723973..0000000
--- a/art/exported/items/equipment/net_Material.png.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://binoqhg45vgbs"
-path.s3tc="res://.godot/imported/net_Material.png-f4c4b0400561bc2751fe719b6b1a1e1a.s3tc.ctex"
-path.etc2="res://.godot/imported/net_Material.png-f4c4b0400561bc2751fe719b6b1a1e1a.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-generator_parameters={
-"md5": "ef2c2ae657c9460c90eb808ba5217f39"
-}
-
-[deps]
-
-source_file="res://art/exported/items/equipment/net_Material.png"
-dest_files=["res://.godot/imported/net_Material.png-f4c4b0400561bc2751fe719b6b1a1e1a.s3tc.ctex", "res://.godot/imported/net_Material.png-f4c4b0400561bc2751fe719b6b1a1e1a.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/items/equipment/net_Plane Base Color.png b/art/exported/items/equipment/net_Plane Base Color.png
deleted file mode 100644
index 795d0a3..0000000
Binary files a/art/exported/items/equipment/net_Plane Base Color.png and /dev/null differ
diff --git a/art/exported/items/equipment/net_Plane Base Color.png.import b/art/exported/items/equipment/net_Plane Base Color.png.import
deleted file mode 100644
index 61077be..0000000
--- a/art/exported/items/equipment/net_Plane Base Color.png.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cdsabucjy5rpk"
-path.s3tc="res://.godot/imported/net_Plane Base Color.png-3875ae3dd186ddf319c55aabb13e663f.s3tc.ctex"
-path.etc2="res://.godot/imported/net_Plane Base Color.png-3875ae3dd186ddf319c55aabb13e663f.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-generator_parameters={
-"md5": "a6001c381d4c8e16bd2d64ea74ccff82"
-}
-
-[deps]
-
-source_file="res://art/exported/items/equipment/net_Plane Base Color.png"
-dest_files=["res://.godot/imported/net_Plane Base Color.png-3875ae3dd186ddf319c55aabb13e663f.s3tc.ctex", "res://.godot/imported/net_Plane Base Color.png-3875ae3dd186ddf319c55aabb13e663f.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/exported/system_icons/straywild.ico b/art/exported/system_icons/straywild.ico
deleted file mode 100644
index c03de8e..0000000
Binary files a/art/exported/system_icons/straywild.ico and /dev/null differ
diff --git a/art/exported/system_icons/straywild_1024.png b/art/exported/system_icons/straywild_1024.png
deleted file mode 100644
index 2a3b204..0000000
Binary files a/art/exported/system_icons/straywild_1024.png and /dev/null differ
diff --git a/art/exported/system_icons/straywild_256.png b/art/exported/system_icons/straywild_256.png
deleted file mode 100644
index 98275eb..0000000
Binary files a/art/exported/system_icons/straywild_256.png and /dev/null differ
diff --git a/art/exported/system_icons/straywild_256.png.import b/art/exported/system_icons/straywild_256.png.import
deleted file mode 100644
index 5726347..0000000
--- a/art/exported/system_icons/straywild_256.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cty16m8ydla06"
-path="res://.godot/imported/straywild_256.png-87b7e62e710eae4d3e3ed24a8454ca6d.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://art/exported/system_icons/straywild_256.png"
-dest_files=["res://.godot/imported/straywild_256.png-87b7e62e710eae4d3e3ed24a8454ca6d.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/art/patterns/pattern_moneyfish.png b/art/patterns/pattern_moneyfish.png
deleted file mode 100644
index 6266f29..0000000
Binary files a/art/patterns/pattern_moneyfish.png and /dev/null differ
diff --git a/art/patterns/pattern_moneyfish.png.import b/art/patterns/pattern_moneyfish.png.import
deleted file mode 100644
index 790317c..0000000
--- a/art/patterns/pattern_moneyfish.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b6xws1d2dnbnn"
-path="res://.godot/imported/pattern_moneyfish.png-c20edef3e829648b3e6aae4e190efaed.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://art/patterns/pattern_moneyfish.png"
-dest_files=["res://.godot/imported/pattern_moneyfish.png-c20edef3e829648b3e6aae4e190efaed.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/art/patterns/pattern_tealdot.png b/art/patterns/pattern_tealdot.png
deleted file mode 100644
index 9b901b6..0000000
Binary files a/art/patterns/pattern_tealdot.png and /dev/null differ
diff --git a/art/patterns/pattern_tealdot.png.import b/art/patterns/pattern_tealdot.png.import
deleted file mode 100644
index a6664e4..0000000
--- a/art/patterns/pattern_tealdot.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://f8a0cs0s7ler"
-path="res://.godot/imported/pattern_tealdot.png-ea096873729758aad8d393553ab96b2d.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://art/patterns/pattern_tealdot.png"
-dest_files=["res://.godot/imported/pattern_tealdot.png-ea096873729758aad8d393553ab96b2d.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/art/source/characters/base/straywild_base_character.blend b/art/source/characters/base/straywild_base_character.blend
deleted file mode 100644
index ed0a7a9..0000000
Binary files a/art/source/characters/base/straywild_base_character.blend and /dev/null differ
diff --git a/art/source/characters/base/straywild_base_character.blend.import b/art/source/characters/base/straywild_base_character.blend.import
deleted file mode 100644
index 2cab53c..0000000
--- a/art/source/characters/base/straywild_base_character.blend.import
+++ /dev/null
@@ -1,63 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://cb4bcakhoprm8"
-path="res://.godot/imported/straywild_base_character.blend-02abbfdb9b1d393f1bd3cfface615e27.scn"
-
-[deps]
-
-source_file="res://art/source/characters/base/straywild_base_character.blend"
-dest_files=["res://.godot/imported/straywild_base_character.blend-02abbfdb9b1d393f1bd3cfface615e27.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-blender/nodes/visible=0
-blender/nodes/active_collection_only=false
-blender/nodes/punctual_lights=true
-blender/nodes/cameras=true
-blender/nodes/custom_properties=true
-blender/nodes/modifiers=1
-blender/meshes/vertex_colors=1
-blender/meshes/uvs=true
-blender/meshes/normals=true
-blender/meshes/export_geometry_nodes_instances=false
-blender/meshes/gpu_instances=false
-blender/meshes/tangents=true
-blender/meshes/skins=2
-blender/meshes/export_bones_deforming_mesh_only=false
-blender/materials/unpack_enabled=true
-blender/materials/export_materials=1
-blender/animation/limit_playback=true
-blender/animation/always_sample=true
-blender/animation/group_tracks=true
-gltf/naming_version=2
-gltf/texture_map_mode=1
diff --git a/art/source/characters/base/straywild_base_character.glb.import b/art/source/characters/base/straywild_base_character.glb.import
deleted file mode 100644
index bda93c0..0000000
--- a/art/source/characters/base/straywild_base_character.glb.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://bfhnup14638r7"
-path="res://.godot/imported/straywild_base_character.glb-c9650c715a1b5b9ae0d8541191f7d7a9.scn"
-
-[deps]
-
-source_file="res://art/source/characters/base/straywild_base_character.glb"
-dest_files=["res://.godot/imported/straywild_base_character.glb-c9650c715a1b5b9ae0d8541191f7d7a9.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-gltf/naming_version=2
-gltf/embedded_image_handling=1
-gltf/texture_map_mode=1
diff --git a/art/source/characters/base/straywild_character_source.blend b/art/source/characters/base/straywild_character_source.blend
deleted file mode 100644
index 17a1b4d..0000000
Binary files a/art/source/characters/base/straywild_character_source.blend and /dev/null differ
diff --git a/art/source/characters/base/straywild_character_source.blend.import b/art/source/characters/base/straywild_character_source.blend.import
deleted file mode 100644
index 741c389..0000000
--- a/art/source/characters/base/straywild_character_source.blend.import
+++ /dev/null
@@ -1,63 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://d38y8blk2w5c5"
-path="res://.godot/imported/straywild_character_source.blend-0e7b8a87008f88e309ad73a63561ef5b.scn"
-
-[deps]
-
-source_file="res://art/source/characters/base/straywild_character_source.blend"
-dest_files=["res://.godot/imported/straywild_character_source.blend-0e7b8a87008f88e309ad73a63561ef5b.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-blender/nodes/visible=0
-blender/nodes/active_collection_only=false
-blender/nodes/punctual_lights=true
-blender/nodes/cameras=true
-blender/nodes/custom_properties=true
-blender/nodes/modifiers=1
-blender/meshes/vertex_colors=1
-blender/meshes/uvs=true
-blender/meshes/normals=true
-blender/meshes/export_geometry_nodes_instances=false
-blender/meshes/gpu_instances=false
-blender/meshes/tangents=true
-blender/meshes/skins=2
-blender/meshes/export_bones_deforming_mesh_only=false
-blender/materials/unpack_enabled=true
-blender/materials/export_materials=1
-blender/animation/limit_playback=true
-blender/animation/always_sample=true
-blender/animation/group_tracks=true
-gltf/naming_version=2
-gltf/texture_map_mode=1
diff --git a/art/source/characters/base/straywild_character_source.glb.import b/art/source/characters/base/straywild_character_source.glb.import
deleted file mode 100644
index bbcee6a..0000000
--- a/art/source/characters/base/straywild_character_source.glb.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://3uq4cxiomttt"
-path="res://.godot/imported/straywild_character_source.glb-3a8ccc55dbe60da1b70b62110b206353.scn"
-
-[deps]
-
-source_file="res://art/source/characters/base/straywild_character_source.glb"
-dest_files=["res://.godot/imported/straywild_character_source.glb-3a8ccc55dbe60da1b70b62110b206353.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-gltf/naming_version=2
-gltf/embedded_image_handling=1
-gltf/texture_map_mode=1
diff --git a/art/source/characters/base/straywild_character_source_eyes1_eyebrows1.png b/art/source/characters/base/straywild_character_source_eyes1_eyebrows1.png
deleted file mode 100644
index d381bf6..0000000
Binary files a/art/source/characters/base/straywild_character_source_eyes1_eyebrows1.png and /dev/null differ
diff --git a/art/source/characters/base/straywild_character_source_eyes1_eyebrows1.png.import b/art/source/characters/base/straywild_character_source_eyes1_eyebrows1.png.import
deleted file mode 100644
index dba5a5f..0000000
--- a/art/source/characters/base/straywild_character_source_eyes1_eyebrows1.png.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bdomsntfeovnc"
-path.s3tc="res://.godot/imported/straywild_character_source_eyes1_eyebrows1.png-3c638377e631a94b998336d0ca1d8b8e.s3tc.ctex"
-path.etc2="res://.godot/imported/straywild_character_source_eyes1_eyebrows1.png-3c638377e631a94b998336d0ca1d8b8e.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-generator_parameters={
-"md5": "58f88343d07131ad09a2872ff719be5e"
-}
-
-[deps]
-
-source_file="res://art/source/characters/base/straywild_character_source_eyes1_eyebrows1.png"
-dest_files=["res://.godot/imported/straywild_character_source_eyes1_eyebrows1.png-3c638377e631a94b998336d0ca1d8b8e.s3tc.ctex", "res://.godot/imported/straywild_character_source_eyes1_eyebrows1.png-3c638377e631a94b998336d0ca1d8b8e.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/source/characters/base/straywild_character_source_eyes_simple_shine.png b/art/source/characters/base/straywild_character_source_eyes_simple_shine.png
deleted file mode 100644
index 36e6859..0000000
Binary files a/art/source/characters/base/straywild_character_source_eyes_simple_shine.png and /dev/null differ
diff --git a/art/source/characters/base/straywild_character_source_eyes_simple_shine.png.import b/art/source/characters/base/straywild_character_source_eyes_simple_shine.png.import
deleted file mode 100644
index 8b99f72..0000000
--- a/art/source/characters/base/straywild_character_source_eyes_simple_shine.png.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://du4u6meapay05"
-path.s3tc="res://.godot/imported/straywild_character_source_eyes_simple_shine.png-97500ad873e0da3aaf0e9e3c3198e38a.s3tc.ctex"
-path.etc2="res://.godot/imported/straywild_character_source_eyes_simple_shine.png-97500ad873e0da3aaf0e9e3c3198e38a.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-generator_parameters={
-"md5": "c2f7c29aaaa95809e140eb308d9bb132"
-}
-
-[deps]
-
-source_file="res://art/source/characters/base/straywild_character_source_eyes_simple_shine.png"
-dest_files=["res://.godot/imported/straywild_character_source_eyes_simple_shine.png-97500ad873e0da3aaf0e9e3c3198e38a.s3tc.ctex", "res://.godot/imported/straywild_character_source_eyes_simple_shine.png-97500ad873e0da3aaf0e9e3c3198e38a.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/source/characters/base/straywild_character_source_mouth1.png b/art/source/characters/base/straywild_character_source_mouth1.png
deleted file mode 100644
index a230423..0000000
Binary files a/art/source/characters/base/straywild_character_source_mouth1.png and /dev/null differ
diff --git a/art/source/characters/base/straywild_character_source_mouth1.png.import b/art/source/characters/base/straywild_character_source_mouth1.png.import
deleted file mode 100644
index 5dd84f4..0000000
--- a/art/source/characters/base/straywild_character_source_mouth1.png.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://css0k0virgoph"
-path.s3tc="res://.godot/imported/straywild_character_source_mouth1.png-ff2ed280ca7e84bdd0749ed5b5b16f54.s3tc.ctex"
-path.etc2="res://.godot/imported/straywild_character_source_mouth1.png-ff2ed280ca7e84bdd0749ed5b5b16f54.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-generator_parameters={
-"md5": "5d01fd41e8705956256c951d0cb53924"
-}
-
-[deps]
-
-source_file="res://art/source/characters/base/straywild_character_source_mouth1.png"
-dest_files=["res://.godot/imported/straywild_character_source_mouth1.png-ff2ed280ca7e84bdd0749ed5b5b16f54.s3tc.ctex", "res://.godot/imported/straywild_character_source_mouth1.png-ff2ed280ca7e84bdd0749ed5b5b16f54.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/source/characters/base/straywild_character_source_mouth_three.png b/art/source/characters/base/straywild_character_source_mouth_three.png
deleted file mode 100644
index 527f300..0000000
Binary files a/art/source/characters/base/straywild_character_source_mouth_three.png and /dev/null differ
diff --git a/art/source/characters/base/straywild_character_source_mouth_three.png.import b/art/source/characters/base/straywild_character_source_mouth_three.png.import
deleted file mode 100644
index 4b2413d..0000000
--- a/art/source/characters/base/straywild_character_source_mouth_three.png.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://di20c1x8vksi1"
-path.s3tc="res://.godot/imported/straywild_character_source_mouth_three.png-adf40ee30e725bafadf6a8fa4dbd8abf.s3tc.ctex"
-path.etc2="res://.godot/imported/straywild_character_source_mouth_three.png-adf40ee30e725bafadf6a8fa4dbd8abf.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-generator_parameters={
-"md5": "f8bf25a1006c5cc2ab5323f4a758eec7"
-}
-
-[deps]
-
-source_file="res://art/source/characters/base/straywild_character_source_mouth_three.png"
-dest_files=["res://.godot/imported/straywild_character_source_mouth_three.png-adf40ee30e725bafadf6a8fa4dbd8abf.s3tc.ctex", "res://.godot/imported/straywild_character_source_mouth_three.png-adf40ee30e725bafadf6a8fa4dbd8abf.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/source/characters/base/straywild_character_source_nose1.2.png b/art/source/characters/base/straywild_character_source_nose1.2.png
deleted file mode 100644
index 57896ed..0000000
Binary files a/art/source/characters/base/straywild_character_source_nose1.2.png and /dev/null differ
diff --git a/art/source/characters/base/straywild_character_source_nose1.2.png.import b/art/source/characters/base/straywild_character_source_nose1.2.png.import
deleted file mode 100644
index e0c3667..0000000
--- a/art/source/characters/base/straywild_character_source_nose1.2.png.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://7dwa0bfev1rk"
-path.s3tc="res://.godot/imported/straywild_character_source_nose1.2.png-0a8355472731651768eaa5fc2ee168d4.s3tc.ctex"
-path.etc2="res://.godot/imported/straywild_character_source_nose1.2.png-0a8355472731651768eaa5fc2ee168d4.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-generator_parameters={
-"md5": "ad576beb7a7035dde96eb8e8f3ffdee7"
-}
-
-[deps]
-
-source_file="res://art/source/characters/base/straywild_character_source_nose1.2.png"
-dest_files=["res://.godot/imported/straywild_character_source_nose1.2.png-0a8355472731651768eaa5fc2ee168d4.s3tc.ctex", "res://.godot/imported/straywild_character_source_nose1.2.png-0a8355472731651768eaa5fc2ee168d4.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/source/characters/base/straywild_character_source_nose_dog_round.png b/art/source/characters/base/straywild_character_source_nose_dog_round.png
deleted file mode 100644
index 18a4590..0000000
Binary files a/art/source/characters/base/straywild_character_source_nose_dog_round.png and /dev/null differ
diff --git a/art/source/characters/base/straywild_character_source_nose_dog_round.png.import b/art/source/characters/base/straywild_character_source_nose_dog_round.png.import
deleted file mode 100644
index 5fe079e..0000000
--- a/art/source/characters/base/straywild_character_source_nose_dog_round.png.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b0p55hwdry2cr"
-path.s3tc="res://.godot/imported/straywild_character_source_nose_dog_round.png-d1ea0db61e41dfeeed3b599286ffb82c.s3tc.ctex"
-path.etc2="res://.godot/imported/straywild_character_source_nose_dog_round.png-d1ea0db61e41dfeeed3b599286ffb82c.etc2.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc", "etc2_astc"],
-"vram_texture": true
-}
-generator_parameters={
-"md5": "3134f170152f9fa1512b41b61ecb01fc"
-}
-
-[deps]
-
-source_file="res://art/source/characters/base/straywild_character_source_nose_dog_round.png"
-dest_files=["res://.godot/imported/straywild_character_source_nose_dog_round.png-d1ea0db61e41dfeeed3b599286ffb82c.s3tc.ctex", "res://.godot/imported/straywild_character_source_nose_dog_round.png-d1ea0db61e41dfeeed3b599286ffb82c.etc2.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/art/source/environment/terrain/starter_island.blend b/art/source/environment/terrain/starter_island.blend
index 8b82c4e..aeee334 100644
Binary files a/art/source/environment/terrain/starter_island.blend and b/art/source/environment/terrain/starter_island.blend differ
diff --git a/art/source/environment/textures/cliff_wall.png b/art/source/environment/textures/cliff_wall.png
deleted file mode 100644
index 3fe157f..0000000
Binary files a/art/source/environment/textures/cliff_wall.png and /dev/null differ
diff --git a/art/source/environment/textures/dirt.png b/art/source/environment/textures/dirt.png
deleted file mode 100644
index 588c8c6..0000000
Binary files a/art/source/environment/textures/dirt.png and /dev/null differ
diff --git a/art/source/environment/textures/dirt_light_wall.png b/art/source/environment/textures/dirt_light_wall.png
deleted file mode 100644
index 623dcd5..0000000
Binary files a/art/source/environment/textures/dirt_light_wall.png and /dev/null differ
diff --git a/art/source/environment/textures/dirt_wall.png b/art/source/environment/textures/dirt_wall.png
deleted file mode 100644
index dcc2364..0000000
Binary files a/art/source/environment/textures/dirt_wall.png and /dev/null differ
diff --git a/art/source/environment/textures/grass.png b/art/source/environment/textures/grass.png
deleted file mode 100644
index 8d9761c..0000000
Binary files a/art/source/environment/textures/grass.png and /dev/null differ
diff --git a/art/source/environment/textures/grass_heavy.png b/art/source/environment/textures/grass_heavy.png
deleted file mode 100644
index 29bfa2a..0000000
Binary files a/art/source/environment/textures/grass_heavy.png and /dev/null differ
diff --git a/art/source/environment/textures/grass_lite.png b/art/source/environment/textures/grass_lite.png
deleted file mode 100644
index 52c7ccb..0000000
Binary files a/art/source/environment/textures/grass_lite.png and /dev/null differ
diff --git a/art/source/environment/textures/grass_olive_tile.png b/art/source/environment/textures/grass_olive_tile.png
deleted file mode 100644
index b701eed..0000000
Binary files a/art/source/environment/textures/grass_olive_tile.png and /dev/null differ
diff --git a/art/source/environment/textures/sand.png b/art/source/environment/textures/sand.png
deleted file mode 100644
index 06f807a..0000000
Binary files a/art/source/environment/textures/sand.png and /dev/null differ
diff --git a/art/source/environment/textures/sand_dark.png b/art/source/environment/textures/sand_dark.png
deleted file mode 100644
index 80633b5..0000000
Binary files a/art/source/environment/textures/sand_dark.png and /dev/null differ
diff --git a/art/source/environment/textures/sand_lite.png b/art/source/environment/textures/sand_lite.png
deleted file mode 100644
index 06f807a..0000000
Binary files a/art/source/environment/textures/sand_lite.png and /dev/null differ
diff --git a/art/source/environment/textures/sandstone_heavy.png b/art/source/environment/textures/sandstone_heavy.png
deleted file mode 100644
index 1bb282c..0000000
Binary files a/art/source/environment/textures/sandstone_heavy.png and /dev/null differ
diff --git a/art/source/environment/textures/sandstone_lite.png b/art/source/environment/textures/sandstone_lite.png
deleted file mode 100644
index c329585..0000000
Binary files a/art/source/environment/textures/sandstone_lite.png and /dev/null differ
diff --git a/art/source/items/equipment/.gdignore b/art/source/items/equipment/.gdignore
deleted file mode 100644
index e69de29..0000000
diff --git a/art/source/items/equipment/net.blend b/art/source/items/equipment/net.blend
deleted file mode 100644
index 0a7ba4c..0000000
Binary files a/art/source/items/equipment/net.blend and /dev/null differ
diff --git a/art/ui/ui_notepad.png b/art/ui/ui_notepad.png
deleted file mode 100644
index eab6ff4..0000000
Binary files a/art/ui/ui_notepad.png and /dev/null differ
diff --git a/art/ui/ui_notepad.png.import b/art/ui/ui_notepad.png.import
deleted file mode 100644
index 9844e04..0000000
--- a/art/ui/ui_notepad.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://qmot6d6u44yq"
-path="res://.godot/imported/ui_notepad.png-4c175e6e85c581f3c18d5efd8a7cf669.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://art/ui/ui_notepad.png"
-dest_files=["res://.godot/imported/ui_notepad.png-4c175e6e85c581f3c18d5efd8a7cf669.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/audio/ambience/rain_loop_ontario.ogg b/audio/ambience/rain_loop_ontario.ogg
deleted file mode 100644
index 179987b..0000000
Binary files a/audio/ambience/rain_loop_ontario.ogg and /dev/null differ
diff --git a/audio/ambience/rain_loop_ontario.ogg.import b/audio/ambience/rain_loop_ontario.ogg.import
deleted file mode 100644
index 280fc22..0000000
--- a/audio/ambience/rain_loop_ontario.ogg.import
+++ /dev/null
@@ -1,19 +0,0 @@
-[remap]
-
-importer="oggvorbisstr"
-type="AudioStreamOggVorbis"
-uid="uid://cfqskba5j2iig"
-path="res://.godot/imported/rain_loop_ontario.ogg-c206e8a7ee5398c9fdc6d59462050964.oggvorbisstr"
-
-[deps]
-
-source_file="res://audio/ambience/rain_loop_ontario.ogg"
-dest_files=["res://.godot/imported/rain_loop_ontario.ogg-c206e8a7ee5398c9fdc6d59462050964.oggvorbisstr"]
-
-[params]
-
-loop=false
-loop_offset=0
-bpm=0
-beat_count=0
-bar_beats=4
diff --git a/audio/ambience/waves.wav b/audio/ambience/waves.wav
deleted file mode 100644
index 15f258e..0000000
Binary files a/audio/ambience/waves.wav and /dev/null differ
diff --git a/audio/ambience/waves.wav.import b/audio/ambience/waves.wav.import
deleted file mode 100644
index 5ff6ba8..0000000
--- a/audio/ambience/waves.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://dck3tf8qkadea"
-path="res://.godot/imported/waves.wav-90ffb886ce064ba838eebb25ec72a17e.sample"
-
-[deps]
-
-source_file="res://audio/ambience/waves.wav"
-dest_files=["res://.godot/imported/waves.wav-90ffb886ce064ba838eebb25ec72a17e.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=1
-edit/loop_begin=0
-edit/loop_end=1044956
-compress/mode=0
diff --git a/audio/music/world/craft.mp3 b/audio/music/world/craft.mp3
deleted file mode 100644
index b73cd15..0000000
Binary files a/audio/music/world/craft.mp3 and /dev/null differ
diff --git a/audio/music/world/craft.mp3.import b/audio/music/world/craft.mp3.import
deleted file mode 100644
index b8e054c..0000000
--- a/audio/music/world/craft.mp3.import
+++ /dev/null
@@ -1,19 +0,0 @@
-[remap]
-
-importer="mp3"
-type="AudioStreamMP3"
-uid="uid://c4y8puv0n6xk8"
-path="res://.godot/imported/craft.mp3-ba8d0c0c16ba90585f5d8eb4f37eb87d.mp3str"
-
-[deps]
-
-source_file="res://audio/music/world/craft.mp3"
-dest_files=["res://.godot/imported/craft.mp3-ba8d0c0c16ba90585f5d8eb4f37eb87d.mp3str"]
-
-[params]
-
-loop=false
-loop_offset=0
-bpm=0
-beat_count=0
-bar_beats=4
diff --git a/audio/music/world/tulips.wav b/audio/music/world/tulips.wav
deleted file mode 100644
index 9cd4605..0000000
Binary files a/audio/music/world/tulips.wav and /dev/null differ
diff --git a/audio/music/world/tulips.wav.import b/audio/music/world/tulips.wav.import
deleted file mode 100644
index 0f8cce1..0000000
--- a/audio/music/world/tulips.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://5hgmht4bq1my"
-path="res://.godot/imported/tulips.wav-7c18a300a62f25396913efb28c2ba3f6.sample"
-
-[deps]
-
-source_file="res://audio/music/world/tulips.wav"
-dest_files=["res://.godot/imported/tulips.wav-7c18a300a62f25396913efb28c2ba3f6.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/audio/sfx/fishing/bobber.wav b/audio/sfx/fishing/bobber.wav
deleted file mode 100644
index 8c9d916..0000000
Binary files a/audio/sfx/fishing/bobber.wav and /dev/null differ
diff --git a/audio/sfx/fishing/bobber.wav.import b/audio/sfx/fishing/bobber.wav.import
deleted file mode 100644
index d5fc82d..0000000
--- a/audio/sfx/fishing/bobber.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://sxydt4wnmrj3"
-path="res://.godot/imported/bobber.wav-b435bb251a112445f1ddebcacc74804a.sample"
-
-[deps]
-
-source_file="res://audio/sfx/fishing/bobber.wav"
-dest_files=["res://.godot/imported/bobber.wav-b435bb251a112445f1ddebcacc74804a.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/audio/sfx/fishing/fighting.wav b/audio/sfx/fishing/fighting.wav
deleted file mode 100644
index f647069..0000000
Binary files a/audio/sfx/fishing/fighting.wav and /dev/null differ
diff --git a/audio/sfx/fishing/fighting.wav.import b/audio/sfx/fishing/fighting.wav.import
deleted file mode 100644
index ec59561..0000000
--- a/audio/sfx/fishing/fighting.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://df6yg85cldetw"
-path="res://.godot/imported/fighting.wav-1427843e18acf5134cc66d4a813033b6.sample"
-
-[deps]
-
-source_file="res://audio/sfx/fishing/fighting.wav"
-dest_files=["res://.godot/imported/fighting.wav-1427843e18acf5134cc66d4a813033b6.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=1
-edit/loop_begin=0
-edit/loop_end=124487
-compress/mode=0
diff --git a/audio/sfx/fishing/reeling.wav b/audio/sfx/fishing/reeling.wav
deleted file mode 100644
index 6be5d03..0000000
Binary files a/audio/sfx/fishing/reeling.wav and /dev/null differ
diff --git a/audio/sfx/fishing/reeling.wav.import b/audio/sfx/fishing/reeling.wav.import
deleted file mode 100644
index e5e6749..0000000
--- a/audio/sfx/fishing/reeling.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://v4yjxtslbrd7"
-path="res://.godot/imported/reeling.wav-97a9415ae2d9e7ca4c976bb15b10e6fd.sample"
-
-[deps]
-
-source_file="res://audio/sfx/fishing/reeling.wav"
-dest_files=["res://.godot/imported/reeling.wav-97a9415ae2d9e7ca4c976bb15b10e6fd.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=1
-edit/loop_begin=0
-edit/loop_end=114628
-compress/mode=0
diff --git a/collection/collection_log.gd b/collection/collection_log.gd
index 59d33d1..ad0ab56 100644
--- a/collection/collection_log.gd
+++ b/collection/collection_log.gd
@@ -2,15 +2,9 @@ class_name CollectionLog
extends Node
signal fish_discovered(fish_id: StringName)
-signal fish_quality_discovered(fish_id: StringName, quality: int)
signal collection_changed
-const FishQualityType = preload("res://fish/fish_quality.gd")
-const MAX_CATCH_COUNT: int = 1000000000
-
var _discovered: Dictionary[StringName, bool] = {}
-var _quality_masks: Dictionary[StringName, int] = {}
-var _catch_counts: Dictionary[StringName, int] = {}
func has_discovered(fish_id: StringName) -> bool:
@@ -25,44 +19,6 @@ func mark_discovered(fish_id: StringName) -> void:
collection_changed.emit()
-func mark_quality_discovered(fish_id: StringName, quality: int) -> void:
- if fish_id.is_empty() or not FishQualityType.is_valid(quality):
- return
- var species_was_discovered: bool = has_discovered(fish_id)
- if not species_was_discovered:
- _discovered[fish_id] = true
- var previous_mask: int = _quality_masks.get(fish_id, 0)
- var next_mask: int = previous_mask | FishQualityType.bit_for(quality)
- if species_was_discovered and next_mask == previous_mask:
- return
- _quality_masks[fish_id] = next_mask
- if not species_was_discovered:
- fish_discovered.emit(fish_id)
- if next_mask != previous_mask:
- fish_quality_discovered.emit(fish_id, quality)
- collection_changed.emit()
-
-
-func record_catch(fish_id: StringName, quality: int) -> void:
- if fish_id.is_empty() or not FishQualityType.is_valid(quality):
- return
- var species_was_discovered: bool = has_discovered(fish_id)
- if not species_was_discovered:
- _discovered[fish_id] = true
- var previous_mask: int = _quality_masks.get(fish_id, 0)
- var next_mask: int = previous_mask | FishQualityType.bit_for(quality)
- _quality_masks[fish_id] = next_mask
- _catch_counts[fish_id] = mini(
- int(_catch_counts.get(fish_id, 0)) + 1,
- MAX_CATCH_COUNT,
- )
- if not species_was_discovered:
- fish_discovered.emit(fish_id)
- if next_mask != previous_mask:
- fish_quality_discovered.emit(fish_id, quality)
- collection_changed.emit()
-
-
func get_discovered_ids() -> Array[StringName]:
var discovered_ids: Array[StringName] = []
for fish_id: StringName in _discovered:
@@ -71,89 +27,12 @@ func get_discovered_ids() -> Array[StringName]:
return discovered_ids
-func has_discovered_quality(fish_id: StringName, quality: int) -> bool:
- return (
- not fish_id.is_empty()
- and FishQualityType.is_valid(quality)
- and (_quality_masks.get(fish_id, 0) & FishQualityType.bit_for(quality)) != 0
- )
-
-
-func get_quality_mask(fish_id: StringName) -> int:
- return _quality_masks.get(fish_id, 0) if has_discovered(fish_id) else 0
-
-
-func get_discovered_quality_masks() -> Dictionary[StringName, int]:
- return _quality_masks.duplicate()
-
-
-func get_catch_count(fish_id: StringName) -> int:
- return _catch_counts.get(fish_id, 0) if has_discovered(fish_id) else 0
-
-
-func get_catch_counts() -> Dictionary[StringName, int]:
- return _catch_counts.duplicate()
-
-
-func has_mastered(fish_id: StringName) -> bool:
- return get_quality_mask(fish_id) == FishQualityType.ALL_TIERS_MASK
-
-
func replace_discovered_ids(fish_ids: Array[StringName]) -> bool:
- var empty_masks: Dictionary[StringName, int] = {}
- return replace_discovery_state(fish_ids, empty_masks)
-
-
-func replace_discovery_state(
- fish_ids: Array[StringName],
- quality_masks: Dictionary[StringName, int],
-) -> bool:
- var preserved_counts: Dictionary[StringName, int] = {}
- for fish_id: StringName in fish_ids:
- if _catch_counts.has(fish_id):
- preserved_counts[fish_id] = _catch_counts[fish_id]
- return replace_collection_state(
- fish_ids,
- quality_masks,
- preserved_counts,
- )
-
-
-func replace_collection_state(
- fish_ids: Array[StringName],
- quality_masks: Dictionary[StringName, int],
- catch_counts: Dictionary[StringName, int],
-) -> bool:
var replacement: Dictionary[StringName, bool] = {}
for fish_id: StringName in fish_ids:
if fish_id.is_empty():
return false
replacement[fish_id] = true
- var replacement_masks: Dictionary[StringName, int] = {}
- for fish_id: StringName in quality_masks:
- var mask: int = quality_masks[fish_id]
- if (
- fish_id.is_empty()
- or not replacement.has(fish_id)
- or mask < 0
- or (mask & ~FishQualityType.ALL_TIERS_MASK) != 0
- ):
- return false
- if mask != 0:
- replacement_masks[fish_id] = mask
- var replacement_counts: Dictionary[StringName, int] = {}
- for fish_id: StringName in catch_counts:
- var count: int = catch_counts[fish_id]
- if (
- fish_id.is_empty()
- or not replacement.has(fish_id)
- or count <= 0
- or count > MAX_CATCH_COUNT
- ):
- return false
- replacement_counts[fish_id] = count
_discovered = replacement
- _quality_masks = replacement_masks
- _catch_counts = replacement_counts
collection_changed.emit()
return true
diff --git a/default_bus_layout.tres b/default_bus_layout.tres
index d780a19..ca38684 100644
--- a/default_bus_layout.tres
+++ b/default_bus_layout.tres
@@ -13,15 +13,3 @@ bus/1/mute = false
bus/1/bypass_fx = false
bus/1/volume_db = 0.0
bus/1/send = &"Master"
-bus/2/name = &"SFX"
-bus/2/solo = false
-bus/2/mute = false
-bus/2/bypass_fx = false
-bus/2/volume_db = 0.0
-bus/2/send = &"Master"
-bus/3/name = &"Environment"
-bus/3/solo = false
-bus/3/mute = false
-bus/3/bypass_fx = false
-bus/3/volume_db = 0.0
-bus/3/send = &"Master"
diff --git a/docs/ATTRIBUTION.md b/docs/ATTRIBUTION.md
deleted file mode 100644
index d8676f0..0000000
--- a/docs/ATTRIBUTION.md
+++ /dev/null
@@ -1,334 +0,0 @@
-# Attribution, licensing, and asset provenance
-
-This is the authoritative project record for ownership, credits, license
-boundaries, third-party notices, asset provenance, and redistribution checks.
-A Git commit proves when bytes entered the repository; it does not by itself
-prove authorship or licensing.
-
-## Ownership and credits
-
-straywild is jointly owned, developed, and published by its creators,
-publicly credited as Voyager and Endeavour and operating collectively as the
-independent team Woofmeow.
-
-- Voyager: co-owner, developer, project director, creator of the game's
- provisional fish placeholder artwork and original 3D work except where
- another creator is credited, and composer of the original title track
- `audio/music/title/as_in_four_wolves.ogg`, dusk/world track
- `audio/music/world/craft.mp3`, and the synthesized robot animalese tones.
-- Endeavour: co-owner, developer, and 2D artist. Her incorporated work includes
- character pattern/channel-map art, environment and UI art, item art, and
- finished fish and creature artwork. Planned work is not described as
- incorporated until its files enter the project.
-- chillnfill: contributor of original 3D models for the character bodies and
- arms, ears and tails, and multiple world props and decorative assets,
- including trees and bridges. The contributor requested to be credited as
- `chillnfill`.
-- kat: supplied the original source voice recording for the kat animalese set.
- Voyager created the sliced, normalized runtime derivatives.
-- kim: supplied the original source voice recording for the kim animalese set.
- Voyager created the sliced, normalized runtime derivatives.
-
-Voyager is also the original catalog porter credit in PortMaster metadata.
-Third-party creators are credited with their individual records below. Those
-credits do not imply endorsement of straywild, Woofmeow, or its owners.
-
-## License map
-
-Do not describe the complete repository as covered only by the GPL.
-
-| Material | Governing terms |
-| --- | --- |
-| Project-owned software code, shaders, code-oriented scene/resource configuration, tests, build tooling, and server software | GPL-3.0-or-later; source-root `LICENSE` and packaged `GPL-3.0-or-later.txt` |
-| Project-owned artwork, textures, models, animation, music, audio, narrative content, characters, and other creative assets | Source-root or packaged `ASSET-LICENSE.md` |
-| straywild and Woofmeow names, logos, icons, and official identity | Source-root or packaged `TRADEMARKS.md` |
-| Third-party engines, fonts, sounds, and other external material | Their original terms, recorded below and in adjacent license texts where applicable |
-| Previously accepted or expressly invited outside contributions | Source-root `CONTRIBUTOR-TERMS.md` plus any separately signed agreement |
-
-Generated Godot import metadata and deterministic generated resources follow
-the terms of their source material or generating code. Repository location
-alone does not relicense third-party material.
-
-The GPL permits study, modification, redistribution, and forks of covered
-code. It does not grant rights to straywild creative assets or branding. The
-straywild Asset License separately permits qualifying noncommercial add-on
-Mods for official straywild software and narrowly scoped Contribution Forks
-used to prepare changes for possible upstream inclusion. It does not permit
-those assets to accompany independent code forks, clones, standalone games,
-unrelated products, or general-purpose asset packs. A GPL fork that is not a
-qualifying Contribution Fork must replace straywild assets and reserved
-branding with independently licensed material and a distinct identity.
-
-Project-wide code, asset, branding, or contributor-license changes require
-written approval from both straywild owners under their private joint
-ownership agreement.
-
-## Godot Engine notice
-
-straywild uses Godot Engine under the MIT License:
-
-> Copyright (c) 2014-present Godot Engine contributors.
-> Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.
->
-> Permission is hereby granted, free of charge, to any person obtaining a copy
-> of this software and associated documentation files (the “Software”), to deal
-> in the Software without restriction, including without limitation the rights
-> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-> copies of the Software, and to permit persons to whom the Software is
-> furnished to do so, subject to the following conditions:
->
-> The above copyright notice and this permission notice shall be included in
-> all copies or substantial portions of the Software.
->
-> THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-> SOFTWARE.
-
-Godot licensing and bundled third-party-component notices are available at
-https://godotengine.org/license/ and
-https://github.com/godotengine/godot/blob/master/COPYRIGHT.txt. Godot uses
-FreeType for font rendering; portions are copyright The FreeType Project
-(https://www.freetype.org) under its applicable license.
-
-## Font notices
-
-- **Tuffy Bold** — Thatcher Ulrich, Karoly Barta, and Michael Evans; dedicated
- to the public domain. Runtime file: `ui/fonts/Tuffy_Bold.otf`; original notice:
- `ui/fonts/Tuffy-LICENSE.txt`.
-- **Seattle Avenue** — JLH Fonts; dedicated to the public domain by the creator.
- Runtime file: `ui/fonts/seattle_avenue.otf`; source, hash, and original notice:
- `ui/fonts/Seattle-Avenue-LICENSE.txt`.
-
-Public-domain fonts are not governed by the straywild Asset License.
-
-## Intake policy
-
-For every new external or supplied asset, retain a durable intake record with:
-
-- creator or supplying party;
-- original source location or delivery channel;
-- source filename and, when useful, its initial repository destination;
-- source and destination SHA-256 when copied without modification;
-- license or explicit permission;
-- any permitted transformations;
-- the importing commit or release.
-
-The intake record may live in the accepted pull request, issue, email, or the
-owners' private project records. This public document should contain the
-credits and license notices needed by recipients; it should not duplicate a
-changing file inventory that can be derived from the repository itself.
-
-straywild does not accept unsolicited contributions. For work expressly
-invited by both Owners, maintainers must obtain affirmative agreement to the
-source-root `CONTRIBUTOR-TERMS.md` and retain the written invitation and
-agreement in a durable Project record.
-
-Runtime resources must use `res://` paths. Workstation Sync folders are intake
-locations only and must never appear in scenes or resources.
-
-## Project-created artwork and music
-
-Fish entries without finished replacement artwork use provisional placeholder
-artwork by Voyager. Incorporated finished fish and creature artwork is by
-Endeavour; work that has not entered this repository is not presented here as
-part of the game or attributed as a shipped asset.
-
-Endeavour's incorporated 2D contributions include the inventory notepad,
-environment and UI artwork, item artwork, finished fish and creature artwork,
-and character customization pattern and channel-map artwork. Voyager's
-incorporated original work includes 3D assets, the provisional fish
-placeholder artwork, the title and dusk music identified above, and the robot
-animalese tones. The current starter motorcoach exterior was supplied by a
-contributing artist who released its copyright to the project; the original
-and cleaned source files are retained in the owners' source-work archive.
-Specific contributor exceptions are recorded below and in the in-game credits.
-
-These credits intentionally describe work by creator and contribution family
-instead of maintaining a second file-by-file asset catalog. Runtime resources
-and Git history identify the files present in a particular revision. A path,
-directory name, commit author, or supplying party is not by itself evidence of
-authorship.
-
-## Third-party and contributor runtime material
-
-The following paths identify distributed material whose external license or
-individual contributor credit must remain easy to find. They were checked
-against the repository when this record was revised.
-
-| Material | Runtime location | Credit or terms |
-| --- | --- | --- |
-| Kat animalese alphabet and number sounds | `sound/dialogue/animalese/alphanumeric/` | Project-processed derivatives of the original source voice recording supplied by straywild community member kat. |
-| Kim animalese alphabet and number sounds | `sound/dialogue/animalese/kim/` | Project-processed derivatives of the original source voice recording supplied by straywild community member kim. |
-| Tuffy font | `ui/fonts/Tuffy_Bold.otf` | Public-domain dedication in `ui/fonts/Tuffy-LICENSE.txt`. |
-| Seattle Avenue font | `ui/fonts/seattle_avenue.otf` | Public-domain font by JLH Fonts; notice in `ui/fonts/Seattle-Avenue-LICENSE.txt`. |
-| Fishing fight loop | `audio/sfx/fishing/fighting.wav` | Edited from “Spinning reel.wav” by Freesound user tosha73, sound 509902, CC0. |
-| Manual-reeling loop | `audio/sfx/fishing/reeling.wav` | Edited from the same CC0 “Spinning reel.wav” source. |
-| Saltwater wave ambience | `audio/ambience/waves.wav` | “Gentle Ocean Waves Loop” by Freesound user kkenny101, sound 852826, CC0. |
-| Bobber water impact | `audio/sfx/fishing/bobber.wav` | “Quick Water Droplet” by Freesound user qubodup, sound 792931, CC0. |
-| Rain ambience | `audio/ambience/rain_loop_ontario.ogg` | Converted from “Rain Loop Ontario” by Freesound user Ayton, sound 212799, CC BY 3.0. |
-| Character bark call | `sound/dialogue/calls/bark.wav` | Edited from `Dog_Bark.wav` by Freesound user ivolipa, sound 328729, CC0. |
-| Character meow call | `sound/dialogue/calls/meow.wav` | Edited from `Cat meow.m4a` by Freesound user Christyboy100, sound 495694, Attribution 3.0. |
-
-Creative Commons Zero information:
-https://creativecommons.org/publicdomain/zero/1.0/. Creative Commons
-Attribution 3.0 legal text:
-https://creativecommons.org/licenses/by/3.0/legalcode.
-
-### chillnfill 3D model contribution record
-
-- Requested credit: `chillnfill`.
-- Original contribution families: character bodies and arms; character ears
- and tails; and multiple world props and decorative assets, including trees
- and bridges.
-- Repository mapping: these contributions may be incorporated into combined
- character or environment resources rather than retained as one file per
- originally supplied model. Git history is the revision-specific file map.
-- Permission record: because these historical contributions predate the
- current `CONTRIBUTOR-TERMS.md`, retain the original permission record or a
- later written confirmation covering Project use, modification,
- distribution, commercial release, and applicable sublicensing.
-
-### Endeavour 2D contribution record
-
-Endeavour created the incorporated character customization patterns and
-channel maps as well as additional UI, environment, item, fish, and creature
-artwork. Voyager's provisional fish placeholder is not part of this credit.
-New or replacement art is added to this record only after it is accepted into
-the repository.
-
-The exact current filenames and bytes are intentionally not copied into this
-document. The character appearance resources and Git history are the
-revision-specific inventory.
-
-
-### Fishing fight loop source record
-
-- Source page: https://freesound.org/s/509902/
-- Creator: tosha73
-- Source title: `Spinning reel.wav`
-- License: Creative Commons Zero (CC0)
-- Downloaded source filename: `509902__tosha73__spinning-reel.wav`
-- Downloaded source SHA-256:
- `9741afdb1ec9c73f5e039f0e5ac174998cb0a1f737b626531e63919d4d232fad`
-- Audacity working project: `fighting.aup3`
-- Audacity project SHA-256:
- `9928f077ecf48d01416f8d05700707e1b07d0f731846db80e27116f5b82dbbf5`
-- Runtime edit source/destination SHA-256:
- `b6f3f2f94bfaf9254ac04dd96b1fd28c15e1011a1db44bb3e70eeaa95de3c212`
-- Runtime format: 48 kHz, 16-bit, stereo PCM WAV; 2.593479 seconds;
- configured as a forward loop by the fishing presentation.
-- Manual-reeling Audacity project: `reeling.aup3`
-- Manual-reeling Audacity project SHA-256:
- `20ad52f6bdd16ae6561f06f8355fe309e95023e815053fd5dfbd5a5ce3c1f9f9`
-- Manual-reeling runtime source/destination SHA-256:
- `e403d5b7c8af1da4b28599cc0f51de3eda70d9e9296db27852fe7dc95617a29d`
-- Manual-reeling runtime format: 48 kHz, 16-bit, stereo PCM WAV;
- 2.388083 seconds; configured as a forward loop.
-- The downloaded source and Audacity projects remain in the project owner's
- source-work archive; only the finished runtime edits ship in the game.
-
-### Saltwater wave ambience source record
-
-- Source page: https://freesound.org/s/852826/
-- Creator: kkenny101
-- Source title: `Gentle Ocean Waves Loop`
-- License: Creative Commons Zero (CC0)
-- Downloaded source filename:
- `852826__kkenny101__gentle-ocean-waves-loop.wav`
-- Downloaded source and runtime source/destination SHA-256:
- `f93e9890583f072ac52cf197b8b5942397d3d5e889fbdb9112a0776142f50229`
-- Runtime format: 48 kHz, 24-bit, mono PCM WAV; 21.769917 seconds;
- configured as a forward loop.
-- The downloaded source remains in the project owner's source-work archive;
- only the finished runtime loop ships in the game.
-
-### Bobber water-impact source record
-
-- Source page: https://freesound.org/s/792931/
-- Creator: qubodup
-- Source title: `Quick Water Droplet`
-- License: Creative Commons Zero (CC0)
-- Downloaded source filename: `792931__qubodup__quick-water-droplet.wav`
-- Downloaded source and runtime source/destination SHA-256:
- `aef0d16384efed4a7b071c4d80c21597617f882f4e0c1d5295ac194d315eafb7`
-- Runtime format: 48 kHz, 16-bit, mono PCM WAV; 0.197146 seconds; one-shot.
-- The downloaded source remains in the project owner's source-work archive;
- only the finished runtime sound ships in the game.
-
-### Character-call source records
-
-#### Bark
-
-- Source page: https://freesound.org/s/328729/
-- Creator: ivolipa
-- Source title: `Dog_Bark.wav`
-- License: Creative Commons Zero (CC0)
-- Downloaded source filename: `328729__ivolipa__dog_bark.wav`
-- Downloaded source SHA-256:
- `c3d542dee7bfcc1901428e0c1408c0bd522d778d8e5e1dbca156cab008f3d712`
-- Runtime edit source/destination SHA-256:
- `5ac2f32f33724a2fc232d725c5c37d4cfe3e93b3c1f19aa4177d2c8e3009ec78`
-- Runtime format: 44.1 kHz, 16-bit, stereo PCM WAV; 0.446780 seconds;
- one-shot.
-
-#### Meow
-
-- Source page: https://freesound.org/s/495694/
-- Creator: Christyboy100
-- Source title: `Cat meow.m4a`
-- License: Attribution 3.0
-- Downloaded source filename: `495694__christyboy100__cat-meow.m4a`
-- Downloaded source SHA-256:
- `afa3f6ef7afa2f7fee1e634d5505d2101cbba4421f8d8ff9adf562939c7a18f8`
-- Converted working WAV SHA-256:
- `a505e6e230e7d9d76b65c46270d97a35a1acc510b43aa5e7343d7b04f424cade`
-- Runtime edit source/destination SHA-256:
- `cce22ce1fbbc9325126a153aedc4526fde1aacf78780574fb432197dd44eb6b3`
-- Runtime format: 44.1 kHz, 16-bit, stereo PCM WAV; 0.605420 seconds;
- one-shot.
-- The downloaded sources and working edits remain in the project owner's
- source-work archive; only the finished runtime sounds ship in the game.
-
-### Rain ambience source record
-
-- Source page: https://freesound.org/s/212799/
-- Creator: Ayton
-- Source title: `Rain Loop Ontario`
-- License: Creative Commons Attribution 3.0
-- Downloaded source filename: `212799__ayton__rain-loop-ontario.aiff`
-- Downloaded source SHA-256:
- `b491fc036b78a274d9ac7f6acbd014af1b5d24f5cba9bd7e3fdc05c5432ee66f`
-- Runtime edit SHA-256:
- `b1979609b20fe34ed6f8902f3de65ae787a09abd71429e8aab850e87b74a4fa2`
-- Changes: converted from AIFF to Ogg Vorbis and configured as looping
- weather ambience.
-
-## Generated resources
-
-Godot `.import` sidecars and deterministic shoreline `.tres` meshes are derived
-repository resources, not original artwork. `.godot/imported/`, editor caches,
-captures, exports, and temporary test data are not tracked.
-
-## Release gate
-
-Before a public binary or source release:
-
-1. Search exported resources for external filesystem paths.
-2. Include or make readily accessible the complete GPL text and identify the
- exact corresponding source tag or commit.
-3. Include `ASSET-LICENSE.md`, `TRADEMARKS.md`, this attribution/provenance
- record, both font notices, and every applicable third-party license.
-4. Retain the complete Godot Engine MIT notice above.
-5. Preserve third-party terms without adding incompatible restrictions.
-6. Avoid referring to a dirty or unavailable source revision.
-7. Publish or offer equivalent access to exact corresponding GPL source for
- every distributed binary release.
-
-Desktop and PortMaster build scripts stage these authoritative files as
-sidecars. Godot export presets include them in exported resource packs for
-platforms where sidecar documents are not normally visible. Store and release
-pages should link to the exact source revision and public notices.
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
deleted file mode 100644
index 76ed032..0000000
--- a/docs/DEVELOPMENT.md
+++ /dev/null
@@ -1,368 +0,0 @@
-# Development reference
-
-This is the authoritative reference for straywild architecture, engineering
-decisions, content policy, authoring, and validation.
-
-## Architecture
-
-### Composition
-
-`main/main.tscn` is the application composition root. It owns long-lived
-services, the active world, player container, save/settings managers, and the
-pixelated UI viewport. Services are scene-owned rather than global autoloads,
-which makes their dependencies visible in the main scene and allows focused
-tests to instantiate the same composition.
-
-The main script coordinates title, session, world, and UI lifecycle. Domain
-logic remains in typed resources and services rather than being stored solely
-in controls.
-
-### Domain boundaries
-
-- `fish/` defines species, availability, catches, pools, quality, and selection.
-- `fishing/` owns cast/chase state and resolves authoritative fishing surfaces.
-- `inventory/`, `items/`, `economy/`, `progression/`, and `jobs/` own persistent
- player-facing game state.
-- `world/` owns authored map composition and presentation. Water type is a
- shared typed definition used by water bodies, fish habitat, fishing
- validation, Logbook classification, and saltwater shoreline presentation.
-- `ui/` observes and invokes domain services. It does not define stable item or
- fish identity.
-
-Resource files (`.tres`) are authored data. Stable IDs, not display names or
-node names, connect persistent and networked state to that data.
-
-### Networking and authority
-
-`NetworkSession` owns ENet session lifecycle and peer registration. Dedicated
-network services validate and replicate bounded domains such as fishing,
-sales, shops, item use, profiles, jobs, mail, chat, drawings, time, and weather.
-
-`DiscoveryClient` is an optional directory layer beside `NetworkSession`. An
-open host may publish a short-lived room lease, and the shared Join Game page
-may browse compatible leases before handing the selected address back to the
-existing direct ENet connection flow. The directory does not carry gameplay
-traffic or become a gameplay authority. Its base URL comes from
-`network/discovery/base_url`, with `straywild_DISCOVERY_URL` available as a
-development/deployment override.
-
-Friendships are local identity relationships shared across save slots. A live,
-authenticated gameplay session is required to exchange the directional
-capabilities that establish a friendship. Blocking an identity removes that
-friendship; unblocking does not recreate it. Discovery may publish opt-in,
-short-lived friend presence and deliver invitations only while both games are
-online. It stores hashed capability identifiers rather than identity
-fingerprints or a complete friend graph, keeps no durable social records, and
-provides no offline delivery. Direct joins still use the existing verified
-public-room and ENet connection path.
-
-The host is authoritative. Clients submit requests or evidence; the host
-derives trusted context from registered peers, authoritative regions, and
-server-owned state before mutating inventory, wallet, progression, or shared
-world state. Private single-player uses the same host path. Client evidence is
-bounded and reconstructed where possible; UI activation is never proof that a
-server-side mutation succeeded.
-
-Moderation follows the same boundary. Player hosts may grant session-scoped
-operator status to an authenticated identity. Dedicated servers derive
-operators from their configured fingerprint allowlist. Operator status is
-replicated for presentation, but kick, ban, unban, and artwork-reset requests
-are always reauthorized against the authenticated sender by the host. Only a
-player host can grant or revoke operators; operators cannot moderate the host
-or another operator.
-
-Protocol compatibility is defined in `network/network_protocol.gd`. A visible
-release version is not a reason to change the protocol number.
-
-### Persistence and stable identifiers
-
-`PlayerDataRoot` selects and validates a portable data root. Stores receive
-paths from that owner rather than inventing unrelated locations. Progression is
-written by `PlayerSaveManager` and indexed as named slots by
-`PlayerSaveSlotCatalog`. Existing single-save installations are adopted as the
-first slot in place. Device settings, appearance, and social/identity stores
-remain shared across slots and have separate formats and lifecycles.
-Progression archive import and export belong to the title-screen Play page:
-imports create a new slot, while exports copy only the selected progression.
-
-Save migrations are sequential and explicit. Existing catches and ownership
-are keyed by stable IDs so authored metadata can evolve without rewriting
-historical records. Current source constants—not documentation—are
-authoritative for format versions.
-
-Repository-owned resources are authoritative authored data. Persistent and
-networked records refer to stable IDs; display names, filenames, node names,
-coordinates, and pool names are not compatibility identifiers. Cross-domain
-concepts such as water type use one typed definition. Removing or changing a
-stable ID requires an explicit compatibility plan.
-
-### Presentation
-
-Gameplay is rendered in 3D with the GL Compatibility renderer. The main UI is
-presented through a uniformly scaled SubViewport. Shared UI components and
-palette resources prevent page-specific geometry and style drift.
-
-World presentation systems (time/weather visuals, procedural sky and water,
-shoreline treatment, player blob shadows) do not own gameplay collision or
-network authority.
-
-### Engineering consequences
-
-- Shared mutations must behave consistently for private hosts, open hosts,
- dedicated servers, and joined clients.
-- Networking changes require local-host and paired host/client validation.
-- Protocol payloads and rejection cleanup are explicit and bounded.
-- Renaming visible UI does not require a save migration.
-- Content validation covers stable-ID uniqueness, catalog completeness, pool
- membership, and typed habitat compatibility.
-- New authored assets require provenance records as well as resource
- references.
-
-## Content and balance
-
-Balance is authored data and should change deliberately. Exact current values
-remain authoritative in `.tres` resources and typed scripts.
-
-### Fish
-
-- Stable fish IDs and catalog numbers must not be changed for display cleanup.
-- Species availability may constrain water type, time, weather, bait, and
- other authored context.
-- Location pools control selection weight; the global catalog remains
- comprehensive.
-- Freshwater and saltwater habitat is validated by the host in addition to
- pool membership.
-- Value, rarity, quality, weight ranges, and authored barrier-health bands
- should be reviewed together because they affect economy and catch
- difficulty.
-
-### Economy and progression
-
-- Sales and purchases are host-authoritative and must mutate inventory and
- wallet exactly once.
-- Reserved assets are rejected atomically; mixed valid and reserved batches
- must not partially succeed.
-- Item effects, shop prices, inventory and storage capacity, fishing upgrades,
- jobs, and experience are separate balance axes. Avoid changing several in
- an unrelated presentation pass.
-
-Record affected stable IDs and resource paths, old and new values, intended
-player-facing outcome, interactions with quality/availability/economy,
-deterministic validation, and save-compatibility impact with each balance
-change. Do not bump a save schema or network protocol merely because an
-authored number changed.
-
-## Authoring
-
-### World composition
-
-`world/test_world.tscn` is the shared gameplay stage. It owns the environment,
-sun, world bounds, and below-world failsafe, then hosts exactly one active
-`WorldRegion`. Two canonical region implementations are supported:
-
-- the generated world, which is the default for new games and is rebuilt from
- the saved seed; and
-- the starter island, which remains an authored selectable layout and a useful
- compatibility/reference scene.
-
-The stable layout IDs live in `world/world_layout.gd`. Do not infer a layout
-from a scene filename, display label, or region node name. Both implementations
-must satisfy the shared `WorldRegion` contract for player spawn, safe respawns,
-water regions, recovery, gathering, digging, the shop, and player storage.
-
-A region owns the content that changes with the selected world:
-
-```text
-WorldRegion
-├── Terrain
-├── WaterBodies
-├── PlayerSpawn
-├── SafeRespawns
-├── DiggableAreas
-├── GatherableAnchors
-└── Interactables
- ├── FishingShopWorld
- └── PlayerStorageBox
-```
-
-The generated region owns its chunk, biome, and prop catalogs and derives
-terrain collision, water coverage, biome placement, and presentation reference
-geometry from the generated result. The starter-island region owns its imported
-terrain hierarchy and explicitly placed content. Code shared by both layouts
-must depend on `WorldRegion`, not a starter-island child path.
-
-Every fishable water body authors its water type explicitly. Fish species use
-the same central type through an allowed-water-type bitmask. Do not infer
-habitat from node names, pool filenames, coordinates, or water height.
-
-In the generated layout, seed and layout ID are persistent data. Chunk IDs,
-edge rules, biome definitions, and prop definitions are authored resources;
-changing them can change the world produced by an existing seed and therefore
-requires deliberate compatibility review.
-
-For the authored starter island, move a meaningful feature root rather than one
-of its implementation children. Child transforms are local offsets owned by
-that feature. Its imported GLB hierarchy is the starter region's terrain and
-collision authority; this rule does not describe generated-world collision.
-
-The active `Environment` and `Sun` remain Inspector-authored on the shared
-stage and are not recreated by either region.
-
-### Reusable world props
-
-Use one placed `WorldProp` root for a complete landmark:
-
-```text
-WorldProp
-├── Visual
-└── Collision
- └── CollisionShape3D
-```
-
-Put optional interaction areas, labels, and markers beneath the same root.
-Instance imported GLB content under `Visual` and keep gameplay collision under
-`Collision`. Author reusable assets at scale `(1, 1, 1)`. Uniform provisional
-root scaling is acceptable when visual and collision scale together; avoid
-non-uniform root scaling. Make mutable per-instance shapes, meshes, and
-materials local to the scene.
-
-### Shoreline presentation
-
-The visible shoreline treatment is currently produced by the water materials.
-The older smooth-ribbon baker and generated starter-island ribbon resource are
-retained as development history/tooling, but the ribbon mesh is disabled and
-is not part of the current rendered shoreline. Do not treat a ribbon rebuild
-as a normal terrain-authoring requirement or re-enable the mesh as incidental
-cleanup.
-
-### Facial-feature textures
-
-Put new PNGs under the matching directory in
-`art/exported/characters/faces/`:
-
-```text
-eyes/.png
-noses/.png
-mouth/.png
-```
-
-The category directory is authoritative. A category prefix is optional, so
-both `sleepy.png` and `eyes_sleepy.png` produce the stable option ID `sleepy`.
-Filenames normalize to lowercase snake_case and are stored in appearance
-snapshots, so do not casually rename them. Preserve the established RGBA
-transparent canvas. Restart a development build after adding an image; rebuild
-exports to package new `res://` files.
-
-The tracked import policy keeps these facial-feature textures at a maximum
-runtime size of 512×512, uses VRAM compression, and does not generate mipmaps.
-The source PNGs remain unchanged. Run the texture import normalizer shown below
-after adding facial artwork so low-memory builds do not retain full-resolution
-copies of the entire customization catalog.
-
-### Texture sampling
-
-straywild artwork uses nearest-neighbor sampling. Do not enable linear,
-bilinear, trilinear, or anisotropic texture filtering. Shader samplers must
-declare `filter_nearest`. The three canonical repeating terrain textures are
-the sole mipmap exception: their projected shader uses nearest-filtered
-mipmaps to prevent distance aliasing without smoothing nearby pixels. The
-`TextureSamplingPolicy` autoload applies nearest sampling to 2D canvas items,
-Sprite3D presentations, imported 3D materials, and dynamically constructed
-mesh materials at runtime.
-
-After importing new artwork, normalize its tracked import metadata where
-needed and run the focused policy check:
-
-```sh
-godot --headless --path . --script scripts/normalize_texture_imports.gd \
- -- --apply --root res://path/to/new/artwork
-godot --headless --path . --script tests/texture_sampling_validation.gd
-```
-
-### Importing animalese voice sets
-
-Animalese clips use one deterministic runtime level and format: -18 dBFS RMS
-with a -3 dBFS peak ceiling, mono, 48 kHz, 16-bit PCM. Import a supplied set
-through the normalization script rather than copying its WAV files directly:
-
-```bash
-scripts/import_animalese_voice.sh /path/to/source_clips
-```
-
-The source directory may contain lowercase letter, number, and underscore WAV
-filenames. The script converts every top-level WAV and writes the results under
-`sound/dialogue/animalese//`. Add a new runtime set to
-`player/animalese_voice_profiles.gd` after import, then run the quick validation
-suite. Keep the untouched submission and its original hashes in the private
-intake record; repository files are normalized derivatives.
-
-### Bubble menus
-
-Instance `ui/components/bubble_menu/bubble_button.tscn` for each action, or
-attach `bubble_button.gd` to an existing button. Author neutral size, desktop
-and compact anchors, font-size limits, and deterministic motion in the
-Inspector. A label child may be assigned with `label_control_path`.
-
-Place buttons under a Control using `bubble_cluster.gd`, pass ordered button
-references to `configure()`, and call `apply_layout()` when available size or
-responsive layout changes. Order defines keyboard/controller focus neighbors.
-The shared profile owns palette, styles, proportional-font ratio, hover
-response, and contact tuning. Parent menus own labels, actions, anchors,
-availability, and confirmation behavior.
-
-`motion_scale = 0.0` disables idle drift/deformation while preserving hover and
-focus feedback. Contact correction is deterministic and bounded; do not replace
-it with physics that destabilizes layout, focus order, or hit targets.
-
-## Validation
-
-Tests are executable Godot `SceneTree` scripts. Some are pure content/domain
-checks, some instantiate the main scene, and multiplayer checks run paired host
-and client processes on loopback. The repository runner provides isolation and
-consistent entry points.
-
-### Consolidated runner
-
-```sh
-scripts/run_validations.sh quick
-scripts/run_validations.sh full
-scripts/run_validations.sh host
-scripts/run_validations.sh network
-scripts/run_validations.sh all
-scripts/run_validations.sh --list
-```
-
-- `quick` runs deterministic content and domain checks.
-- `full` adds socket-free scene/runtime checks.
-- `host` runs single-process authority checks that bind local UDP.
-- `network` runs loopback host/client pairs.
-- `all` runs `full`, `host`, and `network`.
-
-Set `GODOT_BIN` to select an executable and `TEST_TIMEOUT_SECONDS` to change
-the per-process timeout. The runner isolates and removes XDG roots.
-
-For one focused test:
-
-```sh
-test_root="$(mktemp -d)"
-XDG_DATA_HOME="$test_root/data" \
-XDG_CONFIG_HOME="$test_root/config" \
-godot --headless --path . --script tests/fish_catalog_content_validation.gd
-rm -rf -- "$test_root"
-```
-
-Do not point `straywild_DATA_DIR` at an arbitrary empty directory; it is an
-explicit portable-data override and must identify a valid data root. The
-PortMaster launcher is the narrow exception: it also sets
-`straywild_CREATE_DATA_DIR=1`, allowing the game to initialize its known empty
-external save directory on first launch.
-
-Headless tests cannot prove visual alignment, mouse routing, shader appearance,
-controller feel, or resize behavior. Presentation changes require graphical
-review at the canonical 1280×720 layout and relevant low/high and ultrawide
-resolutions.
-
-Network tests require free loopback UDP ports and local-socket permission. A
-release audit additionally requires clean Git/tag verification, editor import,
-platform exports, executable smoke tests, archive inspection, and SHA-256
-verification; those checks are intentionally outside the development runner.
diff --git a/docs/PORTMASTER.md b/docs/PORTMASTER.md
deleted file mode 100644
index 803f72f..0000000
--- a/docs/PORTMASTER.md
+++ /dev/null
@@ -1,270 +0,0 @@
-# PortMaster reference
-
-straywild's PortMaster release is a canonical PortMaster package, not a
-version-named generic ZIP. The installer-facing archive must be named
-`straywild.zip`, and its root must contain exactly:
-
-```text
-straywild.sh
-straywild/
-```
-
-`port.json` belongs at `straywild/port.json` in the distributable archive.
-The upstream source-submission layout keeps it beside the launcher and port
-directory, but PortMaster's release builder relocates it into the declared
-port directory. Do not copy the source-submission layout directly into a
-release ZIP.
-
-HarbourMaster's installer also relocates recognized non-script files found at
-an archive root into the detected port directory. That compatibility behavior
-masked straywild's former root-level `port.json` during auto-install tests.
-Directly extracting the same archive into `/ports` did not perform the repair.
-Do not rely on installer relocation; build the distributable layout correctly.
-
-Do not derive a release package from an older local `portmaster-stage`
-directory. The templates in `scripts/portmaster/` are authoritative.
-
-## Release contract
-
-- `port.json` uses schema version 4, names `straywild.zip`, and marks the
- self-contained package ready to run.
-- `straywild.sh` does not contain a `# PORTMASTER:` signature in the source or
- distributable archive. HarbourMaster adds that installation marker when it
- installs the port.
-- The executable is `straywild/straywild.aarch64`.
-- The package declares AArch64, two analog sticks, GLIBC 2.28, and
- `weston_pkg_0.2.squashfs`.
-- The launcher starts GPTOKEYB with the explicit `straywild.gptk` no-op map.
- Every keyboard and mouse binding is disabled, leaving only GPTOKEYB's
- device-specific force-quit chord while Godot and straywild's controller
- mapping manager handle gameplay input. The explicit map also avoids the
- anomalous controller behavior observed on ROCKNIX without a passthrough map.
-- The launcher calls `pm_platform_helper` for the game executable after
- starting GPTOKEYB and calls `pm_finish` after the game exits.
-- straywild automatically creates player data for new installations under
- `ports/saves/straywild/` and does not present the in-game folder picker.
-- Existing installations with a selected data root keep that location when
- upgraded. Device-local data, configuration, and cache remain under
- `straywild/conf/data`, `straywild/conf/config`, and
- `straywild/conf/cache`.
-- A rebrand upgrade copies an existing `netfishing/conf/` tree into the new
- `straywild/conf/` tree when the latter does not exist. It also reuses an
- existing `ports/saves/netfishing/` data root rather than creating an empty
- replacement. New installations use straywild paths exclusively.
-- The archive must not contain `conf/`, saves, identities, logs, source files,
- `.git`, or `.godot` content.
-- Release downloads must publish only the canonical `straywild.zip` for this
- platform. Do not also publish a byte-identical versioned PortMaster ZIP;
- HarbourMaster identifies the port by its canonical archive name and the
- Forgejo release tag already supplies the version.
-
-## Build
-
-Run this only after the release commit and annotated `v` tag
-are pinned to the same clean `main` commit:
-
-```bash
-bash scripts/build_portmaster.sh
-```
-
-The script deletes and regenerates the Linux ARM64 export, stages the package,
-validates its structure, and writes:
-
-```text
-builds/v/straywild.zip
-```
-
-`--package-only` is reserved for repackaging an already validated ARM64 export.
-It does not rebuild game content.
-
-For a packaging-only correction to an already published game release, commit
-only PortMaster launcher, packaging-script, or PortMaster documentation changes
-after the immutable release tag, then run:
-
-```bash
-bash scripts/build_portmaster.sh --package-only --hotfix
-```
-
-The hotfix keeps the game executable and PCK tied to the release tag while
-recording the newer packaging commit separately in `BUILD-INFO.txt`. Replace
-the release's existing `straywild.zip`; never move the game release tag.
-
-## Local muOS installation
-
-The muOS auto-install directory is:
-
-```text
-/mnt/mmc/MUOS/PortMaster/autoinstall/
-```
-
-For an explicit offline installation test, copy `straywild.zip` to a temporary
-device directory and run:
-
-```bash
-/mnt/mmc/MUOS/PortMaster/harbourmaster \
- --offline --no-check install ./straywild.zip
-```
-
-Before upgrading an existing installation, preserve either the current
-`/mnt/mmc/ports/straywild/` paths or the former `/mnt/mmc/ports/netfishing/`
-paths on the device. After installation:
-
-1. Confirm the installed executable and PCK hashes match the staged release.
-2. Confirm HarbourMaster added its `# PORTMASTER:` installation signature to
- the installed launcher, then confirm the launcher starts GPTOKEYB with
- `straywild.gptk` and calls `pm_platform_helper` for the game executable.
-3. Confirm `conf/` and the external save directory were not replaced or
- removed.
-4. Launch the installed port through the normal muOS menu.
-5. Verify the displayed game version and controller face-button mapping.
-6. Verify GPTOKEYB's device-specific force-quit chord exits the game. This is
- Start+Select on most devices.
-
-Installing the public PortMaster catalog entry can still install an older
-catalog build until the upstream `straywild.zip` is updated. A local release
-test must install the generated local archive explicitly.
-
-## Template provenance
-
-The current catalog screenshot is a 640x480 PNG copied
-byte-for-byte into the package template:
-
-```text
-f24daae48f543b3a270a5aa46b5e73a31904bf0c7216d4c8a9e15a8dc48a5eed screenshot.png
-```
-
-The remaining catalog metadata assets were copied from the installed official
-`straywild.zip` package and are intentionally preserved byte-for-byte:
-
-```text
-e2a9d132744684c67865c01eca027a8c9946b4c5a19da57c82f1af0373dc83b7 gameinfo.xml
-```
-
-The package’s `licenses/` directory is assembled from the authoritative root
-licenses and trademarks, the consolidated `docs/ATTRIBUTION.md`, and the font
-notices at build time. It also records the exact release tag and corresponding
-GPL source commit. Do not restore stale duplicated notice templates beneath
-`scripts/portmaster/`.
-
-The original catalog porter credit, `Voyager`, remains in `port.json`.
-
-## muOS H700 controller mapping
-
-Godot 4.7.1 and the SDL2 utilities shipped by muOS identify the same virtual
-controller differently. The PortMaster SDL2 database uses GUID
-`19000000010000000100000000010000`, while Godot reports GUID
-`19004ca6010000000100000000010000`. Passing the SDL2 entry unchanged leaves
-most controls unmapped in Godot.
-
-The device-verified Godot mapping is:
-
-- A `b0`, B `b1`, Y `b2`, X `b3`
-- left bumper `b4`, right bumper `b5`
-- select `b6`, start `b7`, guide `b8`
-- left-stick click `b9`, left trigger `b10`, right trigger `b11`, right-stick click `b12`
-- D-pad `h0`; left and right sticks `a0..a3`
-
-`scripts/portmaster/straywild.sh` must replace the incompatible SDL2 entry
-with the Godot entry when that SDL2 GUID is selected. Do not append the two
-entries with a newline. WestonPack reconstructs the wrapped game command
-through a shell, so raw mapping values passed in that command can be split into
-unintended commands. Controller names may legally contain spaces, which caused
-this failure with the `GO-Super Gamepad` mapping. The outer Weston environment
-also does not reliably preserve the corrected muOS mapping for the game.
-
-Write the exact selected mapping to `conf/cache/controller_mapping.txt` before
-starting Weston. Weston must launch `launch-straywild.sh`, which reads that
-file, exports `SDL_GAMECONTROLLERCONFIG`, and immediately replaces itself with
-the game process. This applies the mapping at the same point as the working
-0.10.1 launcher without passing its raw text through WestonPack's command
-reconstruction. Do not move the mapping back to either side of
-`westonwrap.sh`.
-
-Always pass `straywild.gptk`, which explicitly unassigns every direct and
-hotkey binding with GPTOKEYB's `\"` value. This keeps GPTOKEYB from injecting
-keyboard or mouse events on top of Godot's native controller input and retains
-the device-specific kill chord. Supplying the explicit passthrough map also
-avoids the anomalous controller behavior observed on ROCKNIX when no map is
-provided. Do not add gameplay mappings to this file.
-
-Do not call `Input.add_joy_mapping(..., true)` for a recognized connected muOS
-controller. Updating this virtual controller after connection can stop Godot
-from delivering its standardized controller events until restart.
-
-Before shipping a PortMaster build on H700 hardware, verify all of the
-following in the installed game:
-
-- A, B, X, and Y each register independently.
-- The D-pad navigates every menu direction.
-- Both sticks, both bumpers, both triggers, Select, Start, L3, and R3 register.
-- A single button never cancels auto-map.
-- Holding both bumpers together for 1.25 seconds cancels auto-map.
-
-## PortMaster performance profile
-
-Every PortMaster launch defaults to the light performance profile, regardless
-of device family. This reflects the low-end hardware that makes up most of the
-PortMaster ecosystem and keeps the canonical package conservative by default.
-
-The light profile passes these Godot options:
-
-- `--single-window`
-- `--disable-vsync`
-- `--max-fps 30`
-- `--audio-output-latency 40`
-
-It also passes `straywild_PERFORMANCE_PROFILE=light` and
-`straywild_LOW_END=1`. The game renders the 3D world at 37.5% linear
-resolution with nearest-neighbor scaling, reducing 3D pixel work by about 86%
-while the separately rendered UI retains its canonical resolution. The light
-profile also:
-
-- disables the additional full-screen world-pixelation pass;
-- replaces animated depth-aware water shaders with opaque, per-vertex water;
-- disables ocean surface motion;
-- disables foliage wind;
-- keeps the title water and full-window menu patterns static;
-- replaces the animated cooler water and decorative bubbles with flat water;
-- disables decorative title-screen fish and bubbles;
-- retains title and dusk music while omitting rain and shoreline ambience;
-- reduces rain to 48 particles simulated at 8 FPS; and
-- replaces procedural sky clouds and 81 moving local cloud patches with one
- flat cloud ceiling.
-
-PortMaster exports are built in an isolated project copy with a dedicated
-micro-texture profile. Every raster texture is capped at 128 pixels on its
-longest side. High-volume gameplay catalogs, including fish, character parts,
-environment art, equipment, gathering art, and item icons, are capped at 64
-pixels. All remaining raster textures use ETC2 VRAM compression. Short sound
-effects, speech, and calls are imported as 22.05 kHz mono QOA samples. The
-authoritative artwork and the desktop, Android, and macOS imports are not
-modified. Source artwork, design documents, tests, platform-specific icons,
-and ambience are omitted from the temporary export project. The original
-compressed title and dusk music remain available in both runtime profiles.
-This build-time profile is always present in the PortMaster package; selecting
-the normal runtime profile restores visual effects, but does not restore
-omitted ambience or replace the micro textures with desktop-resolution assets.
-
-The normal profile otherwise retains the full visual presentation.
-
-To opt a capable device into the normal profile, create the persistent file
-`straywild/conf/performance_profile` containing exactly:
-
-```text
-normal
-```
-
-On a typical muOS installation, the full path is:
-
-```text
-/mnt/mmc/ports/straywild/conf/performance_profile
-```
-
-Set the file to `light`, or remove it, to restore the default. An externally
-provided `straywild_PERFORMANCE_PROFILE=normal` or `light` environment value
-takes precedence over the persistent file. Invalid values safely fall back to
-the light profile and are reported in `straywild/log.txt`.
-
-Do not use Godot's low processor mode for the light profile. It reduces idle
-CPU usage by sleeping between updates and is not a game-performance
-optimization.
diff --git a/docs/README-PLAYTEST.txt b/docs/README-PLAYTEST.txt
deleted file mode 100644
index f187182..0000000
--- a/docs/README-PLAYTEST.txt
+++ /dev/null
@@ -1,124 +0,0 @@
-straywild
-v0.20.2-alpha
-Alpha 0.20.2
-
-Thank you for supporting this private testing release.
-
-straywild is an early multiplayer fishing game. This alpha includes local
-progression, player-hosted and dedicated multiplayer sessions, public room
-discovery, and direct UDP connections. Expect rough edges and please keep a
-backup of any save or identity data you care about.
-
-INSTALLATION
-
-1. Extract the entire ZIP into a new folder.
-2. Keep the executable and straywild.pck together.
-3. Launch straywild.exe on Windows or straywild on Linux.
-
-Windows may show a warning for an unsigned application. This private build is
-not code signed. Only run a copy received from the playtest organizer.
-
-Linux users may need to allow execution:
-
- chmod +x straywild
-
-CONTROLS
-
-Move W / A / S / D or left stick
-Jump Space
-Sprint Shift
-Sneak Ctrl or click right stick
-Slow walk Alt
-Rotate camera Hold right mouse, or use right stick
-Cycle active hotbar slot Mouse wheel or D-pad left/right
-Select hotbar slot 1 through 9
-Zoom camera Shift + mouse wheel
- (swap wheel controls in Settings > Controls)
-Primary tool / fishing Left mouse or right trigger
-Inventory and player pages Tab
-Game Menu / back Escape
-Interact E
-Chat T
-
-Keyboard and controller bindings can be reviewed and changed in Settings.
-
-FISHING
-
-- Hold left mouse to select cast distance, then release to cast.
-- The target preview shows whether the selected landing point is fishable.
-- While waiting, hold left mouse to withdraw the bobber.
-- A bite starts the chase automatically.
-- Hold left mouse to advance the green catch meter.
-- At barriers, use distinct left-click presses to damage the barrier.
-- Do not let the red chase meter catch the green meter.
-- Accessibility auto-click can be enabled in Settings.
-- Approach the Fishing Shop and interact to open it.
-- Move sellable inventory entries into the shop's sell tray, review the total,
- and confirm the sale.
-- Buy supplies, equipment, and persistent upgrades from their shop tabs.
-- Move world-use items into the hotbar and select their slot to use or hold
- them.
-- Coffee improves movement; other supplies temporarily improve fishing.
-- Inventory and storage capacity can be expanded permanently at the shop.
-- Reel Speed upgrades increase authoritative green reeling progress.
-- Rod Power upgrades increase damage per valid barrier action.
-- Shop upgrades persist in the local progression save.
-- Escape closes shop confirmations before closing the shop itself.
-
-FEATURES TO TRY
-
-- Hosting a private or public room and joining compatible online sessions
-- Direct-IP, saved-server, recent-server, and public-room connection flows
-- Text chat, player calls, player lists, relationships, and moderation tools
-- Kat, Kim, and robot speech sample sets
-- Movement, jumping, camera rotation, and zoom
-- Short, medium, and maximum-distance casts, including far ocean casts
-- Valid water and invalid land casts
-- Manual withdrawal before a bite
-- Barrier-and-chase catching
-- Accessibility auto-click settings
-- Catch showcase and fish size variation
-- General inventory, separate tackle, hotbar storage, player storage, and the
- Logbook
-- Fishing, net gathering, and beach digging
-- Equipment and consumable use from the hotbar
-- Fishing Shop sell tray, wallet updates, and persistent upgrades
-- Portable RV homes, multiplayer visits, RV storage, and the early decor
- purchasing and placement prototype
-- Seeded generated worlds and the authored starter-island layout
-- Generated beaches, ponds, lakes, complete rivers, denser forests, varied
- elevation, and closed coastal cliffs
-- Collaborative Art Kit drawing, finalized artwork, PNG export, and stamps
-- Save, Continue, New Game, and Delete Save
-- Game Menu and persistent camera settings
-- Water-entry recovery from several shores
-
-SAVES AND SETTINGS
-
-Progression and settings are stored outside the extracted game folder. The
-Data & Identity page shows the active player-data location. Delete Save removes
-progression but preserves device settings.
-
-This is an early save format. Keep expectations modest and report any failure
-to Continue, save, sell, move, favorite, or retain an inventory entry.
-
-FEEDBACK
-
-Please include:
-
-- Your operating system and whether you used mouse/keyboard or controller
-- What you were doing when a problem occurred
-- Whether the issue repeated after restarting
-- Screenshots or a short recording when useful
-- How movement, casting, chase pressure, barrier clicking, menus, and camera
- controls felt
-
-KNOWN ALPHA LIMITATIONS
-
-- Public sessions remain direct connections; restrictive NAT or carrier-grade
- networks may prevent some players from joining.
-- Multiplayer, dedicated-server deployment, UI, and balance are still alpha.
-- Some fish and character artwork remains temporary.
-- RV exteriors and decor use temporary prototype geometry and presentation.
-- There is no installer or automatic updater.
-- Windows builds are unsigned.
diff --git a/docs/README.md b/docs/README.md
deleted file mode 100644
index 444ebc7..0000000
--- a/docs/README.md
+++ /dev/null
@@ -1,75 +0,0 @@
-# straywild documentation
-
-This directory is the single home for maintained project documentation.
-Start here instead of searching feature, asset, world, or script directories
-for local README files.
-
-## Authoritative references
-
-- [`DEVELOPMENT.md`](DEVELOPMENT.md): architecture, engineering decisions,
- content and balance policy, authoring guidance, and validation.
-- [`ATTRIBUTION.md`](ATTRIBUTION.md): ownership, credits, licensing map,
- third-party notices, asset provenance, and redistribution requirements.
-- [`PORTMASTER.md`](PORTMASTER.md): PortMaster packaging, installation,
- controller mapping, performance behavior, and hardware release checks.
-- [`README-PLAYTEST.txt`](README-PLAYTEST.txt): the player-facing README staged
- into desktop playtest packages.
-
-## Related repositories
-
-- [Discovery service API](https://forge.makearmy.io/woofmeow/straywild-discovery-server/src/branch/main/docs/API.md)
- stays with the service implementation and defines its versioned HTTP
- contract.
-- [Dedicated-server packaging](https://forge.makearmy.io/woofmeow/straywild-dedicated-server/src/branch/main/README.md)
- stays with its installer and update scripts. It consumes this repository's
- attribution and legal notices from the exact pinned game commit instead of
- maintaining copies.
-
-Those code-coupled references remain in their own repositories. Game-wide
-architecture, authoring, licensing, credits, and provenance are not duplicated
-there.
-
-Exact runtime values and behavior remain authoritative in source code, scenes,
-and resources. These documents describe boundaries, policy, and maintenance
-procedures; they should not duplicate changing constants or inventories that
-can be derived reliably from the project.
-
-The same rule applies to credits. `ATTRIBUTION.md` records creators, licenses,
-and durable contribution families. It does not attempt to mirror every current
-asset filename or private intake-directory layout. Planned or unincorporated
-work is not described as shipped, and repository paths are included only when
-they help recipients locate a distributed third-party work or license.
-
-## Files intentionally kept at the repository root
-
-The following are entry points or governing legal documents rather than
-general project documentation, so they remain at conventional root paths:
-
-- `README.md`
-- `CONTRIBUTING.md`
-- `LICENSE`
-- `ASSET-LICENSE.md`
-- `TRADEMARKS.md`
-- `CONTRIBUTOR-TERMS.md`
-
-`CONTRIBUTING.md` records that unsolicited contributions are not accepted.
-`CONTRIBUTOR-TERMS.md` remains a governing legal record for previously
-accepted and expressly invited work, not an invitation for new submissions.
-
-Third-party font license texts stay beside their font files under `ui/fonts/`
-so the licensed files and their original notices cannot drift apart. Build
-scripts collect those legal files and the authoritative attribution record
-when assembling releases.
-
-## Maintenance rule
-
-Do not add feature-specific README files elsewhere in the repository. Add or
-update the appropriate reference above and link to its heading when source
-comments need more context. Player-facing package instructions belong only in
-`README-PLAYTEST.txt`; source and scripts remain authoritative for command-line
-usage.
-
-Before adding a literal project path to prose, verify that it exists in the
-same revision and that readers actually need the path. Prefer a stable owner,
-interface, or resource type over a directory listing. A path or Git author is
-never evidence of creative authorship.
diff --git a/docs/design/.gdignore b/docs/design/.gdignore
deleted file mode 100644
index 8b13789..0000000
--- a/docs/design/.gdignore
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/docs/design/fish_catalog_proposal.csv b/docs/design/fish_catalog_proposal.csv
deleted file mode 100644
index c427dc0..0000000
--- a/docs/design/fish_catalog_proposal.csv
+++ /dev/null
@@ -1,314 +0,0 @@
-catalog_number,display_name,collection_group,water_type,spawn_area,seasons,day_rule,rain_rule,fog_rule,rarity,base_catch_weight,weight_min_lb,weight_max_lb,sell_value_min,sell_value_max,sell_value_curve,required_bait_tags,required_event_tags,forbidden_event_tags,logbook_fact,active_status
-101,bowfin,Ancient freshwater fish,Fresh Water,pond,Spring | Summer | Fall,Night,Any,Required,Rare,0.28,1,20,13,30,0.85,,,,bowfin can gulp air at the surface when warm backwaters run low on oxygen. a long dorsal fin ripples as they stalk prey.,inactive
-102,lake sturgeon,Ancient freshwater fish,Fresh Water,lake,Spring | Summer | Fall | Winter,Night,Any,Any,Epic,0.09,10,180,36,82,0.8,,,,lake sturgeon sweep the bottom with four sensitive barbels before extending a tube-like mouth to feed. they can live for decades.,inactive
-103,longnose gar,Ancient freshwater fish,Fresh Water,river,Spring | Summer | Fall,Day,Any,Any,Rare,0.24,2,35,14,34,0.85,,,,longnose gar wait near the surface before snapping sideways at passing fish. their narrow jaws carry rows of fine needle-like teeth.,inactive
-104,paddlefish,Ancient freshwater fish,Fresh Water,river,Spring | Summer,Day,Required,Any,Epic,0.1,10,120,34,76,0.82,,,,"paddlefish cruise open rivers with their mouths wide, filtering tiny plankton from the water. the broad snout senses faint electrical signals.",inactive
-105,shovelnose sturgeon,Ancient freshwater fish,Fresh Water,river,Spring | Summer | Fall | Winter,Night,Any,Any,Rare,0.22,1,8,12,25,0.9,,,,shovelnose sturgeon hug flowing river bottoms and probe for insect larvae. a flattened snout helps them hold position in strong current.,inactive
-106,spotted gar,Ancient freshwater fish,Fresh Water,pond,Spring | Summer | Fall,Day,Any,Required,Rare,0.2,1,15,13,29,0.88,,,,spotted gar linger among vegetation where their mottled bodies break up their outline. they rise to gulp air as well as breathing through gills.,inactive
-107,West African lungfish,Ancient freshwater fish,Fresh Water,pond,Summer | Fall,Night,Required,Any,Rare,0.3,1,10,12,28,0.86,,,,"west african lungfish breathe air with paired lungs. when floodplains dry, they seal themselves in a mud cocoon until water returns.",inactive
-201,atlantic mudskipper,Brackish oddities,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Required,Any,Uncommon,1,0.03,0.5,5,11,0.95,,,,atlantic mudskippers use sturdy fins to hop and crawl across exposed mud. moist skin and mouth linings let them breathe out of water.,inactive
-202,largescale foureyes,Brackish oddities,Fresh Water | Salt Water,river,Spring | Summer | Fall | Winter,Day,Any,Any,Rare,0.32,0.05,1,10,23,0.95,,,,largescale foureyes cruise with each eye divided across the waterline. one half watches above while the other scans below.,inactive
-301,red mullet,Coastal bottom fish,Salt Water,ocean,Spring | Summer | Fall,Day,Any,Any,Uncommon,0.9,0.1,3,5,11,0.92,,,,red mullet comb sandy bottoms with two chin barbels that taste hidden prey. their foraging leaves winding trails in the sediment.,inactive
-401,European plaice,Coastal flatfish,Salt Water,ocean,Spring | Fall | Winter,Day,Any,Any,Common,1.8,0.2,10,4,10,0.9,,,,european plaice lie flat on the seafloor with both eyes on the same side. orange spots help distinguish them from neighboring flatfish.,inactive
-402,summer flounder,Coastal flatfish,Salt Water,ocean,Spring | Summer | Fall,Day,Any,Any,Uncommon,0.9,0.3,15,6,16,0.88,,,,summer flounder bury themselves in sand with only their eyes exposed. a sudden burst carries them upward to seize passing prey.,inactive
-403,winter flounder,Coastal flatfish,Salt Water,ocean,Spring | Fall | Winter,Day,Forbidden,Any,Common,1.8,0.2,8,4,9,0.9,,,,winter flounder favor cool bays and estuaries where they rest on muddy bottoms. their eyes sit on the right side of the body.,inactive
-501,dugong,Coastal marine mammals,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Legendary,0.011,500,1100,72,165,0.77,,,,"dugongs graze seagrass meadows with a downturned, bristled snout. they are the only fully marine members of the sirenian family.",inactive
-502,harbor seal,Coastal marine mammals,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Required,Legendary,0.018,120,370,60,140,0.79,,,,harbor seals rest on rocky shores and sandbars between hunting trips. sensitive whiskers can trace the water trails left by moving fish.,inactive
-503,sea otter,Coastal marine mammals,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Legendary,0.02,30,100,55,125,0.84,,,,sea otters float on their backs while opening shellfish with stones. dense fur traps insulating air because they lack a thick blubber layer.,inactive
-504,walrus,Coastal marine mammals,Salt Water,ocean,Spring | Winter,Day,Any,Required,Legendary,0.012,800,3700,74,170,0.76,,,,walruses sweep the seafloor for clams and other soft-bodied prey. long tusks help them haul onto ice and settle disputes.,inactive
-505,West Indian manatee,Coastal marine mammals,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Legendary,0.012,440,1300,70,160,0.77,,,,west indian manatees graze aquatic plants in warm coastal water and rivers. they surface regularly to breathe through nostrils atop the snout.,inactive
-601,bay pipefish,Coastal oddities,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Uncommon,0.9,0.005,0.04,4,9,1,,,,"bay pipefish hide upright among seagrass, matching the blades with their slender bodies. males carry developing eggs in a brood pouch.",inactive
-602,lined seahorse,Coastal oddities,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Forbidden,Any,Rare,0.28,0.005,0.05,9,20,1,,,,lined seahorses anchor to grass and coral with curling tails. the male broods eggs in a pouch and releases fully formed young.,inactive
-603,monkfish,Coastal oddities,Salt Water,ocean,Spring | Fall | Winter,Night,Any,Required,Rare,0.25,2,55,14,35,0.84,,,,monkfish hide against the bottom and wave a fleshy lure above a cavernous mouth. a sudden gulp draws fish and crustaceans inward.,inactive
-604,tripletail,Coastal oddities,Salt Water,ocean,Summer | Fall,Day,Any,Required,Rare,0.28,1,30,13,31,0.85,,,,"tripletail often drift sideways near floating debris, resembling a loose leaf. rounded rear fins create the three-tailed silhouette.",inactive
-701,atlantic mackerel,Coastal pelagic fish,Salt Water,ocean,Spring | Summer | Fall,Any,Any,Any,Common,0.5,0.5,5,4,9,0.9,,,,atlantic mackerel travel in fast schools across the north atlantic and feed on plankton and small schooling fish.,active
-702,bluefish,Coastal pelagic fish,Salt Water,ocean,Spring | Summer | Fall,Day,Any,Any,Uncommon,0.85,0.5,25,7,19,0.85,,,,bluefish race in coordinated schools while pursuing smaller fish. sharp teeth and powerful jaws make their feeding bursts especially fierce.,inactive
-703,cero mackerel,Coastal pelagic fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Uncommon,0.35,1,15,7,15,0.9,,,,cero mackerel are streamlined coastal hunters. sharp teeth help them slash through schools of smaller fish.,active
-704,chub mackerel,Coastal pelagic fish,Salt Water,ocean,Spring | Summer | Fall,Any,Any,Any,Common,0.5,0.5,4,4,9,0.9,,,,chub mackerel school near the surface and follow plankton blooms. their dark wavy bars help identify them at a glance.,active
-705,crevalle jack,Coastal pelagic fish,Salt Water,ocean,Summer | Fall,Day,Required,Any,Uncommon,0.8,1,35,7,20,0.84,,,,"crevalle jacks patrol warm coasts in fast-moving schools. their deep bodies and forked tails deliver long, powerful runs.",inactive
-706,king mackerel,Coastal pelagic fish,Salt Water,ocean,Summer | Fall,Any,Any,Any,Uncommon,0.3,3,40,10,24,0.9,,,,king mackerel are swift coastal predators. their pointed teeth and long body are built for sudden bursts of speed.,active
-707,spanish mackerel,Coastal pelagic fish,Salt Water,ocean,Summer | Fall,Any,Any,Any,Uncommon,0.35,1,15,8,18,0.9,,,,spanish mackerel hunt in warm coastal waters and often travel in loose schools while chasing baitfish.,active
-708,talang queenfish,Coastal pelagic fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Uncommon,0.8,0.5,20,7,18,0.86,,,,talang queenfish chase schooling prey near warm coasts and estuaries. hooked teeth and a deeply forked tail suit their rapid attacks.,inactive
-801,American sand lance,Coastal schooling fish,Salt Water,ocean,Spring | Fall | Winter,Day,Any,Any,Common,3,0.02,0.4,1,3,1,,,,american sand lance dive headfirst into loose sand when danger approaches. immense schools carry energy from plankton to larger predators.,inactive
-802,atlantic menhaden,Coastal schooling fish,Salt Water,ocean,Spring | Summer | Fall,Day,Any,Any,Common,3,0.1,1.5,2,5,0.98,,,,"atlantic menhaden strain plankton from coastal water with fine gill rakers. dense schools feed whales, seabirds, and predatory fish.",inactive
-803,black pomfret,Coastal schooling fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Uncommon,0.3,1,15,8,18,0.9,,,,"black pomfret have a deep, laterally compressed body that lets them maneuver neatly through open water.",active
-804,chinese pomfret,Coastal schooling fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Uncommon,0.3,1,20,9,20,0.9,,,,"chinese pomfret are silvery schooling fish with a tall, flattened body and long sickle-shaped fins.",active
-805,european anchovy,Coastal schooling fish,Salt Water,ocean,Spring | Summer | Fall,Any,Any,Any,Common,0.5,0.05,0.25,2,4,0.9,,,,european anchovies form dense schools and feed by filtering tiny plankton from coastal water.,active
-806,European pilchard,Coastal schooling fish,Salt Water,ocean,Spring | Fall | Winter,Any,Any,Any,Common,3.5,0.05,0.6,2,4,1,,,,european pilchards form shimmering schools along atlantic and mediterranean coasts. they feed mainly on plankton near the surface.,inactive
-807,golden pomfret,Coastal schooling fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Rare,0.18,2,25,14,30,0.9,,,,golden pomfret are warm-water swimmers whose golden sheen is most noticeable along the fins and flanks.,active
-808,northern anchovy,Coastal schooling fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Common,0.5,0.05,0.35,2,4,0.9,,,,northern anchovies gather in huge schools along the pacific coast and filter plankton with their fine gill rakers.,active
-809,Pacific sardine,Coastal schooling fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Common,3.5,0.05,0.5,2,4,1,,,,"pacific sardines follow cool, productive water in enormous schools. their movements shift as currents and plankton supplies change.",inactive
-810,white pomfret,Coastal schooling fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Common,0.45,0.5,10,5,12,0.9,,,,"white pomfret gather over coastal grounds and use their compact, deep bodies to turn quickly while feeding.",active
-901,blacktip shark,Coastal sharks,Salt Water,ocean,Summer | Fall,Day,Required,Any,Epic,0.1,15,150,32,75,0.8,,,,blacktip sharks are named for the dark markings on their fins. they hunt schooling fish in warm shallows and sometimes leap clear of the water.,inactive
-902,great hammerhead shark,Coastal sharks,Salt Water,ocean,Summer | Fall,Day,Any,Forbidden,Legendary,0.035,50,1000,62,145,0.78,,,,great hammerheads sweep their wide heads over the bottom while searching for rays. widely spaced senses help pinpoint hidden prey.,inactive
-903,great white shark,Coastal sharks,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Legendary,0.025,100,1000,70,150,0.76,,,,great white sharks conserve heat in their swimming muscles while crossing cool seas. rows of serrated teeth are replaced throughout life.,inactive
-904,tiger shark,Coastal sharks,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Required,Any,Epic,0.06,50,1000,50,120,0.8,,,,tiger sharks carry dark vertical bars that fade as they mature. broad jaws let them investigate an unusually varied range of prey.,inactive
-905,whale shark,Coastal sharks,Salt Water,ocean,Summer | Fall,Day,Any,Forbidden,Legendary,0.025,500,45000,70,160,0.76,,,,whale sharks are enormous filter feeders covered in unique spot patterns. they cruise with open mouths to collect plankton and tiny fish.,inactive
-1001,burbot,Cold freshwater fish,Fresh Water,lake,Spring | Fall | Winter,Night,Any,Required,Rare,0.24,0.5,18,12,28,0.86,,,,burbot are the only freshwater members of the cod family. they become most active in cold darkness and may spawn beneath winter ice.,inactive
-1002,cisco,Cold freshwater fish,Fresh Water,lake,Spring | Fall | Winter,Day,Any,Any,Common,2.2,0.1,3,3,7,0.95,,,,cisco school in the cool open water of northern lakes. these slim relatives of salmon feed on plankton and become prey for larger fish.,inactive
-1003,lake whitefish,Cold freshwater fish,Fresh Water,lake,Spring | Fall | Winter,Any,Any,Any,Common,1.5,0.5,12,4,11,0.9,,,,"lake whitefish browse deep, cold bottoms with small downward-pointing mouths. large schools move toward shallows to spawn in autumn.",inactive
-1101,arctic char,Cold freshwater salmonids,Fresh Water | Salt Water,lake,Spring | Fall | Winter,Any,Forbidden,Any,Rare,0.3,1,25,14,32,0.86,,,,arctic char thrive farther north than any other freshwater fish. some remain in lakes while others migrate between rivers and the sea.,inactive
-1102,arctic grayling,Cold freshwater salmonids,Fresh Water,river,Spring | Fall | Winter,Day,Any,Forbidden,Uncommon,0.75,0.3,6,6,14,0.9,,,,"arctic grayling carry a tall, colorful dorsal fin like a sail. they feed on drifting insects in cold, clear rivers and lakes.",inactive
-1103,brown trout,Cold freshwater salmonids,Fresh Water,river,Spring | Summer | Fall | Winter,Night,Any,Any,Uncommon,0.8,0.5,25,7,20,0.86,,,,"brown trout often shelter by day and hunt more freely after dark. adaptable populations occupy streams, lakes, and even coastal water.",inactive
-1104,cutthroat trout,Cold freshwater salmonids,Fresh Water,river,Spring | Fall | Winter,Any,Any,Any,Uncommon,0.4,0.25,10,5,12,0.9,,,,"cutthroat trout take their name from the red slash beneath the jaw. many populations rely on cold, well-oxygenated streams.",active
-1105,golden trout,Cold freshwater salmonids,Fresh Water,lake,Spring | Summer | Fall,Any,Any,Any,Rare,0.2,0.2,3,8,16,0.9,,,,"golden trout evolved in clear, high-elevation california streams. their bright flanks stand out against rocky alpine water.",active
-1106,rainbow trout,Cold freshwater salmonids,Fresh Water,river,Spring | Summer | Fall | Winter,Any,Any,Any,Uncommon,0.45,0.5,20,5,11,0.9,,,,rainbow trout are adaptable stream hunters. the pink lateral stripe is especially vivid when the fish is in breeding condition.,active
-1201,atlantic wolffish,Cold reef fish,Salt Water,ocean,Spring | Fall | Winter,Any,Forbidden,Any,Rare,0.25,2,40,14,34,0.84,,,,atlantic wolffish use heavy jaws and blunt teeth to crush shellfish and urchins. natural antifreeze proteins help them endure icy water.,inactive
-1202,cusk,Cold reef fish,Salt Water,ocean,Spring | Fall | Winter,Night,Any,Any,Uncommon,0.75,0.5,30,7,19,0.85,,,,"cusk move slowly across cold rocky bottoms, sheltering in cracks and caves. a single long fin runs around much of the rear body.",inactive
-1203,spotted wolffish,Cold reef fish,Salt Water,ocean,Spring | Fall | Winter,Any,Any,Required,Epic,0.11,3,50,26,60,0.84,,,,spotted wolffish crush hard-shelled prey with broad molar-like teeth. their pale spots stand out against a dark cold-water body.,inactive
-1301,atlantic herring,Cold schooling fish,Salt Water,ocean,Spring | Fall | Winter,Any,Any,Any,Common,3,0.1,1.5,2,5,0.98,,,,atlantic herring gather in immense schools that move between feeding and spawning grounds. they filter plankton with fine gill rakers.,inactive
-1302,capelin,Cold schooling fish,Salt Water,ocean,Spring | Fall | Winter,Any,Any,Required,Uncommon,1,0.03,0.2,4,8,1,,,,"capelin are small polar fish that feed whales, seabirds, and larger fish. many schools roll onto beaches or shallows to release their eggs.",inactive
-1303,European sprat,Cold schooling fish,Salt Water,ocean,Spring | Fall | Winter,Any,Any,Any,Common,3.2,0.03,0.3,1,3,1,,,,european sprat form compact schools in northern coastal seas. these tiny herring relatives feed on plankton near the surface.,inactive
-1304,pacific herring,Cold schooling fish,Salt Water,ocean,Spring | Fall | Winter,Any,Required,Any,Common,2.8,0.1,1.5,2,5,0.98,,,,pacific herring return to coastal shallows to coat plants and rocks with eggs. their oily schools support entire marine food webs.,inactive
-1401,Alaska pollock,Cold shelf fish,Salt Water,ocean,Spring | Fall | Winter,Any,Any,Any,Common,2.5,0.3,8,3,8,0.92,,,,alaska pollock form vast schools over cold northern shelves. they feed on plankton and fish while supporting many larger predators.,inactive
-1402,atlantic cod,Cold shelf fish,Salt Water,ocean,Spring | Fall | Winter,Any,Any,Any,Common,1.8,1,55,5,18,0.85,,,,"atlantic cod carry a pale lateral line and one chin barbel. they search cold seafloors for crustaceans, mollusks, and smaller fish.",inactive
-1403,Atlantic pollock,Cold shelf fish,Salt Water,ocean,Spring | Fall | Winter,Day,Any,Forbidden,Uncommon,0.9,1,35,7,18,0.86,,,,atlantic pollock are swift relatives of cod with dark backs and pale lateral lines. adults gather around rocky banks and wrecks.,inactive
-1404,Dover sole,Cold shelf fish,Salt Water,ocean,Spring | Fall | Winter,Night,Any,Any,Uncommon,0.8,0.2,8,6,14,0.9,,,,dover sole glide over deep muddy bottoms of the eastern pacific. both eyes migrate to the right side as the young fish becomes flat.,inactive
-1405,haddock,Cold shelf fish,Salt Water,ocean,Spring | Fall | Winter,Day,Any,Any,Common,2,0.5,20,4,11,0.9,,,,haddock are recognized by a dark shoulder mark and black lateral line. schools forage over cold seafloors for small bottom animals.,inactive
-1406,pacific cod,Cold shelf fish,Salt Water,ocean,Spring | Fall | Winter,Any,Any,Any,Common,1.8,1,45,5,17,0.85,,,,pacific cod use a chin barbel to investigate cold shelf bottoms. seasonal schools follow prey between deeper water and coastal spawning grounds.,inactive
-1407,pacific hake,Cold shelf fish,Salt Water,ocean,Spring | Fall | Winter,Night,Any,Any,Common,1.7,0.3,8,3,9,0.92,,,,pacific hake rise from deep water at night to feed on krill and fish. large schools migrate along the west coast with changing seasons.,inactive
-1408,silver hake,Cold shelf fish,Salt Water,ocean,Spring | Fall | Winter,Night,Any,Any,Common,1.7,0.2,5,3,8,0.92,,,,silver hake hunt fish and crustaceans over sandy atlantic shelves. they move upward after dark and return toward the bottom by day.,inactive
-1501,chambered nautilus,Cuttlefish and nautiluses,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Required,Legendary,0.05,0.5,3,34,76,0.92,,,,chambered nautiluses regulate buoyancy with gas-filled shell chambers. dozens of small tentacles feel for food in dark reef slopes.,inactive
-1502,common cuttlefish,Cuttlefish and nautiluses,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Uncommon,0.55,0.2,12,7,17,0.9,,,,common cuttlefish flash changing colors and patterns through specialized skin cells. an internal cuttlebone controls buoyancy.,inactive
-1503,flamboyant cuttlefish,Cuttlefish and nautiluses,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Required,Any,Epic,0.1,0.05,0.4,26,58,0.96,,,,flamboyant cuttlefish walk along the seafloor on their arms and fins. vivid displays warn predators of potent toxins.,inactive
-1601,blueline tilefish,Deep shelf fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Uncommon,0.7,1,20,7,18,0.86,,,,blueline tilefish shelter in burrows and rocky openings along deep shelves. a narrow blue stripe traces the edge of the dorsal fin.,inactive
-1602,golden tilefish,Deep shelf fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Rare,0.22,2,50,15,37,0.84,,,,golden tilefish excavate burrows in soft clay along deep ocean slopes. their bright yellow spots contrast with blue-green backs.,inactive
-1603,Pacific ocean perch,Deep shelf fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Uncommon,0.8,0.3,12,7,17,0.88,,,,pacific ocean perch hover around deep rocky reefs in the north pacific. these slow-growing fish give birth to live young.,inactive
-1604,Patagonian toothfish,Deep shelf fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Any,Epic,0.09,10,220,32,78,0.8,,,,"patagonian toothfish patrol cold, deep southern water with large canine teeth. natural antifreeze proteins protect their blood from freezing.",inactive
-1605,sablefish,Deep shelf fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Any,Rare,0.22,2,40,15,36,0.84,,,,"sablefish roam deep north pacific slopes and can live for many decades. soft dark bodies conceal rich, oil-filled flesh.",inactive
-1606,wreckfish,Deep shelf fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Required,Epic,0.1,10,180,31,76,0.8,,,,"wreckfish gather around caves, ledges, and shipwrecks in deep water. powerful jaws let them ambush fish and large crustaceans.",inactive
-1701,giant oarfish,Deep-sea legends,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Required,Legendary,0.035,20,600,60,140,0.78,,,,"giant oarfish drift vertically through deep open water, rowing with a crimson dorsal fin. their ribbon-like bodies can reach remarkable lengths.",inactive
-1702,West Indian Ocean coelacanth,Deep-sea legends,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Required,Legendary,0.03,20,200,65,145,0.8,,,,"west indian ocean coelacanths shelter in deep volcanic caves by day. fleshy lobed fins move in an alternating, limb-like rhythm.",inactive
-1801,black seadevil,Deep-sea oddities,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Any,Legendary,0.05,0.1,2,48,105,0.92,,,,black seadevils lure prey with a glowing organ suspended above the mouth. tiny males permanently attach to much larger females.,inactive
-1802,common fangtooth,Deep-sea oddities,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Any,Epic,0.1,0.05,0.5,25,56,0.96,,,,common fangtooths carry outsized teeth that fit into pockets in the roof of the mouth. they migrate upward from deep water after dark.,inactive
-1803,gulper eel,Deep-sea oddities,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Required,Legendary,0.04,0.5,18,44,92,0.86,,,,gulper eels unfold enormous mouths to engulf prey in the deep sea. a whip-like tail ends in a small light-producing organ.,inactive
-1804,Pacific barreleye,Deep-sea oddities,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Required,Legendary,0.045,0.2,2.5,42,88,0.9,,,,pacific barreleyes peer upward through transparent shields covering the head. tubular eyes rotate forward when the fish turns to feed.,inactive
-1805,smooth-head blobfish,Deep-sea oddities,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Required,Legendary,0.04,0.5,5,42,90,0.86,,,,smooth-head blobfish rest on deep slopes where pressure supports their soft bodies. they conserve energy by waiting for edible matter nearby.,inactive
-1806,spotted lanternfish,Deep-sea oddities,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Any,Rare,0.24,0.05,0.5,14,30,0.95,,,,spotted lanternfish carry rows of light-producing organs along their bodies. immense schools rise toward the surface each night.,inactive
-1901,crown-of-thorns sea star,Echinoderms,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Required,Any,Epic,0.1,0.5,7,23,52,0.91,,,,crown-of-thorns sea stars crawl over reefs and digest coral tissue outside the body. venomous spines discourage many predators.,inactive
-1902,giant California sea cucumber,Echinoderms,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Required,Rare,0.24,0.5,5,13,29,0.93,,,,giant california sea cucumbers sweep organic particles from the seafloor with branching tentacles. soft tube feet anchor their long bodies.,inactive
-1903,purple sea urchin,Echinoderms,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Uncommon,0.75,0.05,1,5,12,0.96,,,,purple sea urchins scrape algae from rocks with a five-toothed feeding structure. dense groups can clear kelp from entire reefs.,inactive
-1904,sand dollar,Echinoderms,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Uncommon,0.8,0.01,0.2,4,10,0.98,,,,sand dollars burrow just beneath sandy seafloors and filter tiny food particles. the familiar star pattern marks their five-part body plan.,inactive
-1905,sunflower sea star,Echinoderms,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Required,Rare,0.25,0.5,13,14,32,0.89,,,,"sunflower sea stars move quickly on thousands of tube feet and may carry many arms. they hunt urchins, clams, and other bottom animals.",inactive
-2001,axolotl,Freshwater amphibians,Fresh Water,lake,Spring | Summer | Fall,Night,Any,Required,Legendary,0.04,0.1,0.7,36,78,0.96,,,,axolotls remain aquatic and keep feathery external gills throughout adulthood. lost limbs and parts of several organs can regenerate.,inactive
-2002,hellbender,Freshwater amphibians,Fresh Water,river,Spring | Summer | Fall,Night,Required,Any,Epic,0.1,0.5,5.5,24,54,0.91,,,,"hellbenders hide beneath large rocks in cool, clean rivers. folds of loose skin absorb oxygen while the flattened body grips the current.",inactive
-2101,guppy,Freshwater aquarium fish,Fresh Water,pond,Spring | Summer | Fall | Winter,Day,Any,Any,Common,1.5,0.01,0.15,2,5,0.98,,,,"guppies are livebearers whose young arrive ready to swim. males display bright colors while females often favor larger, less flashy bodies.",inactive
-2102,neon tetra,Freshwater aquarium fish,Fresh Water,pond,Spring | Summer | Fall | Winter,Night,Any,Any,Uncommon,0.9,0.01,0.08,4,8,0.98,,,,"neon tetras school through shaded forest streams. an iridescent blue stripe remains visible in the dim, tannin-stained water.",inactive
-2103,zebrafish,Freshwater aquarium fish,Fresh Water,pond,Spring | Summer | Fall | Winter,Day,Any,Any,Common,1.35,0.01,0.12,2,5,0.98,,,,"zebrafish carry horizontal blue stripes from gill to tail. hardy schools thrive in shallow streams, flooded fields, and quiet pools.",inactive
-2201,largemouth bass,Freshwater bass,Fresh Water,pond,Spring | Summer | Fall,Any,Any,Any,Common,2.5,0.5,18,4,12,0.9,,,,largemouth bass wait among weeds and timber before lunging at prey. the upper jaw extends beyond the eye when the mouth is closed.,inactive
-2202,smallmouth bass,Freshwater bass,Fresh Water,river,Spring | Summer | Fall,Day,Any,Any,Uncommon,1.2,0.5,12,6,15,0.9,,,,smallmouth bass favor clear water with rock and moving current. bronze flanks and dark vertical bars distinguish them from largemouth bass.,inactive
-2301,bigmouth buffalo,Freshwater bottom fish,Fresh Water,river,Spring | Summer | Fall,Any,Any,Required,Epic,0.12,8,80,28,63,0.82,,,,bigmouth buffalo filter plankton with fine gill rakers instead of rooting for prey. some individuals survive for more than a century.,inactive
-2302,golden redhorse,Freshwater bottom fish,Fresh Water,river,Spring | Summer | Fall,Day,Any,Any,Uncommon,0.9,0.5,8,6,14,0.9,,,,"golden redhorses probe flowing river bottoms with fleshy, downward-facing mouths. coppery scales and reddish fins give them a warm glow.",inactive
-2303,quillback,Freshwater bottom fish,Fresh Water,river,Spring | Summer | Fall,Day,Any,Any,Uncommon,0.8,0.5,15,6,16,0.88,,,,quillbacks carry a long first dorsal ray that rises like a quill. small mouths gather insect larvae and other food from river and lake bottoms.,inactive
-2304,smallmouth buffalo,Freshwater bottom fish,Fresh Water,river,Spring | Summer | Fall,Any,Any,Any,Rare,0.3,3,45,14,34,0.84,,,,"smallmouth buffalo use thick lips to graze bottom-dwelling food in warm rivers. their deep, heavy bodies form a pronounced arch behind the head.",inactive
-2305,white sucker,Freshwater bottom fish,Fresh Water,river,Spring | Summer | Fall,Any,Any,Any,Common,2,0.3,8,3,8,0.9,,,,white suckers vacuum insects and organic matter from cool freshwater bottoms. spring runs carry large groups into shallow streams.,inactive
-2401,black bullhead,Freshwater catfish,Fresh Water,pond,Spring | Summer | Fall,Night,Any,Any,Uncommon,1,0.3,6,5,11,0.92,,,,"black bullheads tolerate warm, cloudy water with little oxygen. stout spines protect the dorsal and pectoral fins from predators.",inactive
-2402,blue catfish,Freshwater catfish,Fresh Water,river,Spring | Summer | Fall,Night,Any,Any,Uncommon,1.25,3,12,6,9,0.9,,,,a deep fork in the tail inspired the blue catfish's scientific name. it rests deep by daylight and becomes more active after dark.,active
-2403,brown bullhead,Freshwater catfish,Fresh Water,pond,Spring | Summer | Fall,Night,Any,Any,Common,2.3,0.2,4,3,7,0.95,,,,brown bullheads search muddy bottoms with eight sensitive barbels. males guard nests and herd newly hatched young in a dark moving ball.,inactive
-2404,channel catfish,Freshwater catfish,Fresh Water,pond,Spring | Summer | Fall,Night,Any,Any,Common,2,1.5,7,4,7,1,,,,"a channel catfish can taste through skin all over its body, especially near its gills and whiskers. the male watches the eggs.",active
-2405,flathead catfish,Freshwater catfish,Fresh Water,river,Spring | Summer | Fall,Night,Any,Any,Uncommon,1,3,14,7,10,0.9,,,,"flatheads favor hideouts made by logs, rocks, ledges, and other underwater structure. as they grow, fish dominate their menu.",active
-2406,tadpole madtom,Freshwater catfish,Fresh Water,river,Spring | Summer | Fall,Night,Any,Any,Rare,0.35,0.02,0.15,8,16,0.95,,,,tadpole madtoms are tiny nocturnal catfish that shelter beneath vegetation and debris. venomous fin spines discourage larger predators.,inactive
-2407,white catfish,Freshwater catfish,Fresh Water,pond,Spring | Summer | Fall,Night,Any,Any,Common,2,1,5,4,6,1,,,,white catfish began along the atlantic coast between new york and florida. people later carried them far beyond that home range.,active
-2408,yellow bullhead,Freshwater catfish,Fresh Water,pond,Spring | Summer | Fall,Night,Required,Any,Common,2,0.2,3.5,3,7,0.95,,,,yellow bullheads use pale chin barbels to distinguish food in murky shallows. both parents may guard the nest and schooling young.,inactive
-2501,common barbel,Freshwater coarse fish,Fresh Water,river,Spring | Summer | Fall,Night,Required,Any,Rare,0.28,1,25,13,31,0.85,,,,common barbels hold close to fast river bottoms with streamlined bodies and broad fins. four mouth barbels locate food among stones.,inactive
-2502,common bream,Freshwater coarse fish,Fresh Water,lake,Spring | Fall | Winter,Any,Any,Any,Common,1.8,0.3,15,4,12,0.88,,,,"common bream gather in slow rivers and lakes with silvery, deep-sided bodies. protruding mouths sift insect larvae from soft bottoms.",inactive
-2503,common carp,Freshwater coarse fish,Fresh Water,pond,Spring | Summer | Fall | Winter,Any,Any,Any,Rare,1.5,3,18,10,10,0.85,,,,whisker-like barbels help a common carp investigate the bottom. nosing through sediment can cloud the water and loosen plants.,active
-2504,common roach,Freshwater coarse fish,Fresh Water,pond,Spring | Summer | Fall | Winter,Any,Any,Any,Common,2.8,0.1,4,2,6,0.95,,,,"common roach thrive in many lakes and rivers, often schooling near vegetation. red fins and a red-orange eye brighten their silver bodies.",inactive
-2505,european chub,Freshwater coarse fish,Fresh Water,river,Spring | Summer | Fall | Winter,Any,Any,Any,Common,0.55,0.2,6,2,4,0.9,,,,"european chub are adaptable river fish. adults often patrol shallows and feed on insects, small fish, and fruit.",active
-2506,fallfish,Freshwater coarse fish,Fresh Water,river,Spring | Summer | Fall,Day,Required,Any,Uncommon,0.9,0.2,3.5,5,10,0.92,,,,fallfish are large minnows that build broad stone nests in flowing water. other stream fish often reuse the gravel mounds for spawning.,inactive
-2507,ide,Freshwater coarse fish,Fresh Water,river,Spring | Summer | Fall,Day,Any,Any,Uncommon,0.8,0.3,9,6,14,0.9,,,,"ide move between rivers, lakes, and brackish coastal water in loose schools. adults eat insects, crustaceans, plants, and small fish.",inactive
-2508,rudd,Freshwater coarse fish,Fresh Water,pond,Spring | Summer | Fall,Day,Any,Any,Common,2.4,0.1,4.5,3,7,0.95,,,,"rudd feed near the surface with an upturned mouth and red fins. golden-green bodies disappear easily among warm, weedy shallows.",inactive
-2509,tench,Freshwater coarse fish,Fresh Water,pond,Spring | Summer | Fall,Night,Any,Required,Uncommon,0.75,0.5,12,7,17,0.88,,,,"tench browse quiet, muddy bottoms with two small mouth barbels. a thick coat of slime protects their olive-green scales.",inactive
-2601,red swamp crayfish,Freshwater crustaceans,Fresh Water,pond,Spring | Summer | Fall,Night,Required,Any,Common,1.1,0.03,0.25,3,7,0.98,,,,"red swamp crayfish burrow into wet ground when water recedes. adaptable populations feed on plants, carrion, and small aquatic animals.",inactive
-2701,alligator gar,Freshwater giants,Fresh Water,river,Spring | Summer | Fall,Night,Any,Any,Legendary,0.055,20,300,55,125,0.8,,,,alligator gar combine a broad tooth-filled snout with heavy armor-like scales. a vascular swim bladder lets them breathe air at the surface.,inactive
-2702,arapaima,Freshwater giants,Fresh Water,river,Spring | Summer | Fall | Winter,Day,Any,Any,Legendary,0.04,40,400,60,140,0.78,,,,arapaima must surface frequently to gulp air through a specialized swim bladder. large armored scales protect their long river-going bodies.,inactive
-2703,goliath tigerfish,Freshwater giants,Fresh Water,river,Spring | Summer | Fall | Winter,Day,Any,Any,Legendary,0.035,20,150,58,132,0.8,,,,goliath tigerfish patrol powerful african rivers with interlocking dagger-like teeth. a muscular forked tail drives sudden attacks.,inactive
-2704,muskellunge,Freshwater giants,Fresh Water,lake,Spring | Fall | Winter,Day,Any,Required,Epic,0.11,8,60,32,72,0.82,,,,muskellunge wait motionless among weeds before exploding toward prey. pale bodies carry bars or spots that fade into broken underwater light.,inactive
-2801,common shiner,Freshwater minnows,Fresh Water,river,Spring | Summer | Fall,Day,Any,Any,Common,4,0.03,0.3,1,3,1,,,,common shiners flash silver in clear streams and small rivers. breeding males develop rosy fins and a blue-violet sheen.,inactive
-2802,creek chub,Freshwater minnows,Fresh Water,river,Spring | Summer | Fall,Day,Any,Any,Common,3.2,0.05,0.8,2,4,1,,,,creek chubs gather around pools and undercut banks in small streams. males build gravel ridges where several fish may spawn.,inactive
-2803,emerald shiner,Freshwater minnows,Fresh Water,lake,Spring | Summer | Fall,Any,Any,Any,Common,3.5,0.03,0.25,1,3,1,,,,emerald shiners form bright surface schools in large lakes and rivers. slender bodies feed on plankton and drifting insects.,inactive
-2804,fathead minnow,Freshwater minnows,Fresh Water,pond,Spring | Summer | Fall,Day,Any,Any,Common,4.5,0.02,0.2,1,2,1,,,,"fathead minnows tolerate warm, muddy ponds where many fish struggle. males guard eggs laid beneath rocks, logs, and other cover.",inactive
-2805,flame chub,Freshwater minnows,Fresh Water,river,Spring | Summer | Fall,Any,Any,Any,Uncommon,0.4,0.05,0.15,3,6,0.9,,,,"flame chubs prefer cool, clear streams. their bright breeding colors make a small fish easy to spot in spring water.",active
-2806,golden shiner,Freshwater minnows,Fresh Water,pond,Spring | Summer | Fall,Day,Any,Any,Common,3.8,0.05,1,2,4,1,,,,golden shiners school around dense vegetation in warm water. an upturned mouth takes insects and plankton from near the surface.,inactive
-2807,lake chub,Freshwater minnows,Fresh Water,lake,Spring | Fall | Winter,Any,Any,Any,Common,0.55,0.05,0.5,2,4,0.9,,,,lake chubs school in cold northern water and use their small mouths to pick insects and other tiny prey from the current.,active
-2901,electric eel,Freshwater oddities,Fresh Water,river,Spring | Summer | Fall | Winter,Night,Required,Any,Epic,0.1,1,44,25,60,0.82,,,,electric eels navigate and hunt with electrical pulses produced along most of the body. they must surface regularly to breathe air.,inactive
-2902,freshwater drum,Freshwater oddities,Fresh Water,river,Spring | Summer | Fall,Night,Required,Any,Uncommon,0.7,1,35,7,20,0.85,,,,freshwater drum crush snails and mussels with powerful throat teeth. muscles vibrating against the swim bladder produce a low drumming sound.,inactive
-2903,goldeye,Freshwater oddities,Fresh Water,river,Spring | Fall | Winter,Night,Any,Required,Rare,0.3,0.3,6,12,25,0.9,,,,goldeye patrol turbid rivers with large light-sensitive eyes and a toothed tongue. they rise toward the surface to hunt insects and small fish.,inactive
-2904,mooneye,Freshwater oddities,Fresh Water,river,Spring | Fall | Winter,Night,Any,Any,Uncommon,0.85,0.2,4,5,11,0.92,,,,mooneyes flash silver in clear rivers and lakes while feeding near the surface. their large eyes suit evening hunts for insects and small fish.,inactive
-2905,weather loach,Freshwater oddities,Fresh Water,pond,Spring | Summer | Fall | Winter,Any,Required,Any,Uncommon,0.85,0.05,0.7,5,11,0.95,,,,weather loaches gulp air and pass it through the intestine when oxygen runs low. restless activity often increases as air pressure falls.,inactive
-3001,bubble-eye goldfish,Freshwater ornamental,Fresh Water,pond,Spring | Summer | Fall | Winter,Any,Any,Any,Uncommon,0.35,0.1,0.35,3,7,0.9,,,,"bubble-eye goldfish have delicate fluid-filled sacs beneath their eyes, making careful handling especially important.",active
-3002,goldfish,Freshwater ornamental,Fresh Water,pond,Spring | Summer | Fall | Winter,Any,Any,Any,Common,0.5,0.1,4,1,3,0.9,,,,"goldfish can tolerate a surprisingly wide range of conditions. their wild relatives often live in slow, weedy water.",active
-3003,koi,Freshwater ornamental,Fresh Water,pond,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Rare,0.28,0.5,35,14,34,0.87,,,,koi are colorful domesticated forms of common carp selected over many generations. each fish carries a distinctive arrangement of patches and scales.,inactive
-3101,black crappie,Freshwater panfish,Fresh Water,pond,Spring | Fall | Winter,Any,Any,Required,Uncommon,1,0.2,4,5,11,0.9,,,,black crappie gather around submerged wood and vegetation in clear water. irregular dark speckles scatter across their deep silver bodies.,inactive
-3102,bluegill,Freshwater panfish,Fresh Water,pond,Spring | Summer | Fall,Day,Any,Any,Common,10,0.3,2,3,3,1,,,,"bluegill fathers sweep shallow bowls into the bottom for nests, then stand guard. whole neighborhoods of nests can crowd together.",active
-3103,green sunfish,Freshwater panfish,Fresh Water,pond,Spring | Summer | Fall,Day,Any,Any,Common,3.2,0.1,1.5,2,5,1,,,,"green sunfish tolerate small, warm, cloudy pools better than many relatives. a large mouth lets them tackle insects, crayfish, and fish.",inactive
-3104,pumpkinseed,Freshwater panfish,Fresh Water,pond,Spring | Summer | Fall,Day,Any,Any,Common,3,0.1,1.3,2,5,1,,,,pumpkinseeds use compact teeth to crush snails and other hard prey. blue facial lines and an orange gill spot brighten their round bodies.,inactive
-3105,redear sunfish,Freshwater panfish,Fresh Water,pond,Spring | Summer | Fall,Day,Any,Any,Uncommon,1.1,0.2,3,4,9,0.95,,,,"redear sunfish specialize in snails, crushing shells with strong throat teeth. a red or orange margin marks the dark flap behind the gill.",inactive
-3106,warmouth,Freshwater panfish,Fresh Water,pond,Spring | Summer | Fall,Night,Required,Any,Uncommon,0.8,0.2,2,5,10,0.95,,,,"warmouth hide in weeds, roots, and murky backwaters before ambushing prey. red eyes and dark cheek stripes frame a notably large mouth.",inactive
-3107,white crappie,Freshwater panfish,Fresh Water,lake,Spring | Fall | Winter,Night,Any,Any,Common,1.7,0.2,4.5,4,9,0.9,,,,"white crappie favor warm, cloudy water and gather around structure. vertical bars cross their silver sides instead of scattered speckles.",inactive
-3201,yellow perch,Freshwater perch,Fresh Water,lake,Spring | Fall | Winter,Day,Any,Any,Common,2.2,0.2,4,3,8,0.9,,,,yellow perch travel in schools through cool lakes and slow rivers. dark vertical bars and orange lower fins make them easy to recognize.,inactive
-3301,chain pickerel,Freshwater predators,Fresh Water,pond,Spring | Fall | Winter,Day,Required,Any,Uncommon,0.8,0.5,9,6,15,0.9,,,,chain pickerel hover among vegetation before darting at fish and frogs. a chain-like pattern breaks up their green flanks.,inactive
-3302,grass pickerel,Freshwater predators,Fresh Water,pond,Spring | Fall | Winter,Day,Any,Any,Common,2.1,0.1,1.5,2,6,0.95,,,,"grass pickerel remain small enough to hunt in narrow, weedy creeks and marshes. dark bars camouflage their slim bodies among stems.",inactive
-3303,northern pike,Freshwater predators,Fresh Water,lake,Spring | Fall | Winter,Day,Any,Any,Uncommon,0.75,1,30,8,24,0.85,,,,northern pike are solitary ambush hunters built for sudden acceleration. pale bean-shaped spots cover their long green bodies.,inactive
-3304,sauger,Freshwater predators,Fresh Water,river,Spring | Fall | Winter,Any,Any,Any,Uncommon,0.35,0.5,8,6,12,0.9,,,,"sauger favor turbid rivers and reservoirs. their mottled backs blend into the dim, shifting light near the bottom.",active
-3305,saugeye,Freshwater predators,Fresh Water,lake,Spring | Fall | Winter,Any,Any,Any,Rare,0.2,0.5,12,9,18,0.9,,,,saugeye are a fertile hybrid of sauger and walleye. they combine traits from both parent species and often grow quickly.,active
-3306,walleye,Freshwater predators,Fresh Water,lake,Spring | Fall | Winter,Any,Any,Any,Uncommon,0.4,0.5,15,7,15,0.9,,,,walleye have light-sensitive eyes that help them hunt in murky water and around dusk. their reflective layer gives the eyes a pale glow.,active
-3401,round goby,Freshwater small fish,Fresh Water,lake,Spring | Summer | Fall | Winter,Any,Any,Any,Common,8,0.1,0.6,2,3,1,,,,round gobies use fused pelvic fins like a suction cup to hold onto rocks. they are small fish with a famously large appetite.,active
-3501,crystal jelly,Jellies,Salt Water,ocean,Spring | Winter,Night,Any,Forbidden,Rare,0.24,0.05,4,15,34,0.93,,,,crystal jellies are nearly transparent except for glowing points around the bell. their green fluorescent protein transformed biological research.,inactive
-3502,lion's mane jelly,Jellies,Salt Water,ocean,Spring | Fall | Winter,Night,Any,Required,Epic,0.11,1,200,24,58,0.82,,,,lion's mane jellies trail dense curtains of tentacles through cold seas. the bell darkens from yellow to deep red as the animal grows.,inactive
-3503,moon jelly,Jellies,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Any,Uncommon,0.7,0.05,5,6,14,0.94,,,,moon jellies pulse gently with four horseshoe-shaped organs visible through the bell. short tentacles sweep plankton toward the mouth.,inactive
-3504,Pacific sea nettle,Jellies,Salt Water,ocean,Summer | Fall,Day,Any,Forbidden,Rare,0.28,0.2,12,14,32,0.9,,,,pacific sea nettles carry rust-colored stripes across pale bells. long tentacles and frilled mouth arms capture plankton and small fish.,inactive
-3505,sea wasp,Jellies,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Required,Any,Legendary,0.035,0.2,6,42,90,0.9,,,,sea wasps have box-shaped bells and clusters of eyes along the lower edge. their long tentacles deliver exceptionally potent venom.,inactive
-3601,African pompano,Large coastal predators,Salt Water,ocean,Summer | Fall,Any,Any,Any,Rare,0.26,2,50,14,35,0.84,,,,african pompano develop long streaming fin rays while young. adults become deep-bodied silver hunters around reefs and offshore structure.,inactive
-3602,cobia,Large coastal predators,Salt Water,ocean,Summer | Fall,Day,Any,Any,Rare,0.24,5,80,15,40,0.82,,,,"cobia often follow sharks, rays, and floating structures in warm seas. a broad dark stripe runs along their long, powerful bodies.",inactive
-3603,giant trevally,Large coastal predators,Salt Water,ocean,Summer | Fall,Day,Any,Any,Epic,0.1,10,150,32,78,0.8,,,,giant trevally hunt reef edges with bursts of speed and force. large individuals sometimes charge prey from surf and shallow lagoons.,inactive
-3604,greater amberjack,Large coastal predators,Salt Water,ocean,Summer | Fall,Any,Any,Any,Rare,0.25,5,90,15,42,0.82,,,,"greater amberjacks circle reefs, wrecks, and offshore platforms. a dark stripe crosses the eye of their sturdy torpedo-shaped bodies.",inactive
-3605,lesser amberjack,Large coastal predators,Salt Water,ocean,Summer | Fall,Any,Any,Any,Uncommon,0.75,1,20,7,18,0.86,,,,lesser amberjacks school around deep reefs and floating objects. an enlarged eye suits their preference for dimmer water.,inactive
-3701,atlantic halibut,Large flatfish,Salt Water,ocean,Spring | Fall | Winter,Any,Any,Any,Epic,0.1,10,300,32,82,0.8,,,,atlantic halibut are immense right-eyed flatfish of cold northern seas. adults ambush fish and squid from deep rocky or sandy bottoms.,inactive
-3702,pacific halibut,Large flatfish,Salt Water,ocean,Spring | Fall | Winter,Any,Any,Any,Epic,0.1,10,350,34,86,0.8,,,,pacific halibut lie camouflaged on cold continental shelves. powerful flat bodies can travel long distances between feeding and spawning grounds.,inactive
-3703,turbot,Large flatfish,Salt Water,ocean,Spring | Fall | Winter,Any,Any,Required,Rare,0.24,1,35,14,34,0.85,,,,"turbot have broad, nearly circular bodies covered with bony bumps instead of scales. they bury into sand before ambushing fish and crustaceans.",inactive
-3801,American lobster,Lobsters and shrimp,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Required,Rare,0.28,0.5,44,16,42,0.86,,,,american lobsters shelter in rocky crevices and emerge to forage along the bottom. one claw crushes while the other cuts and grips.,inactive
-3802,Antarctic krill,Lobsters and shrimp,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Any,Rare,0.3,0.001,0.01,10,22,0.99,,,,"antarctic krill form enormous swarms beneath southern seas and ice. these small crustaceans feed whales, seals, penguins, and fish.",inactive
-3803,Caribbean spiny lobster,Lobsters and shrimp,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Forbidden,Rare,0.25,0.5,26,15,38,0.87,,,,caribbean spiny lobsters rely on long antennae and a thorny shell instead of large claws. groups march together across the seafloor.,inactive
-3804,peacock mantis shrimp,Lobsters and shrimp,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Required,Any,Epic,0.1,0.05,0.4,22,50,0.96,,,,peacock mantis shrimp strike hard-shelled prey with club-like limbs. complex eyes move independently and detect colors beyond human vision.,inactive
-3805,skunk cleaner shrimp,Lobsters and shrimp,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Uncommon,0.75,0.01,0.15,5,11,0.98,,,,skunk cleaner shrimp advertise at reef cleaning stations with bright white antennae. visiting fish pause while parasites are picked away.,inactive
-3901,Atlantic ghost crab,Marine crabs,Salt Water,ocean,Summer | Fall,Night,Any,Forbidden,Rare,0.28,0.02,0.5,11,24,0.97,,,,atlantic ghost crabs dash across beaches on long pale legs and retreat into deep burrows. raised eyes scan nearly every direction.,inactive
-3902,blue crab,Marine crabs,Salt Water,ocean,Summer | Fall,Day,Any,Any,Common,1.15,0.1,1.1,3,8,0.95,,,,"blue crabs swim with flattened rear legs shaped like paddles. bright blue claws and an olive shell conceal a quick, defensive temperament.",inactive
-3903,Dungeness crab,Marine crabs,Salt Water,ocean,Spring | Fall | Winter,Day,Any,Required,Uncommon,0.65,0.5,3,6,14,0.92,,,,dungeness crabs bury beneath sand with only their eyes and antennae exposed. broad toothed shells protect them along cool pacific coasts.,inactive
-3904,Japanese spider crab,Marine crabs,Salt Water,ocean,Spring | Fall | Winter,Night,Any,Any,Legendary,0.045,5,44,38,88,0.84,,,,japanese spider crabs carry the longest leg span of any living arthropod. adults roam deep seafloors around japan.,inactive
-3905,red king crab,Marine crabs,Salt Water,ocean,Spring | Fall | Winter,Night,Any,Required,Epic,0.11,2,24,24,58,0.85,,,,"red king crabs stride over cold northern bottoms on long armored legs. despite the name, they are closer to hermit crabs than true crabs.",inactive
-4001,american eel,Migratory eels,Fresh Water | Salt Water,river,Fall | Winter,Night,Required,Any,Rare,0.25,0.3,8,12,27,0.88,,,,american eels hatch far out in the sargasso sea before entering freshwater rivers. adults later reverse the journey to spawn once.,inactive
-4002,european eel,Migratory eels,Fresh Water | Salt Water,river,Fall | Winter,Night,Any,Required,Rare,0.2,0.3,10,13,30,0.88,,,,european eels cross the atlantic as transparent larvae before entering rivers and lakes. mature silver eels return to sea to reproduce.,inactive
-4101,rainbow smelt,Migratory forage fish,Fresh Water | Salt Water,lake,Spring | Winter,Night,Any,Any,Common,2.5,0.05,0.8,2,5,1,,,,"rainbow smelt move between coastal water and rivers, though some remain landlocked. cucumber-like aromas cling to freshly caught fish.",inactive
-4201,eulachon,Ocean-run forage fish,Fresh Water | Salt Water,river,Spring,Night,Required,Any,Uncommon,0.9,0.05,0.5,4,9,0.98,,,,eulachon return from the pacific to spawn in coastal rivers. their oil-rich bodies once served as candles when dried and fitted with wicks.,inactive
-4301,white sturgeon,Ocean-run giants,Fresh Water | Salt Water,river,Spring | Fall,Night,Any,Any,Legendary,0.045,20,500,58,135,0.78,,,,white sturgeon are the largest freshwater fish in north america. armored plates line bodies built for long lives in rivers and estuaries.,inactive
-4401,atlantic salmon,Ocean-run salmon,Salt Water,ocean,Spring | Fall,Any,Any,Any,Epic,0.65,8,40,20,42,0.85,,,,"atlantic salmon can return from the ocean to the river where they hatched. unlike many salmon, some survive to make the trip again.",active
-4402,chum salmon,Ocean-run salmon,Salt Water,ocean,Fall,Any,Any,Any,Uncommon,2.5,6,25,10,22,0.9,,,,chum salmon travel immense distances and develop striking bars when they return to fresh water to spawn.,active
-4403,coho salmon,Ocean-run salmon,Salt Water,ocean,Fall | Winter,Any,Any,Any,Rare,1.4,5,30,14,28,0.88,,,,coho salmon are agile hunters known for powerful leaps. young coho may spend more than a year growing in streams.,active
-4404,pink salmon,Ocean-run salmon,Salt Water,ocean,Summer | Fall,Any,Any,Any,Uncommon,3,3,12,8,15,0.92,,,,pink salmon usually follow a strict two-year life cycle. spawning males develop the hump that inspired their nickname.,active
-4405,sockeye salmon,Ocean-run salmon,Salt Water,ocean,Summer | Fall,Any,Any,Any,Rare,1.6,4,15,12,24,0.9,,,,sockeye salmon feed heavily on plankton at sea. adults turn vivid red as they return inland to spawn.,active
-4501,steelhead trout,Ocean-run salmonids,Fresh Water,river,Spring | Fall | Winter,Any,Any,Any,Rare,0.2,2,35,10,20,0.9,,,,steelhead are the ocean-going form of rainbow trout. adults can leave fresh water and later return to spawn.,active
-4601,black marlin,Oceanic billfish,Salt Water,ocean,Summer | Fall,Any,Any,Any,Epic,0.08,80,700,45,90,0.9,,,,black marlin are powerful billfish that spend much of their lives in warm open ocean and can make blistering runs.,active
-4602,blue marlin,Oceanic billfish,Salt Water,ocean,Summer | Fall,Any,Any,Any,Legendary,0.05,100,1000,60,120,0.9,,,,blue marlin are highly migratory open-ocean hunters. their long bill helps them stun prey before turning back to feed.,active
-4603,sailfish,Oceanic billfish,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Epic,0.1,30,200,38,78,0.9,,,,sailfish raise their enormous dorsal fin while herding baitfish. they are among the fastest fish in the sea.,active
-4604,swordfish,Oceanic billfish,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Legendary,0.06,50,1000,55,110,0.9,,,,"swordfish are fast, wide-ranging predators with a flattened bill. they can cross deep water and warm surface layers in one day.",active
-4605,white marlin,Oceanic billfish,Salt Water,ocean,Summer | Fall,Any,Any,Any,Epic,0.1,30,180,35,70,0.9,,,,white marlin favor warm offshore water and use their rounded dorsal fin and bill to weave through schools of prey.,active
-4701,mahi-mahi,Oceanic pelagic fish,Salt Water,ocean,Summer | Fall,Day,Any,Forbidden,Rare,0.28,2,50,15,38,0.84,,,,"mahi-mahi flash brilliant blue, green, and gold while chasing prey near the surface. they grow quickly and gather around floating objects.",inactive
-4702,opah,Oceanic pelagic fish,Salt Water,ocean,Summer | Fall,Day,Any,Forbidden,Epic,0.09,20,600,36,88,0.8,,,,"opah circulate warm blood through much of the body, an unusual trait among fish. round silver flanks glow with red fins and pale spots.",inactive
-4703,wahoo,Oceanic pelagic fish,Salt Water,ocean,Summer | Fall,Day,Any,Any,Epic,0.11,10,100,30,70,0.82,,,,wahoo are streamlined open-ocean hunters with rows of sharp teeth. blue vertical bars flicker across their sides during fast pursuits.,inactive
-4801,albacore tuna,Oceanic tuna,Salt Water,ocean,Spring | Summer | Fall,Any,Any,Any,Rare,1.5,10,45,12,24,0.85,,,,albacore have unusually long pectoral fins. their warm muscles help them cruise across wide stretches of open ocean.,active
-4802,bigeye tuna,Oceanic tuna,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Epic,0.55,20,120,22,48,0.8,,,,"bigeye tuna make daily trips between deep, cool water and warmer surface layers. their large eyes suit those dim depths.",active
-4803,bluefin tuna,Oceanic tuna,Salt Water,ocean,Spring | Fall,Any,Any,Any,Legendary,0.18,30,250,35,90,0.78,,,,bluefin tuna are powerful ocean travelers. heat retained in their swimming muscles lets them thrive in surprisingly cold seas.,active
-4804,skipjack tuna,Oceanic tuna,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Uncommon,3,5,25,8,16,0.9,,,,skipjack travel in fast-moving schools and have dark stripes along their lower sides. they rarely stop swimming.,active
-4805,yellowfin tuna,Oceanic tuna,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Rare,1,15,80,16,36,0.82,,,,yellowfin are named for their bright yellow fins and finlets. large schools often gather with other open-ocean hunters.,active
-4901,common octopus,Octopuses,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Any,Uncommon,0.65,0.2,22,7,18,0.87,,,,common octopuses squeeze through narrow cracks and solve problems while hunting reefs. skin cells rapidly change color and texture.,inactive
-4902,day octopus,Octopuses,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Rare,0.24,0.2,15,15,36,0.88,,,,"day octopuses forage across tropical reefs while the sun is high. shifting bars and spots communicate alarm, camouflage, and threat.",inactive
-4903,giant Pacific octopus,Octopuses,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Required,Epic,0.1,5,200,28,70,0.81,,,,giant pacific octopuses explore cold rocky dens with thousands of arm suckers. females guard their eggs without feeding until they hatch.,inactive
-4904,greater blue-ringed octopus,Octopuses,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Required,Any,Legendary,0.04,0.05,0.3,38,82,0.96,,,,greater blue-ringed octopuses reveal vivid rings when threatened. a tiny beak can deliver venom strong enough to stop breathing.,inactive
-5001,sunfish,Open-ocean giants,Salt Water,ocean,Spring | Summer | Fall,Day,Any,Any,Common,7,0.4,2.5,3,3,1,,,,ocean sunfish are built more like enormous swimming heads than ordinary fish. they visit the surface and turn sideways to warm up.,active
-5101,barndoor skate,Rays and skates,Salt Water,ocean,Spring | Fall | Winter,Any,Any,Required,Epic,0.1,5,150,28,68,0.8,,,,barndoor skates glide over cold atlantic bottoms with broad wing-like fins. rows of thorny spines protect the back and tail.,inactive
-5102,giant manta ray,Rays and skates,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Legendary,0.025,100,1000,65,150,0.76,,,,giant manta rays funnel plankton into wide mouths with two flexible head fins. each belly carries a unique pattern of dark spots.,inactive
-5103,largetooth sawfish,Rays and skates,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Required,Any,Legendary,0.035,25,1300,58,135,0.78,,,,largetooth sawfish detect hidden prey with a long sensory rostrum. a swift sideways sweep stuns fish and exposes animals buried in mud.,inactive
-5104,ocellate river stingray,Rays and skates,Fresh Water,river,Spring | Summer | Fall | Winter,Day,Required,Any,Epic,0.09,2,35,28,65,0.82,,,,ocellate river stingrays carry bright eye-like spots across a round disc. they hide beneath sand in south american rivers.,inactive
-5105,southern stingray,Rays and skates,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Rare,0.23,2,80,15,40,0.82,,,,southern stingrays bury in warm sand with only their eyes and spiracles visible. crushing plates break open clams and crustaceans.,inactive
-5201,red lionfish,Reef ambush fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Any,Rare,0.3,0.2,3,11,25,0.92,,,,red lionfish spread ornate venomous spines when threatened. broad fins herd smaller fish into corners before a rapid gulp.,inactive
-5202,red scorpionfish,Reef ambush fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Any,Rare,0.3,0.2,7,11,25,0.9,,,,red scorpionfish vanish against rocky reefs through mottled color and skin flaps. venomous dorsal spines punish careless predators.,inactive
-5301,green moray,Reef eels,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Any,Rare,0.28,1,30,13,32,0.86,,,,green morays appear green because yellow mucus coats blue-gray skin. a second set of jaws pulls captured prey deeper into the throat.,inactive
-5401,peacock flounder,Reef flatfish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Rare,0.3,0.2,5,11,24,0.9,,,,peacock flounders change their spots and colors to match tropical sand. both eyes scan above while the flat body remains concealed.,inactive
-5501,blue tang,Reef grazers,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Uncommon,0.9,0.05,1.5,5,11,0.95,,,,blue tangs graze algae from coral reefs with small close-set teeth. sharp folding spines near the tail give surgeonfish their name.,inactive
-5502,rainbow parrotfish,Reef grazers,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Epic,0.12,2,40,25,58,0.84,,,,rainbow parrotfish scrape algae from reefs with fused beak-like teeth. crushed coral passes through them and returns to the reef as sand.,inactive
-5503,stoplight parrotfish,Reef grazers,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Rare,0.28,0.5,15,12,29,0.88,,,,stoplight parrotfish change color dramatically as they mature. powerful dental plates bite algae from hard reef surfaces.,inactive
-5504,yellow tang,Reef grazers,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Forbidden,Any,Rare,0.35,0.05,0.8,9,20,0.96,,,,yellow tangs browse algae across hawaiian reefs during daylight. at night their bright bodies darken and show a pale horizontal stripe.,inactive
-5601,atlantic trumpetfish,Reef oddities,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Rare,0.3,0.1,2,10,24,0.94,,,,atlantic trumpetfish hover head-down among corals and sea fans. long tubular snouts expand suddenly to draw in small fish.,inactive
-5602,gray triggerfish,Reef oddities,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Uncommon,0.8,0.3,12,7,17,0.88,,,,"gray triggerfish lock a stout dorsal spine upright when wedged into shelter. strong teeth crack barnacles, crabs, and shellfish.",inactive
-5603,guineafowl puffer,Reef oddities,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Rare,0.3,0.2,5,11,26,0.9,,,,guineafowl puffers swallow water to inflate when threatened. adults may shift from a spotted dark pattern to a vivid yellow form.,inactive
-5604,longhorn cowfish,Reef oddities,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Required,Epic,0.13,0.1,2,23,52,0.94,,,,longhorn cowfish wear a rigid box of fused scales with horns above the eyes. small fins steer the angular body with surprising precision.,inactive
-5605,queen triggerfish,Reef oddities,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Rare,0.25,0.5,15,14,32,0.88,,,,queen triggerfish blow jets of water to uncover buried prey and flip sea urchins. vivid blue lines radiate across the face.,inactive
-5606,red cornetfish,Reef oddities,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Required,Epic,0.11,0.2,4,24,55,0.92,,,,red cornetfish stalk reefs with extremely long bodies and tubular snouts. a trailing filament extends from the center of the tail.,inactive
-5607,spotted porcupinefish,Reef oddities,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Required,Epic,0.12,0.5,7,24,54,0.9,,,,spotted porcupinefish inflate until long spines stand outward. fused teeth form a strong beak for crushing shelled prey.,inactive
-5608,yellow boxfish,Reef oddities,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Rare,0.32,0.03,0.8,10,22,0.96,,,,yellow boxfish juveniles are bright cubes covered in black spots. rigid body armor leaves small fins to provide careful steering.,inactive
-5701,copperband butterflyfish,Reef ornamentals,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Rare,0.32,0.03,0.4,9,21,0.98,,,,copperband butterflyfish probe reef cracks with long narrow snouts. bold orange bands and a false eyespot confuse predators.,inactive
-5702,ocellaris clownfish,Reef ornamentals,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Uncommon,1,0.01,0.15,5,10,1,,,,ocellaris clownfish shelter safely among stinging anemone tentacles. each group is led by a female that develops from the largest male.,inactive
-5703,queen angelfish,Reef ornamentals,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Rare,0.3,0.1,2,11,25,0.94,,,,queen angelfish graze sponges from warm atlantic reefs. a blue-ringed spot on the forehead resembles a tiny crown.,inactive
-5801,Atlantic goliath grouper,Reef predators,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Required,Legendary,0.05,20,800,50,125,0.78,,,,"atlantic goliath groupers occupy reefs, wrecks, and mangrove roots. enormous mouths pull prey inward with a sudden pressure drop.",inactive
-5802,bluefin trevally,Reef predators,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Rare,0.3,1,25,14,32,0.86,,,,bluefin trevally hunt reef fish in coordinated groups. electric blue fins and scattered blue spots contrast with a golden body.,inactive
-5803,giant grouper,Reef predators,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Required,Legendary,0.045,20,880,52,128,0.78,,,,giant groupers become massive ambush predators around indo-pacific reefs. juveniles carry bold black-and-yellow markings.,inactive
-5804,great barracuda,Reef predators,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Required,Epic,0.12,5,80,28,65,0.82,,,,great barracudas hover motionless before accelerating toward prey. long jaws carry two distinct rows of sharp teeth.,inactive
-5805,gulf grouper,Reef predators,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Uncommon,0.3,2,40,10,24,0.9,,,,gulf grouper are ambush predators that use a sudden gulp to pull prey into their cavernous mouths.,active
-5806,Nassau grouper,Reef predators,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Forbidden,Epic,0.12,2,55,25,62,0.82,,,,nassau groupers shift their bars and colors with mood and surroundings. adults gather at traditional reef sites to spawn.,inactive
-5807,red grouper,Reef predators,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Rare,0.2,2,30,16,36,0.9,,,,red grouper excavate shelters in reef rubble. the hollows they make can become hiding places for many smaller animals.,active
-5901,blue-green chromis,Reef schooling fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Common,2.8,0.01,0.25,2,4,1,,,,blue-green chromis form loose clouds above branching coral. the school dives into narrow shelter whenever a predator approaches.,inactive
-5902,bluestriped grunt,Reef schooling fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Common,2,0.1,3,3,7,0.94,,,,bluestriped grunts shelter on reefs by day and spread over seagrass to feed at night. grinding throat teeth produce audible grunts.,inactive
-5903,French grunt,Reef schooling fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Common,2,0.1,3.5,3,7,0.94,,,,french grunts gather in yellow-striped schools around reefs and wrecks. they travel into nearby sand and grass beds after dark.,inactive
-5904,sergeant major,Reef schooling fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Common,2.5,0.02,0.5,2,5,0.98,,,,sergeant majors guard purple egg patches on hard reef surfaces. five dark bars cross their yellow and silver bodies.,inactive
-6001,lane snapper,Reef snapper,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Common,0.45,0.5,5,5,11,0.9,,,,lane snapper use reef structure for cover and hunt small crustaceans and fish when the light begins to fade.,active
-6002,mangrove snapper,Reef snapper,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Uncommon,0.35,0.5,12,7,16,0.9,,,,"mangrove snapper shelter among roots and docks when young, then move toward reefs as they grow.",active
-6003,mutton snapper,Reef snapper,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Rare,0.2,2,30,12,28,0.9,,,,"mutton snapper have canine teeth that help them pick crabs, shrimp, and other hard-shelled prey from the reef.",active
-6004,red snapper,Reef snapper,Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Any,Any,Uncommon,0.35,1,25,8,19,0.9,,,,"red snapper gather around reefs, wrecks, and ledges. their diet includes small fish, shrimp, and squid.",active
-6101,ballan wrasse,Reef wrasses,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Uncommon,0.8,0.2,10,6,15,0.9,,,,ballan wrasse crush shellfish and sea urchins with strong throat teeth. thick lips and mottled colors suit rocky european reefs.,inactive
-6102,cleaner wrasse,Reef wrasses,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Rare,0.35,0.01,0.2,9,20,1,,,,cleaner wrasses advertise with a bright stripe and dance at reef stations. larger fish wait while parasites are picked from skin and gills.,inactive
-6103,hogfish,Reef wrasses,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Rare,0.3,0.5,20,12,30,0.86,,,,hogfish root through sand with long pig-like snouts in search of shellfish. adults can change from female to male as social groups shift.,inactive
-6201,green sea turtle,Sea turtles,Salt Water,ocean,Summer | Fall,Day,Any,Forbidden,Legendary,0.018,65,500,62,145,0.78,,,,green sea turtles graze seagrass meadows with finely serrated jaws. adults migrate long distances between feeding and nesting grounds.,inactive
-6202,hawksbill sea turtle,Sea turtles,Salt Water,ocean,Summer | Fall,Day,Required,Any,Legendary,0.014,75,250,66,150,0.8,,,,hawksbill sea turtles reach into reef crevices with narrow hooked beaks. a diet rich in sponges helps shape coral communities.,inactive
-6203,leatherback sea turtle,Sea turtles,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Required,Legendary,0.012,500,2000,75,175,0.76,,,,leatherback sea turtles cross entire oceans while hunting jellies. a flexible ridged shell and internal heat help them enter cold water.,inactive
-6204,loggerhead sea turtle,Sea turtles,Salt Water,ocean,Summer | Fall,Day,Any,Any,Legendary,0.016,150,1200,68,155,0.77,,,,loggerhead sea turtles crush crabs and mollusks with massive jaws. females often return near their birth beaches to nest.,inactive
-6301,leafy seadragon,Seahorses and seadragons,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Epic,0.1,0.1,0.5,24,55,0.94,,,,leafy seadragons drift among seaweed with elaborate skin lobes that resemble leaves. males carry eggs beneath the tail.,inactive
-6302,weedy seadragon,Seahorses and seadragons,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Required,Rare,0.22,0.1,0.6,16,38,0.93,,,,weedy seadragons rely on slender bodies and small leaf-like lobes for camouflage. males brood rows of eggs on the underside of the tail.,inactive
-6401,black-lip pearl oyster,Shellfish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Rare,0.22,0.1,5,14,40,0.91,,,,black-lip pearl oysters line their shells with dark-edged nacre. a trapped irritant can become coated in the same pearly material.,inactive
-6402,giant clam,Shellfish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Epic,0.1,5,500,28,72,0.8,,,,giant clams host photosynthetic algae inside brightly colored mantles. heavy shells remain anchored to one reef spot throughout adulthood.,inactive
-6403,Pacific geoduck,Shellfish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Required,Rare,0.25,0.5,15,14,34,0.88,,,,pacific geoducks bury their shells deep while a long siphon reaches the seafloor. these clams can survive for well over a century.,inactive
-6404,queen conch,Shellfish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Rare,0.24,1,5,15,36,0.91,,,,queen conchs graze algae with a long flexible snout. adults develop a broad flared shell lip and move by vaulting on one muscular foot.,inactive
-6405,red abalone,Shellfish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Required,Epic,0.09,0.2,12,24,56,0.88,,,,red abalones cling to pacific rocks with broad muscular feet. rows of shell holes release water that has passed over the gills.,inactive
-6501,bigfin reef squid,Squid,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Rare,0.3,0.2,5,13,30,0.91,,,,bigfin reef squid hover in coordinated groups around warm reefs. shifting colors and body patterns signal to nearby squid.,inactive
-6502,colossal squid,Squid,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Required,Any,Legendary,0.015,400,1100,82,190,0.76,,,,colossal squid carry swiveling hooks on their arms and tentacles. enormous eyes gather faint light in the deep southern ocean.,inactive
-6503,giant squid,Squid,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Required,Legendary,0.02,100,600,70,165,0.77,,,,giant squid hunt deep ocean prey with two long feeding tentacles. the largest eyes in the animal world watch for movement in darkness.,inactive
-6504,Humboldt squid,Squid,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Any,Epic,0.09,5,110,26,64,0.82,,,,humboldt squid flash red and white while hunting in large groups. powerful beaks and grasping suckers handle fish and other squid.,inactive
-6601,ballyhoo,Surface ocean fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Common,2.5,0.05,1,2,5,0.98,,,,ballyhoo skim warm ocean surfaces with an extended lower jaw. tight schools scatter into quick leaps when predators strike.,inactive
-6602,houndfish,Surface ocean fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Rare,0.3,0.5,10,11,25,0.88,,,,houndfish hunt near the surface with long jaws filled with fine teeth. powerful leaps can carry their narrow bodies far above the water.,inactive
-6603,tropical flyingfish,Surface ocean fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Forbidden,Any,Rare,0.35,0.05,1.5,10,22,0.94,,,,tropical flyingfish accelerate underwater before spreading enlarged fins above the surface. glides help them escape pursuing predators.,inactive
-6701,striped bass,Temperate coastal,Salt Water,ocean,Spring | Fall,Any,Any,Any,Uncommon,4,1.5,8,7,7,0.9,,,,"striped bass split their lives between salt and fresh water, returning upriver to spawn. a long-lived striper may see three decades.",active
-6801,atlantic croaker,Temperate estuary,Salt Water,ocean,Spring | Summer | Fall,Night,Required,Any,Common,2.1,0.1,5,3,8,0.92,,,,atlantic croakers vibrate muscles against the swim bladder to make a croaking sound. chin barbels locate worms in sandy bays.,inactive
-6802,black drum,Temperate estuary,Salt Water,ocean,Spring | Summer | Fall,Night,Any,Any,Uncommon,0.7,1,70,8,27,0.82,,,,black drum crush oysters and crabs with rounded throat teeth. large adults develop dark vertical bars that fade with age.,inactive
-6803,red drum,Temperate estuary,Salt Water,ocean,Spring | Summer | Fall,Any,Required,Any,Uncommon,0.75,1,45,8,23,0.84,,,,red drum use powerful tails to patrol surf and estuaries. one or more black spots near the tail may confuse predators about which end is the head.,inactive
-6804,striped mullet,Temperate estuary,Fresh Water | Salt Water,ocean,Spring | Summer | Fall,Day,Any,Any,Common,2.5,0.3,8,3,8,0.92,,,,striped mullet leap clear of the water and travel in coastal schools. they graze algae and detritus with specialized digestive systems.,inactive
-6805,weakfish,Temperate estuary,Salt Water,ocean,Spring | Summer | Fall,Night,Any,Required,Rare,0.32,0.5,18,12,29,0.86,,,,weakfish are named for delicate mouth tissue that tears easily around a hook. iridescent spots scatter across their bronze backs.,inactive
-6806,white perch,Temperate estuary,Fresh Water | Salt Water,river,Spring | Summer | Fall,Any,Any,Any,Uncommon,1.1,0.2,3.5,5,11,0.9,,,,white perch tolerate fresh and brackish water along the atlantic coast. deep silver bodies gather in schools around river mouths.,inactive
-6807,yellowfin croaker,Temperate estuary,Salt Water,ocean,Spring | Summer | Fall,Any,Any,Any,Common,2,0.1,4,3,8,0.92,,,,yellowfin croakers search sandy surf zones with a small chin barbel. internal muscles produce croaks and drumming calls.,inactive
-6901,black rockfish,Temperate reef fish,Salt Water,ocean,Spring | Fall | Winter,Any,Any,Any,Common,1.7,0.3,10,4,11,0.9,,,,"black rockfish gather above reefs and kelp along the north pacific coast. like other rockfish, they give birth to live young.",inactive
-6902,black sea bass,Temperate reef fish,Salt Water,ocean,Spring | Fall | Winter,Day,Any,Any,Common,1.7,0.3,8,4,10,0.9,,,,"black sea bass shelter around reefs, piers, and wrecks. many begin life as female and later become male.",inactive
-6903,canary rockfish,Temperate reef fish,Salt Water,ocean,Spring | Fall | Winter,Day,Any,Any,Uncommon,0.7,0.5,12,7,17,0.88,,,,canary rockfish glow orange-yellow over deep pacific reefs. these long-lived fish mature slowly and give birth to live young.,inactive
-6904,scup,Temperate reef fish,Salt Water,ocean,Spring | Fall | Winter,Day,Any,Any,Common,2.2,0.1,5,3,8,0.92,,,,"scup use small strong teeth to crush shellfish and other bottom prey. deep silver bodies gather around piers, reefs, and eelgrass.",inactive
-6905,sheepshead,Temperate reef fish,Salt Water,ocean,Spring | Fall | Winter,Day,Any,Any,Uncommon,0.75,0.5,20,7,18,0.86,,,,sheepshead carry flat human-like front teeth for biting barnacles and crabs. dark bars cross their high silver bodies.,inactive
-6906,tautog,Temperate reef fish,Salt Water,ocean,Spring | Fall | Winter,Day,Any,Any,Uncommon,0.8,0.5,20,7,18,0.86,,,,tautog pry mussels and crabs from rocky structure with thick lips and strong teeth. adults return to familiar shelters year after year.,inactive
-6907,yelloweye rockfish,Temperate reef fish,Salt Water,ocean,Spring | Fall | Winter,Any,Any,Required,Epic,0.09,2,35,28,64,0.84,,,,yelloweye rockfish can live for more than a century on deep pacific reefs. bright golden eyes stand out against orange-red bodies.,inactive
-7001,lingcod,Temperate reef predators,Salt Water,ocean,Spring | Fall | Winter,Any,Any,Any,Rare,0.25,2,60,14,38,0.83,,,,"lingcod guard rocky dens and ambush fish with a huge tooth-lined mouth. despite the name, they are greenling relatives rather than cod.",inactive
-7101,milkfish,Tropical coastal fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Common,1.8,0.5,25,4,12,0.88,,,,milkfish graze algae and tiny organisms in warm coastal shallows. deeply forked tails drive large schools through lagoons and estuaries.,inactive
-7201,roosterfish,Tropical coastal predators,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Any,Epic,0.11,5,100,30,70,0.82,,,,roosterfish raise seven long dorsal spines while chasing prey through tropical surf. dark bands cross their sleek silver bodies.,inactive
-7301,atlantic tarpon,Tropical estuary,Fresh Water | Salt Water,ocean,Spring | Summer | Fall | Winter,Any,Required,Any,Epic,0.09,10,180,34,82,0.8,,,,atlantic tarpon gulp air with a lung-like swim bladder when coastal water runs low on oxygen. giant silver scales flash during leaps.,inactive
-7302,barramundi,Tropical estuary,Fresh Water | Salt Water,river,Spring | Summer | Fall | Winter,Night,Required,Any,Rare,0.24,2,45,15,38,0.84,,,,barramundi move between rivers and coasts and can breathe in low-oxygen water. many mature first as males and later become females.,inactive
-7303,common snook,Tropical estuary,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Required,Any,Rare,0.3,1,30,13,32,0.85,,,,common snook wait around mangrove roots and current seams to ambush prey. a bold black lateral line divides their silver flanks.,inactive
-7401,bonefish,Tropical flats fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Rare,0.25,0.5,15,13,31,0.86,,,,bonefish sweep tropical flats in silvery schools and root for buried prey. their speed and pale bodies make them difficult to follow in shallow water.,inactive
-7402,Florida pompano,Tropical flats fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Uncommon,0.85,0.5,9,7,16,0.88,,,,"florida pompano search warm surf for crabs, clams, and sand fleas. short blunt snouts lead into deep, silvery bodies.",inactive
-7403,ladyfish,Tropical flats fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Required,Any,Common,1.7,0.2,8,4,10,0.9,,,,ladyfish race through warm shallows after schooling prey and often leap when pursued. a deeply forked tail powers their slender bodies.,inactive
-7404,permit,Tropical flats fish,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Forbidden,Any,Epic,0.11,3,50,27,62,0.84,,,,permit browse tropical flats for crabs and shellfish with rubbery mouths. tall silver bodies carry long sickle-shaped dorsal and anal fins.,inactive
-7501,clown knifefish,Tropical freshwater fish,Fresh Water,river,Spring | Summer | Fall | Winter,Night,Any,Required,Rare,0.25,1,25,16,38,0.86,,,,clown knifefish undulate one long belly fin to glide forward or backward. dark eye-like spots line their silver nocturnal bodies.,inactive
-7502,clown loach,Tropical freshwater fish,Fresh Water,river,Spring | Summer | Fall | Winter,Day,Required,Any,Rare,0.32,0.1,2,10,22,0.9,,,,clown loaches rest in groups and make clicking sounds while feeding. orange bodies carry three broad black bands and small eye spines.,inactive
-7503,discus,Tropical freshwater fish,Fresh Water,river,Spring | Summer | Fall | Winter,Day,Any,Required,Epic,0.12,0.1,1,24,52,0.92,,,,"discus move through quiet amazon backwaters with round, flattened bodies. parents feed newly hatched young with nutrient-rich skin mucus.",inactive
-7504,freshwater angelfish,Tropical freshwater fish,Fresh Water,river,Spring | Summer | Fall | Winter,Day,Any,Any,Rare,0.3,0.05,0.5,10,22,0.95,,,,freshwater angelfish slip between tall plants on narrow triangular bodies. long fins and vertical bars resemble stems and shadows.,inactive
-7505,giant gourami,Tropical freshwater fish,Fresh Water,lake,Spring | Summer | Fall | Winter,Day,Any,Any,Rare,0.25,2,20,13,31,0.86,,,,giant gouramis breathe surface air through a specialized labyrinth organ. adults graze plants but also investigate insects and small animals.,inactive
-7506,Nile tilapia,Tropical freshwater fish,Fresh Water,lake,Spring | Summer | Fall | Winter,Day,Any,Any,Common,2.2,0.3,8,3,9,0.9,,,,nile tilapia thrive in warm lakes and rivers on a varied diet. females protect eggs and young inside their mouths.,inactive
-7507,oscar,Tropical freshwater fish,Fresh Water,river,Spring | Summer | Fall | Winter,Day,Any,Any,Uncommon,0.8,0.3,4,6,14,0.9,,,,oscars are large south american cichlids that recognize routines and defend territories. a tail eyespot may redirect attacks away from the head.,inactive
-7508,Siamese fighting fish,Tropical freshwater fish,Fresh Water,pond,Spring | Summer | Fall | Winter,Day,Required,Any,Uncommon,1,0.01,0.15,5,10,1,,,,siamese fighting fish breathe surface air with a labyrinth organ. males build bubble nests and guard the eggs beneath them.,inactive
-7509,tambaqui,Tropical freshwater fish,Fresh Water,river,Spring | Summer | Fall | Winter,Day,Required,Any,Epic,0.1,8,80,30,68,0.82,,,,tambaqui follow seasonal floods into forests to feed on fruit and seeds. strong square teeth crack tough plant material.,inactive
-7510,walking catfish,Tropical freshwater fish,Fresh Water,pond,Spring | Summer | Fall | Winter,Night,Required,Any,Rare,0.3,0.5,5,11,24,0.9,,,,walking catfish wriggle across wet ground using stiff pectoral spines for support. an air-breathing organ helps them survive the journey.,inactive
-7601,giant snakehead,Tropical freshwater predators,Fresh Water,river,Spring | Summer | Fall | Winter,Night,Any,Required,Epic,0.1,3,45,28,64,0.84,,,,giant snakeheads breathe air and guard schools of young near the surface. long bodies launch sudden attacks from flooded vegetation.,inactive
-7602,golden dorado,Tropical freshwater predators,Fresh Water,river,Spring | Summer | Fall | Winter,Day,Required,Any,Epic,0.1,5,55,29,66,0.82,,,,"golden dorado chase fish through powerful south american rivers. muscular tails, heavy jaws, and golden scales suit life in fast current.",inactive
-7603,golden mahseer,Tropical freshwater predators,Fresh Water,river,Spring | Summer | Fall | Winter,Day,Required,Any,Epic,0.09,8,90,32,72,0.82,,,,golden mahseer migrate through swift mountain rivers to reach spawning grounds. large scales shine bronze beneath reddish fins.,inactive
-7604,peacock bass,Tropical freshwater predators,Fresh Water,river,Spring | Summer | Fall | Winter,Day,Any,Any,Rare,0.25,1,25,15,36,0.84,,,,peacock bass are aggressive south american cichlids rather than true bass. adults guard nests and hunt fish in warm clear water.,inactive
-7605,red-bellied piranha,Tropical freshwater predators,Fresh Water,river,Spring | Summer | Fall | Winter,Any,Required,Any,Rare,0.3,0.2,4,12,27,0.9,,,,"red-bellied piranhas communicate with barks and drum-like sounds. schools provide protection as they search for fish, insects, and carrion.",inactive
-7606,silver arowana,Tropical freshwater predators,Fresh Water,river,Spring | Summer | Fall | Winter,Day,Any,Required,Epic,0.11,1,15,25,56,0.86,,,,silver arowanas patrol the surface with upturned mouths and long bodies. powerful jumps capture insects and small animals above the water.,inactive
-7701,blue whale,Whales and dolphins,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Legendary,0.005,100000,330000,120,300,0.72,,,,blue whales are the largest animals known to have lived. pleated throats expand around vast mouthfuls of water that baleen filters for krill.,inactive
-7702,common bottlenose dolphin,Whales and dolphins,Salt Water,ocean,Spring | Summer | Fall | Winter,Day,Any,Forbidden,Legendary,0.012,330,1400,75,180,0.76,,,,"common bottlenose dolphins coordinate with whistles, clicks, and body signals. echolocation reveals prey hidden in dark or cloudy water.",inactive
-7703,humpback whale,Whales and dolphins,Salt Water,ocean,Spring | Fall | Winter,Day,Any,Required,Legendary,0.008,40000,90000,105,260,0.73,,,,humpback whales use long pectoral fins while turning and feeding. males produce layered songs that change across breeding seasons.,inactive
-7704,killer whale,Whales and dolphins,Salt Water,ocean,Spring | Fall | Winter,Day,Required,Any,Legendary,0.008,3000,12000,95,230,0.74,,,,killer whales live in close family groups with distinct calls and hunting traditions. different populations specialize in different prey.,inactive
-7705,sperm whale,Whales and dolphins,Salt Water,ocean,Spring | Summer | Fall | Winter,Night,Any,Required,Legendary,0.006,15000,125000,115,280,0.72,,,,sperm whales make deep dives to hunt squid with powerful echolocation clicks. their enormous square heads contain a waxy spermaceti organ.,inactive
diff --git a/docs/design/fish_catalog_proposal_final.csv b/docs/design/fish_catalog_proposal_final.csv
deleted file mode 100644
index e0b402f..0000000
--- a/docs/design/fish_catalog_proposal_final.csv
+++ /dev/null
@@ -1,314 +0,0 @@
-catalog_number,id,implementation_status,active_status,art_status,fish_resource_path,display_texture_path,catch_profile_id
-101,bowfin,implemented,inactive,missing icon,res://fish/species/bowfin/bowfin.tres,,common
-102,sturgeon_lake,implemented,inactive,missing icon,res://fish/species/sturgeon_lake/sturgeon_lake.tres,,common
-103,gar_longnose,implemented,inactive,missing icon,res://fish/species/gar_longnose/gar_longnose.tres,,common
-104,paddlefish,implemented,inactive,missing icon,res://fish/species/paddlefish/paddlefish.tres,,common
-105,sturgeon_shovelnose,implemented,inactive,missing icon,res://fish/species/sturgeon_shovelnose/sturgeon_shovelnose.tres,,common
-106,gar_spotted,implemented,inactive,missing icon,res://fish/species/gar_spotted/gar_spotted.tres,,common
-107,lungfish_west_african,implemented,inactive,missing icon,res://fish/species/lungfish_west_african/lungfish_west_african.tres,,common
-201,mudskipper_atlantic,implemented,inactive,missing icon,res://fish/species/mudskipper_atlantic/mudskipper_atlantic.tres,,common
-202,foureyes_largescale,implemented,inactive,missing icon,res://fish/species/foureyes_largescale/foureyes_largescale.tres,,common
-301,mullet_red,implemented,inactive,missing icon,res://fish/species/mullet_red/mullet_red.tres,,common
-401,plaice_european,implemented,inactive,missing icon,res://fish/species/plaice_european/plaice_european.tres,,common
-402,flounder_summer,implemented,inactive,missing icon,res://fish/species/flounder_summer/flounder_summer.tres,,common
-403,flounder_winter,implemented,inactive,missing icon,res://fish/species/flounder_winter/flounder_winter.tres,,common
-501,dugong,implemented,inactive,missing icon,res://fish/species/dugong/dugong.tres,,common
-502,seal_harbor,implemented,inactive,missing icon,res://fish/species/seal_harbor/seal_harbor.tres,,common
-503,sea_otter,implemented,inactive,missing icon,res://fish/species/sea_otter/sea_otter.tres,,common
-504,walrus,implemented,inactive,missing icon,res://fish/species/walrus/walrus.tres,,common
-505,manatee_west_indian,implemented,inactive,missing icon,res://fish/species/manatee_west_indian/manatee_west_indian.tres,,common
-601,pipefish_bay,implemented,inactive,missing icon,res://fish/species/pipefish_bay/pipefish_bay.tres,,common
-602,seahorse_lined,implemented,inactive,missing icon,res://fish/species/seahorse_lined/seahorse_lined.tres,,common
-603,monkfish,implemented,inactive,missing icon,res://fish/species/monkfish/monkfish.tres,,common
-604,tripletail,implemented,inactive,missing icon,res://fish/species/tripletail/tripletail.tres,,common
-701,mackerel_atlantic,implemented,active,complete,res://fish/species/mackerel_atlantic/mackerel_atlantic.tres,res://fish/species/mackerel_atlantic/fish_mackerel_atlantic.png,common
-702,bluefish,implemented,inactive,missing icon,res://fish/species/bluefish/bluefish.tres,,common
-703,mackerel_cero,implemented,active,complete,res://fish/species/mackerel_cero/mackerel_cero.tres,res://fish/species/mackerel_cero/fish_mackerel_cero.png,common
-704,mackerel_chub,implemented,active,complete,res://fish/species/mackerel_chub/mackerel_chub.tres,res://fish/species/mackerel_chub/fish_mackerel_chub.png,common
-705,jack_crevalle,implemented,inactive,missing icon,res://fish/species/jack_crevalle/jack_crevalle.tres,,common
-706,mackerel_king,implemented,active,complete,res://fish/species/mackerel_king/mackerel_king.tres,res://fish/species/mackerel_king/fish_mackerel_king.png,common
-707,mackerel_spanish,implemented,active,complete,res://fish/species/mackerel_spanish/mackerel_spanish.tres,res://fish/species/mackerel_spanish/fish_mackerel_spanish.png,common
-708,queenfish_talang,implemented,inactive,missing icon,res://fish/species/queenfish_talang/queenfish_talang.tres,,common
-801,sand_lance_american,implemented,inactive,missing icon,res://fish/species/sand_lance_american/sand_lance_american.tres,,common
-802,menhaden_atlantic,implemented,inactive,missing icon,res://fish/species/menhaden_atlantic/menhaden_atlantic.tres,,common
-803,pomfret_black,implemented,active,complete,res://fish/species/pomfret_black/pomfret_black.tres,res://fish/species/pomfret_black/fish_pomfret_black.png,common
-804,pomfret_chinese,implemented,active,complete,res://fish/species/pomfret_chinese/pomfret_chinese.tres,res://fish/species/pomfret_chinese/fish_pomfret_chinese.png,common
-805,anchovy_european,implemented,active,complete,res://fish/species/anchovy_european/anchovy_european.tres,res://fish/species/anchovy_european/fish_anchovy_european.png,common
-806,sardine_european,implemented,inactive,missing icon,res://fish/species/sardine_european/sardine_european.tres,,common
-807,pomfret_golden,implemented,active,complete,res://fish/species/pomfret_golden/pomfret_golden.tres,res://fish/species/pomfret_golden/fish_pomfret_golden.png,common
-808,anchovy_northern,implemented,active,complete,res://fish/species/anchovy_northern/anchovy_northern.tres,res://fish/species/anchovy_northern/fish_anchovy_northern.png,common
-809,sardine_pacific,implemented,inactive,missing icon,res://fish/species/sardine_pacific/sardine_pacific.tres,,common
-810,pomfret_white,implemented,active,complete,res://fish/species/pomfret_white/pomfret_white.tres,res://fish/species/pomfret_white/fish_pomfret_white.png,common
-901,shark_blacktip,implemented,inactive,missing icon,res://fish/species/shark_blacktip/shark_blacktip.tres,,common
-902,shark_great_hammerhead,implemented,inactive,missing icon,res://fish/species/shark_great_hammerhead/shark_great_hammerhead.tres,,common
-903,shark_great_white,implemented,inactive,missing icon,res://fish/species/shark_great_white/shark_great_white.tres,,common
-904,shark_tiger,implemented,inactive,missing icon,res://fish/species/shark_tiger/shark_tiger.tres,,common
-905,shark_whale,implemented,inactive,missing icon,res://fish/species/shark_whale/shark_whale.tres,,common
-1001,burbot,implemented,inactive,missing icon,res://fish/species/burbot/burbot.tres,,common
-1002,cisco,implemented,inactive,missing icon,res://fish/species/cisco/cisco.tres,,common
-1003,whitefish_lake,implemented,inactive,missing icon,res://fish/species/whitefish_lake/whitefish_lake.tres,,common
-1101,char_arctic,implemented,inactive,missing icon,res://fish/species/char_arctic/char_arctic.tres,,common
-1102,grayling_arctic,implemented,inactive,missing icon,res://fish/species/grayling_arctic/grayling_arctic.tres,,common
-1103,trout_brown,implemented,inactive,missing icon,res://fish/species/trout_brown/trout_brown.tres,,common
-1104,trout_cutthroat,implemented,active,complete,res://fish/species/trout_cutthroat/trout_cutthroat.tres,res://fish/species/trout_cutthroat/fish_trout_cutthroat.png,common
-1105,trout_golden,implemented,active,complete,res://fish/species/trout_golden/trout_golden.tres,res://fish/species/trout_golden/fish_trout_golden.png,common
-1106,trout_rainbow,implemented,active,complete,res://fish/species/trout_rainbow/trout_rainbow.tres,res://fish/species/trout_rainbow/fish_trout_rainbow.png,common
-1201,wolffish_atlantic,implemented,inactive,missing icon,res://fish/species/wolffish_atlantic/wolffish_atlantic.tres,,common
-1202,cusk,implemented,inactive,missing icon,res://fish/species/cusk/cusk.tres,,common
-1203,wolffish_spotted,implemented,inactive,missing icon,res://fish/species/wolffish_spotted/wolffish_spotted.tres,,common
-1301,herring_atlantic,implemented,inactive,missing icon,res://fish/species/herring_atlantic/herring_atlantic.tres,,common
-1302,capelin,implemented,inactive,missing icon,res://fish/species/capelin/capelin.tres,,common
-1303,sprat_european,implemented,inactive,missing icon,res://fish/species/sprat_european/sprat_european.tres,,common
-1304,herring_pacific,implemented,inactive,missing icon,res://fish/species/herring_pacific/herring_pacific.tres,,common
-1401,pollock_alaska,implemented,inactive,missing icon,res://fish/species/pollock_alaska/pollock_alaska.tres,,common
-1402,cod_atlantic,implemented,inactive,missing icon,res://fish/species/cod_atlantic/cod_atlantic.tres,,common
-1403,pollock_atlantic,implemented,inactive,missing icon,res://fish/species/pollock_atlantic/pollock_atlantic.tres,,common
-1404,sole_dover,implemented,inactive,missing icon,res://fish/species/sole_dover/sole_dover.tres,,common
-1405,haddock,implemented,inactive,missing icon,res://fish/species/haddock/haddock.tres,,common
-1406,cod_pacific,implemented,inactive,missing icon,res://fish/species/cod_pacific/cod_pacific.tres,,common
-1407,hake_pacific,implemented,inactive,missing icon,res://fish/species/hake_pacific/hake_pacific.tres,,common
-1408,hake_silver,implemented,inactive,missing icon,res://fish/species/hake_silver/hake_silver.tres,,common
-1501,nautilus_chambered,implemented,inactive,missing icon,res://fish/species/nautilus_chambered/nautilus_chambered.tres,,common
-1502,cuttlefish_common,implemented,inactive,missing icon,res://fish/species/cuttlefish_common/cuttlefish_common.tres,,common
-1503,cuttlefish_flamboyant,implemented,inactive,missing icon,res://fish/species/cuttlefish_flamboyant/cuttlefish_flamboyant.tres,,common
-1601,tilefish_blueline,implemented,inactive,missing icon,res://fish/species/tilefish_blueline/tilefish_blueline.tres,,common
-1602,tilefish_golden,implemented,inactive,missing icon,res://fish/species/tilefish_golden/tilefish_golden.tres,,common
-1603,ocean_perch_pacific,implemented,inactive,missing icon,res://fish/species/ocean_perch_pacific/ocean_perch_pacific.tres,,common
-1604,sea_bass_chilean,implemented,inactive,missing icon,res://fish/species/sea_bass_chilean/sea_bass_chilean.tres,,common
-1605,sablefish,implemented,inactive,missing icon,res://fish/species/sablefish/sablefish.tres,,common
-1606,wreckfish,implemented,inactive,missing icon,res://fish/species/wreckfish/wreckfish.tres,,common
-1701,oarfish_giant,implemented,inactive,missing icon,res://fish/species/oarfish_giant/oarfish_giant.tres,,common
-1702,coelacanth_west_indian,implemented,inactive,missing icon,res://fish/species/coelacanth_west_indian/coelacanth_west_indian.tres,,common
-1801,anglerfish_black_seadevil,implemented,inactive,missing icon,res://fish/species/anglerfish_black_seadevil/anglerfish_black_seadevil.tres,,common
-1802,fangtooth_common,implemented,inactive,missing icon,res://fish/species/fangtooth_common/fangtooth_common.tres,,common
-1803,gulper_eel,implemented,inactive,missing icon,res://fish/species/gulper_eel/gulper_eel.tres,,common
-1804,barreleye_pacific,implemented,inactive,missing icon,res://fish/species/barreleye_pacific/barreleye_pacific.tres,,common
-1805,blobfish_smooth_head,implemented,inactive,missing icon,res://fish/species/blobfish_smooth_head/blobfish_smooth_head.tres,,common
-1806,lanternfish_spotted,implemented,inactive,missing icon,res://fish/species/lanternfish_spotted/lanternfish_spotted.tres,,common
-1901,sea_star_crown_of_thorns,implemented,inactive,missing icon,res://fish/species/sea_star_crown_of_thorns/sea_star_crown_of_thorns.tres,,common
-1902,sea_cucumber_giant_california,implemented,inactive,missing icon,res://fish/species/sea_cucumber_giant_california/sea_cucumber_giant_california.tres,,common
-1903,urchin_purple,implemented,inactive,missing icon,res://fish/species/urchin_purple/urchin_purple.tres,,common
-1904,sand_dollar,implemented,inactive,missing icon,res://fish/species/sand_dollar/sand_dollar.tres,,common
-1905,sea_star_sunflower,implemented,inactive,missing icon,res://fish/species/sea_star_sunflower/sea_star_sunflower.tres,,common
-2001,axolotl,implemented,inactive,missing icon,res://fish/species/axolotl/axolotl.tres,,common
-2002,hellbender,implemented,inactive,missing icon,res://fish/species/hellbender/hellbender.tres,,common
-2101,guppy,implemented,inactive,missing icon,res://fish/species/guppy/guppy.tres,,common
-2102,tetra_neon,implemented,inactive,missing icon,res://fish/species/tetra_neon/tetra_neon.tres,,common
-2103,danio_zebra,implemented,inactive,missing icon,res://fish/species/danio_zebra/danio_zebra.tres,,common
-2201,largemouth_bass,implemented,inactive,missing icon,res://fish/species/largemouth_bass/largemouth_bass.tres,,common
-2202,smallmouth_bass,implemented,inactive,missing icon,res://fish/species/smallmouth_bass/smallmouth_bass.tres,,common
-2301,buffalo_bigmouth,implemented,inactive,missing icon,res://fish/species/buffalo_bigmouth/buffalo_bigmouth.tres,,common
-2302,redhorse_golden,implemented,inactive,missing icon,res://fish/species/redhorse_golden/redhorse_golden.tres,,common
-2303,quillback,implemented,inactive,missing icon,res://fish/species/quillback/quillback.tres,,common
-2304,buffalo_smallmouth,implemented,inactive,missing icon,res://fish/species/buffalo_smallmouth/buffalo_smallmouth.tres,,common
-2305,sucker_white,implemented,inactive,missing icon,res://fish/species/sucker_white/sucker_white.tres,,common
-2401,bullhead_black,implemented,inactive,missing icon,res://fish/species/bullhead_black/bullhead_black.tres,,common
-2402,catfish_blue,implemented,active,complete,res://fish/species/catfish_blue/catfish_blue.tres,res://fish/species/catfish_blue/fish_catfish_blue.png,bass
-2403,bullhead_brown,implemented,inactive,missing icon,res://fish/species/bullhead_brown/bullhead_brown.tres,,common
-2404,catfish_channel,implemented,active,complete,res://fish/species/catfish_channel/catfish_channel.tres,res://fish/species/catfish_channel/fish_catfish_channel.png,common
-2405,catfish_flathead,implemented,active,complete,res://fish/species/catfish_flathead/catfish_flathead.tres,res://fish/species/catfish_flathead/fish_catfish_flathead.png,bass
-2406,madtom_tadpole,implemented,inactive,missing icon,res://fish/species/madtom_tadpole/madtom_tadpole.tres,,common
-2407,catfish_white,implemented,active,complete,res://fish/species/catfish_white/catfish_white.tres,res://fish/species/catfish_white/fish_catfish_white.png,common
-2408,bullhead_yellow,implemented,inactive,missing icon,res://fish/species/bullhead_yellow/bullhead_yellow.tres,,common
-2501,barbel_common,implemented,inactive,missing icon,res://fish/species/barbel_common/barbel_common.tres,,common
-2502,bream_common,implemented,inactive,missing icon,res://fish/species/bream_common/bream_common.tres,,common
-2503,carp,implemented,active,complete,res://fish/species/carp/carp.tres,res://fish/species/carp/fish_carp_common.png,carp
-2504,roach_common,implemented,inactive,missing icon,res://fish/species/roach_common/roach_common.tres,,common
-2505,chub_european,implemented,active,complete,res://fish/species/chub_european/chub_european.tres,res://fish/species/chub_european/fish_chub_european.png,common
-2506,fallfish,implemented,inactive,missing icon,res://fish/species/fallfish/fallfish.tres,,common
-2507,ide,implemented,inactive,missing icon,res://fish/species/ide/ide.tres,,common
-2508,rudd,implemented,inactive,missing icon,res://fish/species/rudd/rudd.tres,,common
-2509,tench,implemented,inactive,missing icon,res://fish/species/tench/tench.tres,,common
-2601,crayfish_red_swamp,implemented,inactive,missing icon,res://fish/species/crayfish_red_swamp/crayfish_red_swamp.tres,,common
-2701,gar_alligator,implemented,inactive,missing icon,res://fish/species/gar_alligator/gar_alligator.tres,,common
-2702,arapaima,implemented,inactive,missing icon,res://fish/species/arapaima/arapaima.tres,,common
-2703,tigerfish_goliath,implemented,inactive,missing icon,res://fish/species/tigerfish_goliath/tigerfish_goliath.tres,,common
-2704,muskellunge,implemented,inactive,missing icon,res://fish/species/muskellunge/muskellunge.tres,,common
-2801,shiner_common,implemented,inactive,missing icon,res://fish/species/shiner_common/shiner_common.tres,,common
-2802,chub_creek,implemented,inactive,missing icon,res://fish/species/chub_creek/chub_creek.tres,,common
-2803,shiner_emerald,implemented,inactive,missing icon,res://fish/species/shiner_emerald/shiner_emerald.tres,,common
-2804,minnow_fathead,implemented,inactive,missing icon,res://fish/species/minnow_fathead/minnow_fathead.tres,,common
-2805,chub_flame,implemented,active,complete,res://fish/species/chub_flame/chub_flame.tres,res://fish/species/chub_flame/fish_chub_flame.png,common
-2806,shiner_golden,implemented,inactive,missing icon,res://fish/species/shiner_golden/shiner_golden.tres,,common
-2807,chub_lake,implemented,active,complete,res://fish/species/chub_lake/chub_lake.tres,res://fish/species/chub_lake/fish_chub_lake.png,common
-2901,electric_eel,implemented,inactive,missing icon,res://fish/species/electric_eel/electric_eel.tres,,common
-2902,freshwater_drum,implemented,inactive,missing icon,res://fish/species/freshwater_drum/freshwater_drum.tres,,common
-2903,goldeye,implemented,inactive,missing icon,res://fish/species/goldeye/goldeye.tres,,common
-2904,mooneye,implemented,inactive,missing icon,res://fish/species/mooneye/mooneye.tres,,common
-2905,loach_weather,implemented,inactive,missing icon,res://fish/species/loach_weather/loach_weather.tres,,common
-3001,goldfish_bubbleeye,implemented,active,complete,res://fish/species/goldfish_bubbleeye/goldfish_bubbleeye.tres,res://fish/species/goldfish_bubbleeye/fish_goldfish_bubbleeye.png,common
-3002,goldfish,implemented,active,complete,res://fish/species/goldfish/goldfish.tres,res://fish/species/goldfish/fish_goldfish.png,common
-3003,koi,implemented,inactive,missing icon,res://fish/species/koi/koi.tres,,common
-3101,crappie_black,implemented,inactive,missing icon,res://fish/species/crappie_black/crappie_black.tres,,common
-3102,bluegill,implemented,active,complete,res://fish/species/bluegill/bluegill.tres,res://fish/species/bluegill/fish_bluegill.png,common
-3103,green_sunfish,implemented,inactive,missing icon,res://fish/species/green_sunfish/green_sunfish.tres,,common
-3104,pumpkinseed,implemented,inactive,missing icon,res://fish/species/pumpkinseed/pumpkinseed.tres,,common
-3105,redear_sunfish,implemented,inactive,missing icon,res://fish/species/redear_sunfish/redear_sunfish.tres,,common
-3106,warmouth,implemented,inactive,missing icon,res://fish/species/warmouth/warmouth.tres,,common
-3107,crappie_white,implemented,inactive,missing icon,res://fish/species/crappie_white/crappie_white.tres,,common
-3201,yellow_perch,implemented,inactive,missing icon,res://fish/species/yellow_perch/yellow_perch.tres,,common
-3301,pickerel_chain,implemented,inactive,missing icon,res://fish/species/pickerel_chain/pickerel_chain.tres,,common
-3302,pickerel_grass,implemented,inactive,missing icon,res://fish/species/pickerel_grass/pickerel_grass.tres,,common
-3303,pike_northern,implemented,inactive,missing icon,res://fish/species/pike_northern/pike_northern.tres,,common
-3304,sauger,implemented,active,complete,res://fish/species/sauger/sauger.tres,res://fish/species/sauger/fish_sauger.png,common
-3305,saugeye,implemented,active,complete,res://fish/species/saugeye/saugeye.tres,res://fish/species/saugeye/fish_saugeye.png,common
-3306,walleye,implemented,active,complete,res://fish/species/walleye/walleye.tres,res://fish/species/walleye/fish_walleye.png,common
-3401,goby_round,implemented,active,complete,res://fish/species/goby_round/goby_round.tres,res://fish/species/goby_round/fish_goby_round.png,common
-3501,jelly_crystal,implemented,inactive,missing icon,res://fish/species/jelly_crystal/jelly_crystal.tres,,common
-3502,jelly_lions_mane,implemented,inactive,missing icon,res://fish/species/jelly_lions_mane/jelly_lions_mane.tres,,common
-3503,jelly_moon,implemented,inactive,missing icon,res://fish/species/jelly_moon/jelly_moon.tres,,common
-3504,jelly_pacific_sea_nettle,implemented,inactive,missing icon,res://fish/species/jelly_pacific_sea_nettle/jelly_pacific_sea_nettle.tres,,common
-3505,jelly_sea_wasp,implemented,inactive,missing icon,res://fish/species/jelly_sea_wasp/jelly_sea_wasp.tres,,common
-3601,pompano_african,implemented,inactive,missing icon,res://fish/species/pompano_african/pompano_african.tres,,common
-3602,cobia,implemented,inactive,missing icon,res://fish/species/cobia/cobia.tres,,common
-3603,trevally_giant,implemented,inactive,missing icon,res://fish/species/trevally_giant/trevally_giant.tres,,common
-3604,amberjack_greater,implemented,inactive,missing icon,res://fish/species/amberjack_greater/amberjack_greater.tres,,common
-3605,amberjack_lesser,implemented,inactive,missing icon,res://fish/species/amberjack_lesser/amberjack_lesser.tres,,common
-3701,halibut_atlantic,implemented,inactive,missing icon,res://fish/species/halibut_atlantic/halibut_atlantic.tres,,common
-3702,halibut_pacific,implemented,inactive,missing icon,res://fish/species/halibut_pacific/halibut_pacific.tres,,common
-3703,turbot,implemented,inactive,missing icon,res://fish/species/turbot/turbot.tres,,common
-3801,lobster_american,implemented,inactive,missing icon,res://fish/species/lobster_american/lobster_american.tres,,common
-3802,krill_antarctic,implemented,inactive,missing icon,res://fish/species/krill_antarctic/krill_antarctic.tres,,common
-3803,lobster_caribbean_spiny,implemented,inactive,missing icon,res://fish/species/lobster_caribbean_spiny/lobster_caribbean_spiny.tres,,common
-3804,shrimp_peacock_mantis,implemented,inactive,missing icon,res://fish/species/shrimp_peacock_mantis/shrimp_peacock_mantis.tres,,common
-3805,shrimp_skunk_cleaner,implemented,inactive,missing icon,res://fish/species/shrimp_skunk_cleaner/shrimp_skunk_cleaner.tres,,common
-3901,crab_ghost,implemented,inactive,missing icon,res://fish/species/crab_ghost/crab_ghost.tres,,common
-3902,crab_blue,implemented,inactive,missing icon,res://fish/species/crab_blue/crab_blue.tres,,common
-3903,crab_dungeness,implemented,inactive,missing icon,res://fish/species/crab_dungeness/crab_dungeness.tres,,common
-3904,crab_japanese_spider,implemented,inactive,missing icon,res://fish/species/crab_japanese_spider/crab_japanese_spider.tres,,common
-3905,crab_red_king,implemented,inactive,missing icon,res://fish/species/crab_red_king/crab_red_king.tres,,common
-4001,eel_american,implemented,inactive,missing icon,res://fish/species/eel_american/eel_american.tres,,common
-4002,eel_european,implemented,inactive,missing icon,res://fish/species/eel_european/eel_european.tres,,common
-4101,smelt_rainbow,implemented,inactive,missing icon,res://fish/species/smelt_rainbow/smelt_rainbow.tres,,common
-4201,smelt_eulachon,implemented,inactive,missing icon,res://fish/species/smelt_eulachon/smelt_eulachon.tres,,common
-4301,sturgeon_white,implemented,inactive,missing icon,res://fish/species/sturgeon_white/sturgeon_white.tres,,common
-4401,salmon_atlantic,implemented,active,complete,res://fish/species/salmon_atlantic/salmon_atlantic.tres,res://fish/species/salmon_atlantic/fish_salmon_atlantic.png,bass
-4402,salmon_chum,implemented,active,complete,res://fish/species/salmon_chum/salmon_chum.tres,res://fish/species/salmon_chum/fish_salmon_chum.png,bass
-4403,salmon_coho,implemented,active,complete,res://fish/species/salmon_coho/salmon_coho.tres,res://fish/species/salmon_coho/fish_salmon_coho.png,bass
-4404,salmon_pink,implemented,active,complete,res://fish/species/salmon_pink/salmon_pink.tres,res://fish/species/salmon_pink/fish_salmon_pink.png,bass
-4405,salmon_sockeye,implemented,active,complete,res://fish/species/salmon_sockeye/salmon_sockeye.tres,res://fish/species/salmon_sockeye/fish_salmon_sockeye.png,bass
-4501,trout_steelhead,implemented,active,complete,res://fish/species/trout_steelhead/trout_steelhead.tres,res://fish/species/trout_steelhead/fish_trout_steelhead.png,common
-4601,marlin_black,implemented,active,complete,res://fish/species/marlin_black/marlin_black.tres,res://fish/species/marlin_black/fish_marlin_black.png,common
-4602,marlin_blue,implemented,active,complete,res://fish/species/marlin_blue/marlin_blue.tres,res://fish/species/marlin_blue/fish_marlin_blue.png,common
-4603,sailfish,implemented,active,complete,res://fish/species/sailfish/sailfish.tres,res://fish/species/sailfish/fish_sailfish.png,common
-4604,swordfish,implemented,active,complete,res://fish/species/swordfish/swordfish.tres,res://fish/species/swordfish/fish_swordfish.png,common
-4605,marlin_white,implemented,active,complete,res://fish/species/marlin_white/marlin_white.tres,res://fish/species/marlin_white/fish_marlin_white.png,common
-4701,mahi_mahi,implemented,inactive,missing icon,res://fish/species/mahi_mahi/mahi_mahi.tres,,common
-4702,opah,implemented,inactive,missing icon,res://fish/species/opah/opah.tres,,common
-4703,wahoo,implemented,inactive,missing icon,res://fish/species/wahoo/wahoo.tres,,common
-4801,tuna_albacore,implemented,active,complete,res://fish/species/tuna_albacore/tuna_albacore.tres,res://fish/species/tuna_albacore/fish_tuna_albacore.png,bass
-4802,tuna_bigeye,implemented,active,complete,res://fish/species/tuna_bigeye/tuna_bigeye.tres,res://fish/species/tuna_bigeye/fish_tuna_bigeye.png,bass
-4803,tuna_bluefin,implemented,active,complete,res://fish/species/tuna_bluefin/tuna_bluefin.tres,res://fish/species/tuna_bluefin/fish_tuna_bluefin.png,bass
-4804,tuna_skipjack,implemented,active,complete,res://fish/species/tuna_skipjack/tuna_skipjack.tres,res://fish/species/tuna_skipjack/fish_tuna_skipjack.png,bass
-4805,tuna_yellowfin,implemented,active,complete,res://fish/species/tuna_yellowfin/tuna_yellowfin.tres,res://fish/species/tuna_yellowfin/fish_tuna_yellowfin.png,bass
-4901,octopus_common,implemented,inactive,missing icon,res://fish/species/octopus_common/octopus_common.tres,,common
-4902,octopus_day,implemented,inactive,missing icon,res://fish/species/octopus_day/octopus_day.tres,,common
-4903,octopus_giant_pacific,implemented,inactive,missing icon,res://fish/species/octopus_giant_pacific/octopus_giant_pacific.tres,,common
-4904,octopus_greater_blue_ringed,implemented,inactive,missing icon,res://fish/species/octopus_greater_blue_ringed/octopus_greater_blue_ringed.tres,,common
-5001,sunfish,implemented,active,complete,res://fish/species/sunfish/sunfish.tres,res://fish/species/sunfish/fish_sunfish.png,common
-5101,skate_barndoor,implemented,inactive,missing icon,res://fish/species/skate_barndoor/skate_barndoor.tres,,common
-5102,manta_ray_giant,implemented,inactive,missing icon,res://fish/species/manta_ray_giant/manta_ray_giant.tres,,common
-5103,sawfish_largetooth,implemented,inactive,missing icon,res://fish/species/sawfish_largetooth/sawfish_largetooth.tres,,common
-5104,stingray_ocellate_river,implemented,inactive,missing icon,res://fish/species/stingray_ocellate_river/stingray_ocellate_river.tres,,common
-5105,stingray_southern,implemented,inactive,missing icon,res://fish/species/stingray_southern/stingray_southern.tres,,common
-5201,lionfish_red,implemented,inactive,missing icon,res://fish/species/lionfish_red/lionfish_red.tres,,common
-5202,scorpionfish_red,implemented,inactive,missing icon,res://fish/species/scorpionfish_red/scorpionfish_red.tres,,common
-5301,moray_green,implemented,inactive,missing icon,res://fish/species/moray_green/moray_green.tres,,common
-5401,flounder_peacock,implemented,inactive,missing icon,res://fish/species/flounder_peacock/flounder_peacock.tres,,common
-5501,surgeonfish_blue,implemented,inactive,missing icon,res://fish/species/surgeonfish_blue/surgeonfish_blue.tres,,common
-5502,parrotfish_rainbow,implemented,inactive,missing icon,res://fish/species/parrotfish_rainbow/parrotfish_rainbow.tres,,common
-5503,parrotfish_stoplight,implemented,inactive,missing icon,res://fish/species/parrotfish_stoplight/parrotfish_stoplight.tres,,common
-5504,tang_yellow,implemented,inactive,missing icon,res://fish/species/tang_yellow/tang_yellow.tres,,common
-5601,trumpetfish_atlantic,implemented,inactive,missing icon,res://fish/species/trumpetfish_atlantic/trumpetfish_atlantic.tres,,common
-5602,triggerfish_gray,implemented,inactive,missing icon,res://fish/species/triggerfish_gray/triggerfish_gray.tres,,common
-5603,pufferfish_guineafowl,implemented,inactive,missing icon,res://fish/species/pufferfish_guineafowl/pufferfish_guineafowl.tres,,common
-5604,cowfish_longhorn,implemented,inactive,missing icon,res://fish/species/cowfish_longhorn/cowfish_longhorn.tres,,common
-5605,triggerfish_queen,implemented,inactive,missing icon,res://fish/species/triggerfish_queen/triggerfish_queen.tres,,common
-5606,cornetfish_red,implemented,inactive,missing icon,res://fish/species/cornetfish_red/cornetfish_red.tres,,common
-5607,porcupinefish_spotted,implemented,inactive,missing icon,res://fish/species/porcupinefish_spotted/porcupinefish_spotted.tres,,common
-5608,boxfish_yellow,implemented,inactive,missing icon,res://fish/species/boxfish_yellow/boxfish_yellow.tres,,common
-5701,butterflyfish_copperband,implemented,inactive,missing icon,res://fish/species/butterflyfish_copperband/butterflyfish_copperband.tres,,common
-5702,clownfish_ocellaris,implemented,inactive,missing icon,res://fish/species/clownfish_ocellaris/clownfish_ocellaris.tres,,common
-5703,angelfish_queen,implemented,inactive,missing icon,res://fish/species/angelfish_queen/angelfish_queen.tres,,common
-5801,grouper_atlantic_goliath,implemented,inactive,missing icon,res://fish/species/grouper_atlantic_goliath/grouper_atlantic_goliath.tres,,common
-5802,trevally_bluefin,implemented,inactive,missing icon,res://fish/species/trevally_bluefin/trevally_bluefin.tres,,common
-5803,grouper_giant,implemented,inactive,missing icon,res://fish/species/grouper_giant/grouper_giant.tres,,common
-5804,barracuda_great,implemented,inactive,missing icon,res://fish/species/barracuda_great/barracuda_great.tres,,common
-5805,grouper_gulf,implemented,active,complete,res://fish/species/grouper_gulf/grouper_gulf.tres,res://fish/species/grouper_gulf/fish_grouper_gulf.png,common
-5806,grouper_nassau,implemented,inactive,missing icon,res://fish/species/grouper_nassau/grouper_nassau.tres,,common
-5807,grouper_red,implemented,active,complete,res://fish/species/grouper_red/grouper_red.tres,res://fish/species/grouper_red/fish_grouper_red.png,common
-5901,chromis_blue_green,implemented,inactive,missing icon,res://fish/species/chromis_blue_green/chromis_blue_green.tres,,common
-5902,grunt_bluestriped,implemented,inactive,missing icon,res://fish/species/grunt_bluestriped/grunt_bluestriped.tres,,common
-5903,grunt_french,implemented,inactive,missing icon,res://fish/species/grunt_french/grunt_french.tres,,common
-5904,damselfish_sergeant_major,implemented,inactive,missing icon,res://fish/species/damselfish_sergeant_major/damselfish_sergeant_major.tres,,common
-6001,snapper_lane,implemented,active,complete,res://fish/species/snapper_lane/snapper_lane.tres,res://fish/species/snapper_lane/fish_snapper_lane.png,common
-6002,snapper_mangrove,implemented,active,complete,res://fish/species/snapper_mangrove/snapper_mangrove.tres,res://fish/species/snapper_mangrove/fish_snapper_mangrove.png,common
-6003,snapper_mutton,implemented,active,complete,res://fish/species/snapper_mutton/snapper_mutton.tres,res://fish/species/snapper_mutton/fish_snapper_mutton.png,common
-6004,snapper_red,implemented,active,complete,res://fish/species/snapper_red/snapper_red.tres,res://fish/species/snapper_red/fish_snapper_red.png,common
-6101,wrasse_ballan,implemented,inactive,missing icon,res://fish/species/wrasse_ballan/wrasse_ballan.tres,,common
-6102,wrasse_cleaner,implemented,inactive,missing icon,res://fish/species/wrasse_cleaner/wrasse_cleaner.tres,,common
-6103,hogfish,implemented,inactive,missing icon,res://fish/species/hogfish/hogfish.tres,,common
-6201,turtle_green,implemented,inactive,missing icon,res://fish/species/turtle_green/turtle_green.tres,,common
-6202,turtle_hawksbill,implemented,inactive,missing icon,res://fish/species/turtle_hawksbill/turtle_hawksbill.tres,,common
-6203,turtle_leatherback,implemented,inactive,missing icon,res://fish/species/turtle_leatherback/turtle_leatherback.tres,,common
-6204,turtle_loggerhead,implemented,inactive,missing icon,res://fish/species/turtle_loggerhead/turtle_loggerhead.tres,,common
-6301,seadragon_leafy,implemented,inactive,missing icon,res://fish/species/seadragon_leafy/seadragon_leafy.tres,,common
-6302,seadragon_weedy,implemented,inactive,missing icon,res://fish/species/seadragon_weedy/seadragon_weedy.tres,,common
-6401,oyster_black_lip_pearl,implemented,inactive,missing icon,res://fish/species/oyster_black_lip_pearl/oyster_black_lip_pearl.tres,,common
-6402,clam_giant,implemented,inactive,missing icon,res://fish/species/clam_giant/clam_giant.tres,,common
-6403,clam_geoduck,implemented,inactive,missing icon,res://fish/species/clam_geoduck/clam_geoduck.tres,,common
-6404,conch_queen,implemented,inactive,missing icon,res://fish/species/conch_queen/conch_queen.tres,,common
-6405,abalone_red,implemented,inactive,missing icon,res://fish/species/abalone_red/abalone_red.tres,,common
-6501,squid_bigfin_reef,implemented,inactive,missing icon,res://fish/species/squid_bigfin_reef/squid_bigfin_reef.tres,,common
-6502,squid_colossal,implemented,inactive,missing icon,res://fish/species/squid_colossal/squid_colossal.tres,,common
-6503,squid_giant,implemented,inactive,missing icon,res://fish/species/squid_giant/squid_giant.tres,,common
-6504,squid_humboldt,implemented,inactive,missing icon,res://fish/species/squid_humboldt/squid_humboldt.tres,,common
-6601,halfbeak_ballyhoo,implemented,inactive,missing icon,res://fish/species/halfbeak_ballyhoo/halfbeak_ballyhoo.tres,,common
-6602,needlefish_hound,implemented,inactive,missing icon,res://fish/species/needlefish_hound/needlefish_hound.tres,,common
-6603,flyingfish_tropical,implemented,inactive,missing icon,res://fish/species/flyingfish_tropical/flyingfish_tropical.tres,,common
-6701,bass,implemented,active,complete,res://fish/species/bass/bass.tres,res://fish/species/bass/fish_bass_striped.png,bass
-6801,croaker_atlantic,implemented,inactive,missing icon,res://fish/species/croaker_atlantic/croaker_atlantic.tres,,common
-6802,black_drum,implemented,inactive,missing icon,res://fish/species/black_drum/black_drum.tres,,common
-6803,red_drum,implemented,inactive,missing icon,res://fish/species/red_drum/red_drum.tres,,common
-6804,mullet_striped,implemented,inactive,missing icon,res://fish/species/mullet_striped/mullet_striped.tres,,common
-6805,weakfish,implemented,inactive,missing icon,res://fish/species/weakfish/weakfish.tres,,common
-6806,white_perch,implemented,inactive,missing icon,res://fish/species/white_perch/white_perch.tres,,common
-6807,croaker_yellowfin,implemented,inactive,missing icon,res://fish/species/croaker_yellowfin/croaker_yellowfin.tres,,common
-6901,rockfish_black,implemented,inactive,missing icon,res://fish/species/rockfish_black/rockfish_black.tres,,common
-6902,sea_bass_black,implemented,inactive,missing icon,res://fish/species/sea_bass_black/sea_bass_black.tres,,common
-6903,rockfish_canary,implemented,inactive,missing icon,res://fish/species/rockfish_canary/rockfish_canary.tres,,common
-6904,porgy_scup,implemented,inactive,missing icon,res://fish/species/porgy_scup/porgy_scup.tres,,common
-6905,sheepshead,implemented,inactive,missing icon,res://fish/species/sheepshead/sheepshead.tres,,common
-6906,tautog,implemented,inactive,missing icon,res://fish/species/tautog/tautog.tres,,common
-6907,rockfish_yelloweye,implemented,inactive,missing icon,res://fish/species/rockfish_yelloweye/rockfish_yelloweye.tres,,common
-7001,lingcod,implemented,inactive,missing icon,res://fish/species/lingcod/lingcod.tres,,common
-7101,milkfish,implemented,inactive,missing icon,res://fish/species/milkfish/milkfish.tres,,common
-7201,roosterfish,implemented,inactive,missing icon,res://fish/species/roosterfish/roosterfish.tres,,common
-7301,tarpon_atlantic,implemented,inactive,missing icon,res://fish/species/tarpon_atlantic/tarpon_atlantic.tres,,common
-7302,barramundi,implemented,inactive,missing icon,res://fish/species/barramundi/barramundi.tres,,common
-7303,snook_common,implemented,inactive,missing icon,res://fish/species/snook_common/snook_common.tres,,common
-7401,bonefish,implemented,inactive,missing icon,res://fish/species/bonefish/bonefish.tres,,common
-7402,pompano_florida,implemented,inactive,missing icon,res://fish/species/pompano_florida/pompano_florida.tres,,common
-7403,ladyfish,implemented,inactive,missing icon,res://fish/species/ladyfish/ladyfish.tres,,common
-7404,permit,implemented,inactive,missing icon,res://fish/species/permit/permit.tres,,common
-7501,knifefish_clown,implemented,inactive,missing icon,res://fish/species/knifefish_clown/knifefish_clown.tres,,common
-7502,loach_clown,implemented,inactive,missing icon,res://fish/species/loach_clown/loach_clown.tres,,common
-7503,discus,implemented,inactive,missing icon,res://fish/species/discus/discus.tres,,common
-7504,angelfish_freshwater,implemented,inactive,missing icon,res://fish/species/angelfish_freshwater/angelfish_freshwater.tres,,common
-7505,gourami_giant,implemented,inactive,missing icon,res://fish/species/gourami_giant/gourami_giant.tres,,common
-7506,tilapia_nile,implemented,inactive,missing icon,res://fish/species/tilapia_nile/tilapia_nile.tres,,common
-7507,oscar,implemented,inactive,missing icon,res://fish/species/oscar/oscar.tres,,common
-7508,betta_siamese,implemented,inactive,missing icon,res://fish/species/betta_siamese/betta_siamese.tres,,common
-7509,tambaqui,implemented,inactive,missing icon,res://fish/species/tambaqui/tambaqui.tres,,common
-7510,catfish_walking,implemented,inactive,missing icon,res://fish/species/catfish_walking/catfish_walking.tres,,common
-7601,snakehead_giant,implemented,inactive,missing icon,res://fish/species/snakehead_giant/snakehead_giant.tres,,common
-7602,dorado_golden,implemented,inactive,missing icon,res://fish/species/dorado_golden/dorado_golden.tres,,common
-7603,mahseer_golden,implemented,inactive,missing icon,res://fish/species/mahseer_golden/mahseer_golden.tres,,common
-7604,peacock_bass,implemented,inactive,missing icon,res://fish/species/peacock_bass/peacock_bass.tres,,common
-7605,piranha_red_bellied,implemented,inactive,missing icon,res://fish/species/piranha_red_bellied/piranha_red_bellied.tres,,common
-7606,arowana_silver,implemented,inactive,missing icon,res://fish/species/arowana_silver/arowana_silver.tres,,common
-7701,whale_blue,implemented,inactive,missing icon,res://fish/species/whale_blue/whale_blue.tres,,common
-7702,dolphin_bottlenose,implemented,inactive,missing icon,res://fish/species/dolphin_bottlenose/dolphin_bottlenose.tres,,common
-7703,whale_humpback,implemented,inactive,missing icon,res://fish/species/whale_humpback/whale_humpback.tres,,common
-7704,whale_killer,implemented,inactive,missing icon,res://fish/species/whale_killer/whale_killer.tres,,common
-7705,whale_sperm,implemented,inactive,missing icon,res://fish/species/whale_sperm/whale_sperm.tres,,common
diff --git a/drawing/surface_drawing_canvas.gd b/drawing/surface_drawing_canvas.gd
deleted file mode 100644
index 01d34d7..0000000
--- a/drawing/surface_drawing_canvas.gd
+++ /dev/null
@@ -1,589 +0,0 @@
-class_name SurfaceDrawingCanvas
-extends Node3D
-
-const GUIDED_PIXEL_FILL: float = 0.88
-const SURFACE_OFFSET: float = 0.012
-const LAYER_OFFSET_STEP: float = 0.0002
-const SURFACE_SAMPLE_DEPTH: float = 0.45
-
-var canvas_id: String = ""
-var grid_width: int = 0
-var grid_height: int = 0
-var cell_size: float = 0.0
-var revision: int = -1
-var creator_fingerprint: String = ""
-var participant_fingerprints: Array[String] = []
-
-var _surface_origin: Vector3
-var _surface_normal: Vector3
-var _surface_tangent: Vector3
-var _surface_bitangent: Vector3
-var _cells: Dictionary[int, Dictionary] = {}
-var _cell_surface_transforms: Array[Transform3D] = []
-var _pixel_image: Image
-var _pixel_texture: ImageTexture
-var _pixel_instance: MeshInstance3D
-var _grid_instance: MeshInstance3D
-var _guide_visible: bool = true
-var _finalized: bool = false
-var _layer: int = 0
-var _stencil_requested_visible: bool = false
-var _relationships: PlayerRelationshipStore
-var _solid_surface_mask: int = 1
-
-
-func setup(
- data: Dictionary,
- relationships: PlayerRelationshipStore,
- solid_surface_mask: int = 1,
-) -> bool:
- if not SurfaceDrawingProtocol.validate_canvas_state(data):
- return false
- canvas_id = str(data["canvas_id"])
- grid_width = int(data["width"])
- grid_height = int(data["height"])
- cell_size = float(data["cell_size"])
- revision = int(data["revision"])
- creator_fingerprint = str(data["creator_fingerprint"])
- participant_fingerprints = _participants_from_state(data)
- _finalized = bool(data.get("finalized", false))
- _guide_visible = bool(data.get("guide_visible", true)) and not _finalized
- _layer = int(data.get("layer", 0))
- _surface_origin = SurfaceDrawingProtocol.array_to_vector(data["origin"])
- _surface_normal = SurfaceDrawingProtocol.array_to_vector(
- data["normal"]
- ).normalized()
- _surface_tangent = SurfaceDrawingProtocol.array_to_vector(
- data["tangent"]
- )
- _surface_tangent = (
- _surface_tangent - _surface_normal * _surface_tangent.dot(_surface_normal)
- ).normalized()
- if _surface_normal.is_zero_approx() or _surface_tangent.is_zero_approx():
- return false
- _surface_bitangent = _surface_normal.cross(_surface_tangent).normalized()
- _relationships = relationships
- _solid_surface_mask = solid_surface_mask
- _cells.clear()
- for cell_value: Variant in data["cells"]:
- var cell: Dictionary = cell_value
- _cells[_cell_key(int(cell["x"]), int(cell["y"]))] = cell.duplicate(true)
- _build_grid()
- _build_pixel_renderer()
- return true
-
-
-func apply_update(data: Dictionary) -> bool:
- if (
- not SurfaceDrawingProtocol.validate_canvas_update(data)
- or str(data["canvas_id"]) != canvas_id
- or int(data["revision"]) <= revision
- ):
- return false
- for edit_value: Variant in data["edits"]:
- var edit: Dictionary = edit_value
- var key: int = _cell_key(int(edit["x"]), int(edit["y"]))
- if str(edit["color_id"]).is_empty():
- _cells.erase(key)
- else:
- _cells[key] = edit.duplicate(true)
- participant_fingerprints = _participants_from_update(
- data, participant_fingerprints
- )
- revision = int(data["revision"])
- if is_hidden_by_relationship():
- _rebuild_pixel_image()
- else:
- _apply_pixel_edits(data["edits"])
- _refresh_grid_visibility()
- return true
-
-
-func apply_guide_update(data: Dictionary) -> bool:
- if (
- not SurfaceDrawingProtocol.validate_guide_update(data)
- or str(data["canvas_id"]) != canvas_id
- or int(data["revision"]) <= revision
- ):
- return false
- revision = int(data["revision"])
- participant_fingerprints = _participants_from_update(
- data, participant_fingerprints
- )
- _finalized = bool(data["finalized"])
- _guide_visible = bool(data["guide_visible"]) and not _finalized
- if _finalized:
- _destroy_grid()
- else:
- _refresh_grid_visibility()
- return true
-
-
-func refresh_relationship_visibility() -> void:
- _rebuild_pixel_image()
- if _pixel_instance != null:
- _pixel_instance.visible = not is_hidden_by_relationship()
- _refresh_grid_visibility()
-
-
-func is_hidden_by_relationship() -> bool:
- if _relationships == null:
- return false
- for fingerprint: String in participant_fingerprints:
- if _relationships.is_blocked(fingerprint):
- return true
- return false
-
-
-func set_stencil_visible(should_be_visible: bool) -> void:
- _stencil_requested_visible = should_be_visible
- _refresh_grid_visibility()
-
-
-func is_guide_visible() -> bool:
- return _guide_visible
-
-
-func is_finalized() -> bool:
- return _finalized
-
-
-func get_rendered_pixel_size() -> float:
- return cell_size
-
-
-func get_rendered_pixel_count() -> int:
- if is_hidden_by_relationship():
- return 0
- var count: int = 0
- for cell: Dictionary in _cells.values():
- if not _visible_cell_color(cell).is_equal_approx(Color.TRANSPARENT):
- count += 1
- return count
-
-
-func get_export_image() -> Image:
- if _pixel_image == null or _pixel_image.is_empty():
- return null
- return _pixel_image.duplicate() as Image
-
-
-func has_guide_geometry() -> bool:
- return _grid_instance != null and is_instance_valid(_grid_instance)
-
-
-func contains_world_point(world_point: Vector3, tolerance: float = 0.3) -> bool:
- var relative: Vector3 = world_point - _surface_origin
- if absf(relative.dot(_surface_normal)) > tolerance:
- return false
- var half_width: float = float(grid_width) * cell_size * 0.5
- var half_height: float = float(grid_height) * cell_size * 0.5
- var horizontal: float = relative.dot(_surface_tangent)
- var vertical: float = relative.dot(_surface_bitangent)
- return (
- horizontal >= -half_width
- and horizontal < half_width
- and vertical >= -half_height
- and vertical < half_height
- )
-
-
-func get_surface_plane_distance(world_point: Vector3) -> float:
- return absf((world_point - _surface_origin).dot(_surface_normal))
-
-
-func cell_at_world_point(world_point: Vector3) -> Vector2i:
- var relative: Vector3 = world_point - _surface_origin
- var half_width: float = float(grid_width) * cell_size * 0.5
- var half_height: float = float(grid_height) * cell_size * 0.5
- var x: int = floori(
- (relative.dot(_surface_tangent) + half_width) / cell_size
- )
- var y: int = floori(
- (relative.dot(_surface_bitangent) + half_height) / cell_size
- )
- if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
- return Vector2i(-1, -1)
- return Vector2i(x, y)
-
-
-func get_cell_world_position(x: int, y: int) -> Vector3:
- var horizontal: float = (
- (float(x) + 0.5 - float(grid_width) * 0.5) * cell_size
- )
- var vertical: float = (
- (float(y) + 0.5 - float(grid_height) * 0.5) * cell_size
- )
- return (
- _surface_origin
- + _surface_tangent * horizontal
- + _surface_bitangent * vertical
- )
-
-
-func get_surface_normal() -> Vector3:
- return _surface_normal
-
-
-func get_surface_tangent() -> Vector3:
- return _surface_tangent
-
-
-func get_surface_bitangent() -> Vector3:
- return _surface_bitangent
-
-
-func get_cell_surface_transform(
- x: int,
- y: int,
- fill: float = 0.94,
-) -> Transform3D:
- if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
- return Transform3D.IDENTITY
- var key: int = _cell_key(x, y)
- var local_transform: Transform3D = (
- _cell_surface_transforms[key]
- if key >= 0 and key < _cell_surface_transforms.size()
- else _sample_cell_transform(x, y, 1.0)
- )
- var scaled_basis: Basis = local_transform.basis
- var resolved_fill: float = clampf(fill, 0.05, 1.0)
- scaled_basis.x *= resolved_fill
- scaled_basis.y *= resolved_fill
- local_transform.basis = scaled_basis
- return global_transform * local_transform
-
-
-func get_authoritative_cells() -> Array[Dictionary]:
- var result: Array[Dictionary] = []
- for value: Dictionary in _cells.values():
- result.append(value.duplicate(true))
- result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
- var a_key: int = _cell_key(int(a["x"]), int(a["y"]))
- var b_key: int = _cell_key(int(b["x"]), int(b["y"]))
- return a_key < b_key
- )
- return result
-
-
-func _build_grid() -> void:
- if _grid_instance != null:
- _grid_instance.queue_free()
- _grid_instance = null
- if _finalized:
- return
- var immediate := ImmediateMesh.new()
- var material := StandardMaterial3D.new()
- material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
- material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
- material.albedo_color = Color(0.75, 0.94, 0.96, 0.38)
- material.no_depth_test = false
- immediate.surface_begin(Mesh.PRIMITIVE_LINES, material)
- var half_width: float = float(grid_width) * cell_size * 0.5
- var half_height: float = float(grid_height) * cell_size * 0.5
- var sampled_vertices: Dictionary[Vector2i, Dictionary] = {}
- for x: int in range(grid_width + 1):
- var horizontal: float = -half_width + float(x) * cell_size
- for y_segment: int in range(grid_height):
- var start_vertical: float = (
- -half_height + float(y_segment) * cell_size
- )
- _add_grid_vertex(
- immediate,
- _surface_origin + _surface_tangent * horizontal
- + _surface_bitangent * start_vertical,
- Vector2i(x, y_segment),
- sampled_vertices,
- )
- _add_grid_vertex(
- immediate,
- _surface_origin + _surface_tangent * horizontal
- + _surface_bitangent * (start_vertical + cell_size),
- Vector2i(x, y_segment + 1),
- sampled_vertices,
- )
- for y: int in range(grid_height + 1):
- var vertical: float = -half_height + float(y) * cell_size
- for x_segment: int in range(grid_width):
- var start_horizontal: float = (
- -half_width + float(x_segment) * cell_size
- )
- _add_grid_vertex(
- immediate,
- _surface_origin + _surface_tangent * start_horizontal
- + _surface_bitangent * vertical,
- Vector2i(x_segment, y),
- sampled_vertices,
- )
- _add_grid_vertex(
- immediate,
- _surface_origin
- + _surface_tangent * (start_horizontal + cell_size)
- + _surface_bitangent * vertical,
- Vector2i(x_segment + 1, y),
- sampled_vertices,
- )
- immediate.surface_end()
- _grid_instance = MeshInstance3D.new()
- _grid_instance.name = "PixelGrid"
- _grid_instance.mesh = immediate
- _grid_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
- add_child(_grid_instance)
- _refresh_grid_visibility()
-
-
-func _add_grid_vertex(
- immediate: ImmediateMesh,
- world_point: Vector3,
- grid_point: Vector2i,
- sampled_vertices: Dictionary[Vector2i, Dictionary],
-) -> void:
- var sampled: Dictionary = sampled_vertices.get(grid_point, {})
- if sampled.is_empty():
- sampled = _sample_surface(world_point)
- sampled_vertices[grid_point] = sampled
- var point: Vector3 = sampled["position"]
- var normal: Vector3 = sampled["normal"]
- immediate.surface_add_vertex(
- to_local(point + normal * (_surface_offset() * 1.5))
- )
-
-
-func _build_pixel_renderer() -> void:
- if _pixel_instance != null:
- _pixel_instance.queue_free()
- _pixel_instance = null
- var cell_count: int = grid_width * grid_height
- var vertices := PackedVector3Array()
- var texture_coordinates := PackedVector2Array()
- var indices := PackedInt32Array()
- vertices.resize(cell_count * 4)
- texture_coordinates.resize(cell_count * 4)
- indices.resize(cell_count * 6)
- _cell_surface_transforms.clear()
- _cell_surface_transforms.resize(cell_count)
- for y: int in range(grid_height):
- for x: int in range(grid_width):
- var cell_index: int = _cell_key(x, y)
- var surface_transform := _sample_cell_transform(x, y, 1.0)
- _cell_surface_transforms[cell_index] = surface_transform
- var center: Vector3 = surface_transform.origin
- var horizontal: Vector3 = surface_transform.basis.x * 0.5
- var vertical: Vector3 = surface_transform.basis.y * 0.5
- var vertex_offset: int = cell_index * 4
- vertices[vertex_offset] = center - horizontal - vertical
- vertices[vertex_offset + 1] = center + horizontal - vertical
- vertices[vertex_offset + 2] = center + horizontal + vertical
- vertices[vertex_offset + 3] = center - horizontal + vertical
- var image_row: int = _image_y(y)
- var u_min: float = float(x) / float(grid_width)
- var u_max: float = float(x + 1) / float(grid_width)
- var v_min: float = float(image_row) / float(grid_height)
- var v_max: float = float(image_row + 1) / float(grid_height)
- texture_coordinates[vertex_offset] = Vector2(u_min, v_max)
- texture_coordinates[vertex_offset + 1] = Vector2(u_max, v_max)
- texture_coordinates[vertex_offset + 2] = Vector2(u_max, v_min)
- texture_coordinates[vertex_offset + 3] = Vector2(u_min, v_min)
- var index_offset: int = cell_index * 6
- indices[index_offset] = vertex_offset
- indices[index_offset + 1] = vertex_offset + 1
- indices[index_offset + 2] = vertex_offset + 2
- indices[index_offset + 3] = vertex_offset
- indices[index_offset + 4] = vertex_offset + 2
- indices[index_offset + 5] = vertex_offset + 3
- _rebuild_pixel_image()
- var arrays: Array = []
- arrays.resize(Mesh.ARRAY_MAX)
- arrays[Mesh.ARRAY_VERTEX] = vertices
- arrays[Mesh.ARRAY_TEX_UV] = texture_coordinates
- arrays[Mesh.ARRAY_INDEX] = indices
- var mesh := ArrayMesh.new()
- mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
- var material := StandardMaterial3D.new()
- material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
- material.albedo_color = Color.WHITE
- material.albedo_texture = _pixel_texture
- material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST
- material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR
- material.alpha_scissor_threshold = 0.5
- material.cull_mode = BaseMaterial3D.CULL_DISABLED
- mesh.surface_set_material(0, material)
- _pixel_instance = MeshInstance3D.new()
- _pixel_instance.name = "ArtworkTexture"
- _pixel_instance.mesh = mesh
- _pixel_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
- add_child(_pixel_instance)
- _pixel_instance.visible = not is_hidden_by_relationship()
-
-
-func _rebuild_pixel_image() -> void:
- if (
- _pixel_image == null
- or _pixel_image.get_width() != grid_width
- or _pixel_image.get_height() != grid_height
- ):
- _pixel_image = Image.create(
- grid_width, grid_height, false, Image.FORMAT_RGBA8
- )
- _pixel_image.fill(Color.TRANSPARENT)
- if not is_hidden_by_relationship():
- for cell: Dictionary in _cells.values():
- _set_image_cell(int(cell["x"]), int(cell["y"]), cell)
- if _pixel_texture == null:
- _pixel_texture = ImageTexture.create_from_image(_pixel_image)
- else:
- _pixel_texture.update(_pixel_image)
-
-
-func _apply_pixel_edits(edits: Array) -> void:
- if _pixel_image == null or _pixel_texture == null:
- _rebuild_pixel_image()
- return
- for edit_value: Variant in edits:
- var edit: Dictionary = edit_value
- var x: int = int(edit["x"])
- var y: int = int(edit["y"])
- var cell: Dictionary = _cells.get(_cell_key(x, y), {})
- _set_image_cell(x, y, cell)
- _pixel_texture.update(_pixel_image)
-
-
-func _set_image_cell(x: int, y: int, cell: Dictionary) -> void:
- if (
- _pixel_image == null
- or x < 0
- or x >= grid_width
- or y < 0
- or y >= grid_height
- ):
- return
- _pixel_image.set_pixel(x, _image_y(y), _visible_cell_color(cell))
-
-
-func _visible_cell_color(cell: Dictionary) -> Color:
- if cell.is_empty():
- return Color.TRANSPARENT
- var color_id := StringName(str(cell.get("color_id", "")))
- return (
- SurfaceDrawingPalette.get_color(color_id)
- if SurfaceDrawingPalette.has_color(color_id)
- else Color.TRANSPARENT
- )
-
-
-func _image_y(cell_y: int) -> int:
- return grid_height - 1 - cell_y
-
-
-func _sample_cell_transform(
- x: int,
- y: int,
- fill: float = GUIDED_PIXEL_FILL,
-) -> Transform3D:
- var expected: Vector3 = get_cell_world_position(x, y)
- var sampled: Dictionary = _sample_surface(expected)
- var point: Vector3 = sampled["position"]
- var normal: Vector3 = sampled["normal"]
- var tangent: Vector3 = (
- _surface_tangent - normal * _surface_tangent.dot(normal)
- ).normalized()
- if tangent.is_zero_approx():
- tangent = normal.cross(Vector3.UP).normalized()
- if tangent.is_zero_approx():
- tangent = Vector3.RIGHT
- var bitangent: Vector3 = normal.cross(tangent).normalized()
- var pixel_size: float = cell_size * clampf(fill, 0.05, 1.0)
- var cell_basis := Basis(
- tangent * pixel_size,
- bitangent * pixel_size,
- normal,
- )
- return Transform3D(
- cell_basis,
- to_local(point + normal * _surface_offset()),
- )
-
-
-func _sample_surface(expected: Vector3) -> Dictionary:
- var point: Vector3 = expected
- var normal: Vector3 = _surface_normal
- var world: World3D = get_world_3d()
- if world != null:
- var query := PhysicsRayQueryParameters3D.create(
- expected + _surface_normal * SURFACE_SAMPLE_DEPTH,
- expected - _surface_normal * SURFACE_SAMPLE_DEPTH,
- _solid_surface_mask,
- )
- query.collide_with_areas = false
- query.collide_with_bodies = true
- var hit: Dictionary = world.direct_space_state.intersect_ray(query)
- if (
- not hit.is_empty()
- and hit.get("collider") is StaticBody3D
- ):
- var hit_normal: Vector3 = hit.get("normal", _surface_normal)
- if hit_normal.dot(_surface_normal) >= 0.35:
- var hit_position: Vector3 = hit.get("position", expected)
- point = hit_position
- normal = hit_normal.normalized()
- return {"position": point, "normal": normal}
-
-
-func _surface_offset() -> float:
- return SURFACE_OFFSET + float(_layer) * LAYER_OFFSET_STEP
-
-
-func _destroy_grid() -> void:
- if _grid_instance == null:
- return
- _grid_instance.queue_free()
- _grid_instance = null
-
-
-func _refresh_grid_visibility() -> void:
- if _grid_instance != null:
- _grid_instance.visible = (
- _stencil_requested_visible
- and _guide_visible
- and not is_hidden_by_relationship()
- )
-
-
-func _participants_from_state(data: Dictionary) -> Array[String]:
- var result: Array[String] = []
- _append_participant(result, str(data.get("creator_fingerprint", "")))
- for value: Variant in data.get("participant_fingerprints", []):
- _append_participant(result, str(value))
- for cell_value: Variant in data.get("cells", []):
- var cell: Dictionary = cell_value
- _append_participant(
- result, str(cell.get("author_fingerprint", ""))
- )
- return result
-
-
-func _participants_from_update(
- data: Dictionary,
- fallback: Array[String],
-) -> Array[String]:
- var result: Array[String] = fallback.duplicate()
- for value: Variant in data.get("participant_fingerprints", []):
- _append_participant(result, str(value))
- for edit_value: Variant in data.get("edits", []):
- var edit: Dictionary = edit_value
- _append_participant(
- result, str(edit.get("author_fingerprint", ""))
- )
- return result
-
-
-func _append_participant(result: Array[String], fingerprint: String) -> void:
- if (
- NetworkIdentityCrypto.valid_fingerprint(fingerprint)
- and fingerprint not in result
- ):
- result.append(fingerprint)
-
-
-func _cell_key(x: int, y: int) -> int:
- return y * grid_width + x
diff --git a/drawing/surface_drawing_canvas.gd.uid b/drawing/surface_drawing_canvas.gd.uid
deleted file mode 100644
index 9da19e3..0000000
--- a/drawing/surface_drawing_canvas.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cj2gq63l4mysl
diff --git a/drawing/surface_drawing_highlight.gdshader b/drawing/surface_drawing_highlight.gdshader
deleted file mode 100644
index 02dae33..0000000
--- a/drawing/surface_drawing_highlight.gdshader
+++ /dev/null
@@ -1,12 +0,0 @@
-shader_type spatial;
-render_mode unshaded, cull_disabled, depth_draw_never;
-
-uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest;
-
-
-void fragment() {
- vec3 surface_color = textureLod(screen_texture, SCREEN_UV, 0.0).rgb;
- vec3 inverted_color = vec3(1.0) - surface_color;
- ALBEDO = inverted_color;
- EMISSION = inverted_color;
-}
diff --git a/drawing/surface_drawing_highlight.gdshader.uid b/drawing/surface_drawing_highlight.gdshader.uid
deleted file mode 100644
index aebbc9f..0000000
--- a/drawing/surface_drawing_highlight.gdshader.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://k82me23ylsos
diff --git a/drawing/surface_drawing_palette.gd b/drawing/surface_drawing_palette.gd
deleted file mode 100644
index c8bfc8e..0000000
--- a/drawing/surface_drawing_palette.gd
+++ /dev/null
@@ -1,57 +0,0 @@
-class_name SurfaceDrawingPalette
-extends RefCounted
-
-const DEFAULT_COLOR_ID: StringName = &"chalk_white"
-
-# Stable IDs are the network and future progression boundary. New colors may
-# be appended without changing drawings created with an older palette.
-const COLORS: Array[Dictionary] = [
- {"id": &"chalk_white", "name": "Chalk white", "color": Color("f5eed9")},
- {"id": &"ocean_teal", "name": "Ocean teal", "color": Color("35b9c7")},
- {"id": &"coral", "name": "Coral", "color": Color("ef5b62")},
- {"id": &"sunny", "name": "Sunny", "color": Color("ffd166")},
- {"id": &"leaf", "name": "Leaf", "color": Color("46c878")},
- {"id": &"blue", "name": "Blue", "color": Color("5596f6")},
- {"id": &"violet", "name": "Violet", "color": Color("b176e8")},
- {"id": &"charcoal", "name": "Charcoal", "color": Color("28251f")},
-]
-
-
-static func has_color(color_id: StringName) -> bool:
- return not get_entry(color_id).is_empty()
-
-
-static func get_entry(color_id: StringName) -> Dictionary:
- for entry: Dictionary in COLORS:
- if StringName(entry.get("id", &"")) == color_id:
- return entry
- return {}
-
-
-static func get_color(color_id: StringName) -> Color:
- var entry: Dictionary = get_entry(color_id)
- var value: Variant = entry.get("color", Color.WHITE)
- return value if typeof(value) == TYPE_COLOR else Color.WHITE
-
-
-static func get_display_name(color_id: StringName) -> String:
- return str(get_entry(color_id).get("name", "Unknown"))
-
-
-static func get_color_ids() -> Array[StringName]:
- var result: Array[StringName] = []
- for entry: Dictionary in COLORS:
- result.append(StringName(entry.get("id", &"")))
- return result
-
-
-static func filter_unlocked_ids(
- unlocked_ids: Array[StringName],
-) -> Array[StringName]:
- var result: Array[StringName] = []
- for color_id: StringName in unlocked_ids:
- if has_color(color_id) and color_id not in result:
- result.append(color_id)
- if result.is_empty():
- result.append(DEFAULT_COLOR_ID)
- return result
diff --git a/drawing/surface_drawing_palette.gd.uid b/drawing/surface_drawing_palette.gd.uid
deleted file mode 100644
index f6ece03..0000000
--- a/drawing/surface_drawing_palette.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bk16cl6f3v887
diff --git a/drawing/surface_drawing_placement.gd b/drawing/surface_drawing_placement.gd
deleted file mode 100644
index bfa1abc..0000000
--- a/drawing/surface_drawing_placement.gd
+++ /dev/null
@@ -1,92 +0,0 @@
-class_name SurfaceDrawingPlacement
-extends RefCounted
-
-const SNAP_DISTANCE: float = 0.55
-const SNAP_PLANE_DISTANCE: float = 0.18
-const SNAP_NORMAL_DOT: float = 0.94
-
-
-static func resolve(
- origin: Vector3,
- normal: Vector3,
- fallback_tangent: Vector3,
- canvas_states: Array[Dictionary],
- requested_grid_size: int = SurfaceDrawingProtocol.DEFAULT_GRID_SIZE,
-) -> Dictionary:
- var surface_normal: Vector3 = normal.normalized()
- var surface_tangent: Vector3 = _projected_tangent(
- fallback_tangent, surface_normal
- )
- var best: Dictionary = {
- "origin": origin,
- "normal": surface_normal,
- "tangent": surface_tangent,
- "snapped": false,
- }
- var nearest_distance: float = SNAP_DISTANCE
- for state: Dictionary in canvas_states:
- if not SurfaceDrawingProtocol.validate_canvas_state(state):
- continue
- var anchor_normal: Vector3 = SurfaceDrawingProtocol.array_to_vector(
- state["normal"]
- ).normalized()
- if anchor_normal.dot(surface_normal) < SNAP_NORMAL_DOT:
- continue
- var anchor_origin: Vector3 = SurfaceDrawingProtocol.array_to_vector(
- state["origin"]
- )
- var relative: Vector3 = origin - anchor_origin
- if absf(relative.dot(anchor_normal)) > SNAP_PLANE_DISTANCE:
- continue
- var anchor_tangent: Vector3 = _projected_tangent(
- SurfaceDrawingProtocol.array_to_vector(state["tangent"]),
- anchor_normal,
- )
- var anchor_bitangent: Vector3 = anchor_normal.cross(
- anchor_tangent
- ).normalized()
- var width: float = float(state["width"]) * float(state["cell_size"])
- var height: float = float(state["height"]) * float(state["cell_size"])
- var requested_extent: float = (
- float(requested_grid_size) * SurfaceDrawingProtocol.CELL_SIZE
- )
- if width <= 0.0 or height <= 0.0:
- continue
- var horizontal_spacing: float = (width + requested_extent) * 0.5
- var vertical_spacing: float = (height + requested_extent) * 0.5
- var horizontal_step: int = roundi(
- relative.dot(anchor_tangent) / horizontal_spacing
- )
- var vertical_step: int = roundi(
- relative.dot(anchor_bitangent) / vertical_spacing
- )
- if horizontal_step == 0 and vertical_step == 0:
- continue
- var candidate: Vector3 = (
- anchor_origin
- + anchor_tangent * float(horizontal_step) * horizontal_spacing
- + anchor_bitangent * float(vertical_step) * vertical_spacing
- )
- var distance: float = candidate.distance_to(origin)
- if distance > nearest_distance:
- continue
- nearest_distance = distance
- best = {
- "origin": candidate,
- "normal": anchor_normal,
- "tangent": anchor_tangent,
- "snapped": true,
- }
- return best
-
-
-static func _projected_tangent(
- value: Vector3,
- normal: Vector3,
-) -> Vector3:
- var tangent: Vector3 = (value - normal * value.dot(normal)).normalized()
- if tangent.is_zero_approx():
- tangent = Vector3.UP.cross(normal).normalized()
- if tangent.is_zero_approx():
- tangent = Vector3.RIGHT
- return tangent
diff --git a/drawing/surface_drawing_placement.gd.uid b/drawing/surface_drawing_placement.gd.uid
deleted file mode 100644
index 9cf47f2..0000000
--- a/drawing/surface_drawing_placement.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dgb7stq5k85d0
diff --git a/drawing/surface_drawing_protocol.gd b/drawing/surface_drawing_protocol.gd
deleted file mode 100644
index ff132f8..0000000
--- a/drawing/surface_drawing_protocol.gd
+++ /dev/null
@@ -1,340 +0,0 @@
-class_name SurfaceDrawingProtocol
-extends RefCounted
-
-const CAPABILITY: StringName = &"surface_drawing_v2"
-const RELIABLE_CHANNEL: int = NetworkProtocol.DRAWING_RELIABLE_CHANNEL
-const GRID_SIZES: Array[int] = [16, 32, 64, 128]
-const DEFAULT_GRID_SIZE: int = 16
-const MAX_GRID_SIZE: int = 128
-# Compatibility aliases for callers that only need the default dimensions.
-const GRID_WIDTH: int = DEFAULT_GRID_SIZE
-const GRID_HEIGHT: int = DEFAULT_GRID_SIZE
-const CELL_SIZE: float = 0.075
-const MAX_ACTIVE_CANVASES: int = 24
-const MAX_CANVASES: int = 48
-const MAX_SESSION_GRID_CELLS: int = 49152
-const MAX_EDITS_PER_REQUEST: int = 16
-const MAX_CANVAS_ID_LENGTH: int = 64
-const MAX_REQUEST_ID_LENGTH: int = 64
-const MAX_STROKE_ID_LENGTH: int = 64
-const MAX_SESSION_ID_LENGTH: int = 96
-# Sessions support up to 128 simultaneous players. Keep enough bounded history
-# for a complete room turnover while never accepting unattributed edits.
-const MAX_PARTICIPANTS: int = 256
-const MAX_COORDINATE: float = 10000.0
-
-
-static func validate_canvas_request(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- return (
- _valid_common(value)
- and _valid_vector(value.get("origin"))
- and _valid_vector(value.get("normal"))
- and _valid_vector(value.get("tangent"))
- and typeof(value.get("width")) == TYPE_INT
- and int(value["width"]) in GRID_SIZES
- and typeof(value.get("height")) == TYPE_INT
- and int(value["height"]) == int(value["width"])
- and typeof(value.get("cell_size")) in [TYPE_FLOAT, TYPE_INT]
- and is_equal_approx(float(value["cell_size"]), CELL_SIZE)
- )
-
-
-static func validate_edit_request(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- if (
- not _valid_common(value)
- or typeof(value.get("canvas_id")) != TYPE_STRING
- or str(value["canvas_id"]).is_empty()
- or str(value["canvas_id"]).length() > MAX_CANVAS_ID_LENGTH
- or not _valid_stroke_id(value.get("stroke_id"))
- or typeof(value.get("brush_size")) != TYPE_INT
- or int(value["brush_size"]) < 1
- or int(value["brush_size"]) > 4
- or typeof(value.get("edits")) != TYPE_ARRAY
- ):
- return false
- var edits: Array = value["edits"]
- if edits.is_empty() or edits.size() > MAX_EDITS_PER_REQUEST:
- return false
- for edit_value: Variant in edits:
- if not validate_cell_edit(edit_value):
- return false
- return true
-
-
-static func validate_stamp_request(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- if (
- not _valid_common(value)
- or not _valid_vector(value.get("origin"))
- or not _valid_vector(value.get("normal"))
- or not _valid_vector(value.get("tangent"))
- or typeof(value.get("width")) != TYPE_INT
- or int(value["width"]) not in GRID_SIZES
- or typeof(value.get("height")) != TYPE_INT
- or int(value["height"]) != int(value["width"])
- or typeof(value.get("cell_size")) not in [TYPE_FLOAT, TYPE_INT]
- or not is_equal_approx(float(value["cell_size"]), CELL_SIZE)
- or typeof(value.get("pixels")) != TYPE_PACKED_BYTE_ARRAY
- ):
- return false
- var pixels: PackedByteArray = value["pixels"]
- var expected_size: int = int(value["width"]) * int(value["height"])
- if pixels.size() != expected_size:
- return false
- var maximum_palette_index: int = SurfaceDrawingPalette.COLORS.size()
- var painted_count: int = 0
- for palette_index: int in pixels:
- if palette_index > maximum_palette_index:
- return false
- if palette_index > 0:
- painted_count += 1
- return painted_count > 0
-
-
-static func validate_guide_request(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- return (
- _valid_common(value)
- and _valid_canvas_id(value.get("canvas_id"))
- and typeof(value.get("guide_visible")) == TYPE_BOOL
- and typeof(value.get("finalized")) == TYPE_BOOL
- )
-
-
-static func validate_undo_request(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- return _valid_common(value) and _valid_stroke_id(value.get("stroke_id"))
-
-
-static func validate_canvas_state(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- if (
- typeof(value.get("session_id")) != TYPE_STRING
- or str(value["session_id"]).is_empty()
- or str(value["session_id"]).length() > MAX_SESSION_ID_LENGTH
- or typeof(value.get("canvas_id")) != TYPE_STRING
- or str(value["canvas_id"]).is_empty()
- or str(value["canvas_id"]).length() > MAX_CANVAS_ID_LENGTH
- or not _valid_vector(value.get("origin"))
- or not _valid_vector(value.get("normal"))
- or not _valid_vector(value.get("tangent"))
- or typeof(value.get("width")) != TYPE_INT
- or int(value["width"]) not in GRID_SIZES
- or typeof(value.get("height")) != TYPE_INT
- or int(value["height"]) != int(value["width"])
- or typeof(value.get("cell_size")) not in [TYPE_FLOAT, TYPE_INT]
- or not is_equal_approx(float(value["cell_size"]), CELL_SIZE)
- or typeof(value.get("revision")) != TYPE_INT
- or int(value["revision"]) < 0
- or typeof(value.get("guide_visible", true)) != TYPE_BOOL
- or typeof(value.get("finalized", false)) != TYPE_BOOL
- or typeof(value.get("layer", 0)) != TYPE_INT
- or int(value.get("layer", 0)) < 0
- or not NetworkIdentityCrypto.valid_fingerprint(
- value.get("creator_fingerprint")
- )
- or not _valid_participant_fingerprints(
- value.get("participant_fingerprints", [])
- )
- or typeof(value.get("cells")) != TYPE_ARRAY
- ):
- return false
- var cells: Array = value["cells"]
- var grid_width: int = int(value["width"])
- var grid_height: int = int(value["height"])
- if cells.size() > grid_width * grid_height:
- return false
- for cell_value: Variant in cells:
- if (
- not validate_authoritative_cell(cell_value)
- or int((cell_value as Dictionary)["x"]) >= grid_width
- or int((cell_value as Dictionary)["y"]) >= grid_height
- ):
- return false
- var participants: Array = value.get("participant_fingerprints", [])
- if not participants.is_empty():
- if str(value["creator_fingerprint"]) not in participants:
- return false
- for cell_value: Variant in cells:
- if str((cell_value as Dictionary)["author_fingerprint"]) not in participants:
- return false
- return true
-
-
-static func validate_guide_update(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- return (
- typeof(value.get("session_id")) == TYPE_STRING
- and not str(value["session_id"]).is_empty()
- and str(value["session_id"]).length() <= MAX_SESSION_ID_LENGTH
- and _valid_canvas_id(value.get("canvas_id"))
- and typeof(value.get("revision")) == TYPE_INT
- and int(value["revision"]) >= 1
- and typeof(value.get("guide_visible")) == TYPE_BOOL
- and typeof(value.get("finalized")) == TYPE_BOOL
- and _valid_participant_fingerprints(
- value.get("participant_fingerprints", [])
- )
- )
-
-
-static func validate_canvas_update(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- if (
- typeof(value.get("session_id")) != TYPE_STRING
- or str(value["session_id"]).is_empty()
- or str(value["session_id"]).length() > MAX_SESSION_ID_LENGTH
- or typeof(value.get("canvas_id")) != TYPE_STRING
- or str(value["canvas_id"]).is_empty()
- or str(value["canvas_id"]).length() > MAX_CANVAS_ID_LENGTH
- or typeof(value.get("revision")) != TYPE_INT
- or int(value["revision"]) < 1
- or typeof(value.get("edits")) != TYPE_ARRAY
- or not _valid_participant_fingerprints(
- value.get("participant_fingerprints", [])
- )
- ):
- return false
- var edits: Array = value["edits"]
- if edits.is_empty() or edits.size() > MAX_EDITS_PER_REQUEST:
- return false
- for edit_value: Variant in edits:
- if not validate_authoritative_cell(edit_value, true):
- return false
- var participants: Array = value.get("participant_fingerprints", [])
- if not participants.is_empty():
- for edit_value: Variant in edits:
- var edit: Dictionary = edit_value
- var author_fingerprint: String = str(
- edit.get("author_fingerprint", "")
- )
- if (
- not str(edit.get("color_id", "")).is_empty()
- and author_fingerprint not in participants
- ):
- return false
- return true
-
-
-static func validate_cell_edit(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- if (
- typeof(value.get("x")) != TYPE_INT
- or int(value["x"]) < 0
- or int(value["x"]) >= MAX_GRID_SIZE
- or typeof(value.get("y")) != TYPE_INT
- or int(value["y"]) < 0
- or int(value["y"]) >= MAX_GRID_SIZE
- or typeof(value.get("color_id")) not in [TYPE_STRING, TYPE_STRING_NAME]
- ):
- return false
- var color_id := StringName(str(value["color_id"]))
- return color_id.is_empty() or SurfaceDrawingPalette.has_color(color_id)
-
-
-static func validate_authoritative_cell(
- data: Variant,
- allow_erased: bool = false,
-) -> bool:
- if not validate_cell_edit(data):
- return false
- var value: Dictionary = data
- var color_id := StringName(str(value["color_id"]))
- if color_id.is_empty():
- return allow_erased and str(value.get("author_fingerprint", "")).is_empty()
- return NetworkIdentityCrypto.valid_fingerprint(
- value.get("author_fingerprint")
- )
-
-
-static func vector_to_array(value: Vector3) -> Array[float]:
- return [value.x, value.y, value.z]
-
-
-static func array_to_vector(value: Variant) -> Vector3:
- if not _valid_vector(value):
- return Vector3.ZERO
- var array: Array = value
- return Vector3(float(array[0]), float(array[1]), float(array[2]))
-
-
-static func _valid_common(value: Dictionary) -> bool:
- return (
- typeof(value.get("request_id")) == TYPE_STRING
- and not str(value["request_id"]).is_empty()
- and str(value["request_id"]).length() <= MAX_REQUEST_ID_LENGTH
- and typeof(value.get("session_id")) == TYPE_STRING
- and not str(value["session_id"]).is_empty()
- and str(value["session_id"]).length() <= MAX_SESSION_ID_LENGTH
- )
-
-
-static func _valid_canvas_id(value: Variant) -> bool:
- return (
- typeof(value) == TYPE_STRING
- and not str(value).is_empty()
- and str(value).length() <= MAX_CANVAS_ID_LENGTH
- )
-
-
-static func _valid_stroke_id(value: Variant) -> bool:
- return (
- typeof(value) == TYPE_STRING
- and not str(value).is_empty()
- and str(value).length() <= MAX_STROKE_ID_LENGTH
- )
-
-
-static func _valid_participant_fingerprints(value: Variant) -> bool:
- if typeof(value) != TYPE_ARRAY:
- return false
- var fingerprints: Array = value
- if fingerprints.size() > MAX_PARTICIPANTS:
- return false
- var seen: Dictionary[String, bool] = {}
- for fingerprint_value: Variant in fingerprints:
- if typeof(fingerprint_value) != TYPE_STRING:
- return false
- var fingerprint: String = str(fingerprint_value)
- if (
- not NetworkIdentityCrypto.valid_fingerprint(fingerprint)
- or seen.has(fingerprint)
- ):
- return false
- seen[fingerprint] = true
- return true
-
-
-static func _valid_vector(value: Variant) -> bool:
- if typeof(value) != TYPE_ARRAY:
- return false
- var array: Array = value
- if array.size() != 3:
- return false
- for component: Variant in array:
- if typeof(component) not in [TYPE_FLOAT, TYPE_INT]:
- return false
- var number: float = float(component)
- if not is_finite(number) or absf(number) > MAX_COORDINATE:
- return false
- return true
diff --git a/drawing/surface_drawing_protocol.gd.uid b/drawing/surface_drawing_protocol.gd.uid
deleted file mode 100644
index 9c45515..0000000
--- a/drawing/surface_drawing_protocol.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c258sivd8k8nc
diff --git a/economy/art_shop_stock.gd b/economy/art_shop_stock.gd
deleted file mode 100644
index ddd6f3c..0000000
--- a/economy/art_shop_stock.gd
+++ /dev/null
@@ -1,71 +0,0 @@
-class_name ArtShopStock
-extends RefCounted
-
-const ART_KIT_ITEM_ID: StringName = &"art_kit"
-const ART_KIT_PRICE: int = 50
-const UPGRADE_PRICE: int = 50
-
-const MARKER_PRODUCTS: Array[StringName] = [
- &"marker_ocean_teal",
- &"marker_coral",
- &"marker_sunny",
- &"marker_leaf",
- &"marker_blue",
- &"marker_violet",
- &"marker_charcoal",
-]
-const BRUSH_PRODUCTS: Array[StringName] = [
- &"brush_2x",
- &"brush_3x",
- &"brush_4x",
-]
-const GRID_PRODUCTS: Array[StringName] = [
- &"grid_32x",
- &"grid_64x",
- &"grid_128x",
-]
-const STAMP_PRODUCTS: Array[StringName] = [
- PlayerArtUnlocks.STAMP_PRODUCT_ID,
-]
-
-
-static func get_price(product_id: StringName) -> int:
- if product_id == ART_KIT_ITEM_ID:
- return ART_KIT_PRICE
- return UPGRADE_PRICE if PlayerArtUnlocks.is_product_id(product_id) else -1
-
-
-static func get_display_name(product_id: StringName) -> String:
- if product_id == ART_KIT_ITEM_ID:
- return "Art Kit"
- var color_id: StringName = PlayerArtUnlocks.color_id_for_product(product_id)
- if not color_id.is_empty():
- return "%s marker" % SurfaceDrawingPalette.get_display_name(color_id)
- var brush_size: int = PlayerArtUnlocks.brush_size_for_product(product_id)
- if brush_size > 0:
- return "%d× brush" % brush_size
- var grid_size: int = PlayerArtUnlocks.grid_size_for_product(product_id)
- if grid_size > 0:
- return "%d×%d grid" % [grid_size, grid_size]
- if product_id == PlayerArtUnlocks.STAMP_PRODUCT_ID:
- return "Stamp tool"
- return "Unknown art supply"
-
-
-static func get_description(product_id: StringName) -> String:
- if product_id == ART_KIT_ITEM_ID:
- return "Select the Art Kit in the Hotbar to paint."
- var color_id: StringName = PlayerArtUnlocks.color_id_for_product(product_id)
- if not color_id.is_empty():
- return "Unlocks this marker color in the Paint UI."
- var brush_size: int = PlayerArtUnlocks.brush_size_for_product(product_id)
- if brush_size > 0:
- return "Unlocks the %d× brush in the Paint UI." % brush_size
- var grid_size: int = PlayerArtUnlocks.grid_size_for_product(product_id)
- if grid_size > 0:
- return "Unlocks the %d×%d grid in the Paint UI." % [
- grid_size, grid_size,
- ]
- if product_id == PlayerArtUnlocks.STAMP_PRODUCT_ID:
- return "Unlocks saved artwork stamps in the Art Kit."
- return ""
diff --git a/economy/art_shop_stock.gd.uid b/economy/art_shop_stock.gd.uid
deleted file mode 100644
index 3358b54..0000000
--- a/economy/art_shop_stock.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cqagg27unh12c
diff --git a/economy/buyers/pelicans.tres b/economy/buyers/pelicans.tres
index f1a4807..7dce4ec 100644
--- a/economy/buyers/pelicans.tres
+++ b/economy/buyers/pelicans.tres
@@ -9,4 +9,4 @@ display_name = "pelicans"
animal_name_singular = "pelican"
animal_name_plural = "pelicans"
payout_multiplier = 0.25
-sale_message_template = "You sold your {fish} to the pelicans."
+sale_message_template = "You sold your {fish} to the pelicans for ${payout}."
diff --git a/economy/fish_buyer_profile.gd b/economy/fish_buyer_profile.gd
index 9bb59de..bca1cea 100644
--- a/economy/fish_buyer_profile.gd
+++ b/economy/fish_buyer_profile.gd
@@ -1,8 +1,6 @@
class_name FishBuyerProfile
extends Resource
-const FishQualityType = preload("res://fish/fish_quality.gd")
-
@export var id: StringName
@export var display_name: String
@export var animal_name_singular: String
@@ -27,18 +25,6 @@ func get_offer(base_value: int) -> int:
return maxi(offer, 0)
-func get_quality_offer(base_value: int, quality: int) -> int:
- if not FishQualityType.is_valid(quality):
- return -1
- var ordinary_offer: int = get_offer(base_value)
- var adjusted_offer: int = get_offer(
- FishQualityType.apply_sale_value(base_value, quality)
- )
- if ordinary_offer < 0 or adjusted_offer < 0:
- return -1
- return maxi(adjusted_offer, ordinary_offer + quality)
-
-
func get_sale_message(
fish_name: String,
payout: int,
@@ -53,7 +39,8 @@ func get_sale_message(
var buyer_name: String = animal_name_plural
if buyer_name.is_empty():
buyer_name = display_name
- return "you sold your %s to the %s." % [
+ return "you sold your %s to the %s for $%d." % [
fish_name,
buyer_name,
+ payout,
]
diff --git a/economy/fish_sale_result.gd b/economy/fish_sale_result.gd
index 74aa72b..e133245 100644
--- a/economy/fish_sale_result.gd
+++ b/economy/fish_sale_result.gd
@@ -9,7 +9,6 @@ enum Status {
INVALID_BUYER,
INVALID_OFFER,
INVALID_SELECTION,
- RESERVED,
TRANSACTION_FAILED,
}
@@ -47,7 +46,5 @@ func get_message() -> String:
return "invalid buyer offer."
Status.INVALID_SELECTION:
return "the fish selection is invalid."
- Status.RESERVED:
- return "reserved fish cannot be sold."
_:
return "transaction failed."
diff --git a/economy/fish_sale_service.gd b/economy/fish_sale_service.gd
index 6aca45d..b5b81b2 100644
--- a/economy/fish_sale_service.gd
+++ b/economy/fish_sale_service.gd
@@ -11,17 +11,14 @@ signal sale_completed(result: FishSaleResultType)
var _inventory: FishInventoryType
var _wallet: PlayerWalletType
-var _reservations: PlayerAssetReservationService
func setup(
inventory: FishInventoryType,
wallet: PlayerWalletType,
- reservations: PlayerAssetReservationService = null,
) -> void:
_inventory = inventory
_wallet = wallet
- _reservations = reservations
func can_sell(
@@ -117,26 +114,12 @@ func _validate_batch(
if fish_catch == null:
result.status = FishSaleResultType.Status.NOT_FOUND
return result
- if (
- _reservations != null
- and _reservations.is_fish_reserved(catch_id)
- ):
- result.status = FishSaleResultType.Status.RESERVED
- return result
if fish_catch.is_favorited:
contains_favorite = true
if fish_catch.sale_value < 0:
result.status = FishSaleResultType.Status.INVALID_VALUE
return result
- var ordinary_value: int = (
- fish_catch.fish.get_sale_value_for_weight(
- fish_catch.weight_lb
- )
- )
- var offer: int = buyer.get_quality_offer(
- ordinary_value,
- fish_catch.quality,
- )
+ var offer: int = buyer.get_offer(fish_catch.sale_value)
if offer < 0:
result.status = FishSaleResultType.Status.INVALID_OFFER
return result
@@ -165,10 +148,9 @@ func _validate_batch(
result.payout
)
else:
- result.sale_message = (
- "you sold %d fish to the %s." % [
+ result.sale_message = "you sold %d fish to the %s for $%d." % [
result.fish_count,
result.buyer_animal_name,
- ]
- )
+ result.payout,
+ ]
return result
diff --git a/economy/fishing_shop_stock.gd b/economy/fishing_shop_stock.gd
index b9d3eaf..3d5e867 100644
--- a/economy/fishing_shop_stock.gd
+++ b/economy/fishing_shop_stock.gd
@@ -3,62 +3,20 @@ extends RefCounted
const ItemCatalogType = preload("res://items/item_catalog.gd")
const ItemDataType = preload("res://items/item_data.gd")
-const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
const PlayerBagType = preload("res://inventory/player_bag.gd")
const PlayerWalletType = preload("res://economy/player_wallet.gd")
-const WORM_MAX_STACK: int = 10
-const FISH_FINDER_ID: StringName = &"fish_finder"
-const MAGNET_ID: StringName = &"magnet"
-const BATTERIES_ID: StringName = &"batteries"
-const CRAB_NET_ID: StringName = &"crab_net"
-const STANDARD_SHOVEL_ID: StringName = &"standard_shovel"
-const FISHING_NET_ID: StringName = &"fishing_net"
const ITEM_PRICES: Dictionary[StringName, int] = {
- &"worms": 1,
- &"snails": 3,
- &"shrimp": 7,
- &"squid_chunks": 15,
- &"whole_anchovy": 60,
- &"whole_sardine": 30,
- &"luminous_roe": 125,
- &"the_standby": 750,
&"coffee": 20,
&"energy_drink": 35,
&"snack": 30,
- FISH_FINDER_ID: 500,
- MAGNET_ID: 250,
- BATTERIES_ID: 15,
- CRAB_NET_ID: 50,
- STANDARD_SHOVEL_ID: 75,
- FISHING_NET_ID: 250,
-}
-const BAIT_UNLOCK_PRICES: Dictionary[StringName, int] = {
- &"snails": 400,
- &"shrimp": 1200,
- &"squid_chunks": 3500,
- &"whole_anchovy": 22000,
- &"whole_sardine": 9000,
- &"luminous_roe": 50000,
+ &"fish_finder": 60,
}
const ITEM_ORDER: Array[StringName] = [
- &"worms",
- &"snails",
- &"shrimp",
- &"squid_chunks",
- &"whole_sardine",
- &"whole_anchovy",
- &"luminous_roe",
- &"the_standby",
- FISH_FINDER_ID,
- MAGNET_ID,
- CRAB_NET_ID,
- STANDARD_SHOVEL_ID,
- FISHING_NET_ID,
&"coffee",
&"energy_drink",
&"snack",
- BATTERIES_ID,
+ &"fish_finder",
]
@@ -70,86 +28,6 @@ static func get_stock_item_ids() -> Array[StringName]:
return ITEM_ORDER.duplicate()
-static func get_stock_order_index(item_id: StringName) -> int:
- return ITEM_ORDER.find(item_id)
-
-
-static func get_rod_stock(
- catalog: ItemCatalogType,
-) -> Array[FishingRodDataType]:
- var rods: Array[FishingRodDataType] = []
- if catalog == null:
- return rods
- for item: ItemDataType in catalog.get_available_items():
- var rod := item as FishingRodDataType
- if rod != null and rod.is_shop_available():
- rods.append(rod)
- rods.sort_custom(
- func(left: FishingRodDataType, right: FishingRodDataType) -> bool:
- if left.shop_order == right.shop_order:
- return str(left.item_id) < str(right.item_id)
- return left.shop_order < right.shop_order
- )
- return rods
-
-
-static func get_unlock_price(item_id: StringName) -> int:
- return BAIT_UNLOCK_PRICES.get(item_id, -1)
-
-
-static func get_purchase_quantity(
- item_id: StringName,
- owned: int,
- maximum_stack: int = WORM_MAX_STACK,
-) -> int:
- if is_bait_topoff(item_id):
- return maxi(maximum_stack - owned, 0)
- return 1
-
-
-static func is_bait_topoff(item_id: StringName) -> bool:
- return item_id == &"worms" or BAIT_UNLOCK_PRICES.has(item_id)
-
-
-static func is_permanent_unlock(
- item_id: StringName,
- item: ItemDataType,
-) -> bool:
- return (
- item != null
- and (
- item.is_lure()
- or item_id == FISH_FINDER_ID
- or item_id == MAGNET_ID
- or item_id == CRAB_NET_ID
- or item_id == STANDARD_SHOVEL_ID
- or item_id == FISHING_NET_ID
- or item is FishingRodDataType
- )
- )
-
-
-static func is_passive_supply(item_id: StringName) -> bool:
- return item_id == BATTERIES_ID
-
-
-static func is_gathering_tool(item_id: StringName) -> bool:
- return item_id == CRAB_NET_ID or item_id == STANDARD_SHOVEL_ID
-
-
-static func get_purchase_cost(
- item_id: StringName,
- quantity: int,
- bait_unlocked: bool,
-) -> int:
- if quantity <= 0:
- return -1
- if is_bait_topoff(item_id) and item_id != &"worms" and not bait_unlocked:
- return get_unlock_price(item_id)
- var unit_price: int = get_price(item_id)
- return unit_price * quantity if unit_price >= 0 else -1
-
-
static func purchase_one(
item_id: StringName,
wallet: PlayerWalletType,
@@ -158,49 +36,22 @@ static func purchase_one(
) -> bool:
if wallet == null or bag == null or catalog == null:
return false
+ var price: int = get_price(item_id)
var item: ItemDataType = catalog.get_item_by_id(item_id)
- if item == null or not item.is_available():
- return false
- var bait_topoff: bool = is_bait_topoff(item_id)
- var permanent_unlock: bool = is_permanent_unlock(item_id, item)
- var quantity: int = get_purchase_quantity(
- item_id,
- bag.get_quantity(item_id),
- item.max_stack if item != null else WORM_MAX_STACK,
- )
- var rod := item as FishingRodDataType
- var price: int = (
- rod.shop_price
- if rod != null
- else get_purchase_cost(
- item_id,
- quantity,
- bag.is_bait_unlocked(item_id),
- )
- )
if (
price < 0
- or not item.is_available()
- or (rod != null and not rod.is_shop_available())
- or (
- item.category != ItemDataType.Category.CONSUMABLE
- and not bait_topoff
- and not permanent_unlock
- )
- or (not item.stackable and not permanent_unlock)
- or (
- not item.usable
- and not bait_topoff
- and not permanent_unlock
- and not is_passive_supply(item_id)
- )
- or not bag.can_add_item(item_id, quantity)
+ or item == null
+ or not item.is_valid()
+ or item.category != ItemDataType.Category.CONSUMABLE
+ or not item.stackable
+ or not item.usable
+ or not bag.can_add_item(item_id, 1)
or not wallet.can_afford(price)
):
return false
if not wallet.debit(price):
return false
- if bag.add_item(item_id, quantity):
+ if bag.add_item(item_id, 1):
return true
if not wallet.credit(price):
push_error("Fishing Shop failed to roll back an item purchase.")
diff --git a/economy/item_resale_policy.gd b/economy/item_resale_policy.gd
deleted file mode 100644
index 299fc48..0000000
--- a/economy/item_resale_policy.gd
+++ /dev/null
@@ -1,21 +0,0 @@
-class_name ItemResalePolicy
-extends RefCounted
-
-const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
-const ItemDataType = preload("res://items/item_data.gd")
-
-
-static func is_sellable(item: ItemDataType) -> bool:
- # Bait and lures live in their own tackle collection. Rods, tools, and
- # utility gear are permanent equipment. The unified sell tray currently
- # accepts ordinary consumable supplies only.
- return item != null and item.is_available() and (
- item.category == ItemDataType.Category.CONSUMABLE
- )
-
-
-static func get_unit_value(item: ItemDataType) -> int:
- if not is_sellable(item):
- return -1
- var purchase_price := FishingShopStockType.get_price(item.item_id)
- return purchase_price / 2 if purchase_price >= 0 else -1
diff --git a/economy/item_resale_policy.gd.uid b/economy/item_resale_policy.gd.uid
deleted file mode 100644
index b8270b2..0000000
--- a/economy/item_resale_policy.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://brsykj7darpyw
diff --git a/export_presets.cfg b/export_presets.cfg
index a49f01f..e01b43e 100644
--- a/export_presets.cfg
+++ b/export_presets.cfg
@@ -7,9 +7,9 @@ advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
-include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
-exclude_filter="builds/*,scripts/*"
-export_path="builds/v0.20.2-alpha/windows-x86_64/straywild.exe"
+include_filter=""
+exclude_filter="builds/*,playtest/*,scripts/*"
+export_path="builds/playtest-0.2.0-prealpha.2/windows-x86_64/netfishing.exe"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
@@ -32,16 +32,16 @@ codesign/digest_algorithm=1
codesign/description=""
codesign/custom_options=PackedStringArray()
application/modify_resources=true
-application/icon="res://art/exported/system_icons/straywild.ico"
+application/icon=""
application/console_wrapper_icon=""
-application/icon_interpolation=0
-application/file_version="0.20.2.0"
-application/product_version="0.20.2.0"
-application/company_name="Woofmeow"
-application/product_name="straywild"
-application/file_description="straywild v0.20.2-alpha"
-application/copyright="Copyright © 2026 Woofmeow"
-application/trademarks="straywild and Woofmeow branding is reserved; see TRADEMARKS.md"
+application/icon_interpolation=4
+application/file_version="0.2.0.2"
+application/product_version="0.2.0.2"
+application/company_name=""
+application/product_name="NETFISHING"
+application/file_description="NETFISHING Pre-Alpha Playtest 0.2"
+application/copyright=""
+application/trademarks=""
application/export_angle=0
ssh_remote_deploy/enabled=false
ssh_remote_deploy/host="user@host_ip"
@@ -51,158 +51,6 @@ ssh_remote_deploy/extra_args_scp=""
ssh_remote_deploy/run_script="#!/usr/bin/env bash\nexport DISPLAY=:0\nunzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"\n\"{temp_dir}/{exe_name}\" {cmd_args}"
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash\nkill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")\nrm -rf \"{temp_dir}\""
-[preset.3]
-
-name="Linux ARM64"
-platform="Linux"
-runnable=false
-advanced_options=false
-dedicated_server=false
-custom_features=""
-export_filter="all_resources"
-include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
-exclude_filter="builds/*,scripts/*"
-export_path="builds/v0.20.2-alpha/linux-arm64/straywild.arm64"
-patches=PackedStringArray()
-encryption_include_filters=""
-encryption_exclude_filters=""
-seed=0
-encrypt_pck=false
-encrypt_directory=false
-script_export_mode=2
-
-[preset.3.options]
-
-custom_template/debug=""
-custom_template/release=""
-debug/export_console_wrapper=0
-binary_format/embed_pck=false
-binary_format/architecture="arm64"
-texture_format/s3tc_bptc=false
-texture_format/etc2_astc=true
-
-[preset.4]
-
-name="macOS"
-platform="macOS"
-runnable=false
-advanced_options=false
-dedicated_server=false
-custom_features=""
-export_filter="all_resources"
-include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
-exclude_filter="builds/*,scripts/*"
-export_path="builds/v0.20.2-alpha/macos/straywild.zip"
-patches=PackedStringArray()
-encryption_include_filters=""
-encryption_exclude_filters=""
-seed=0
-encrypt_pck=false
-encrypt_directory=false
-script_export_mode=2
-
-[preset.4.options]
-
-custom_template/debug=""
-custom_template/release=""
-application/bundle_identifier="io.woofmeow.netfishing"
-application/icon="res://art/exported/system_icons/straywild_1024.png"
-application/icon_interpolation=0
-application/short_version="0.20.2"
-application/version="0.20.2"
-application/architecture="universal"
-codesign/enable=false
-notarization/enable=false
-
-[preset.5]
-
-name="Linux Dedicated Server"
-platform="Linux"
-runnable=false
-advanced_options=false
-dedicated_server=true
-custom_features=""
-export_filter="all_resources"
-include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
-exclude_filter="builds/*,scripts/*,tests/*"
-export_path="builds/v0.20.2-alpha/server-linux-x86_64/straywildServer.x86_64"
-patches=PackedStringArray()
-encryption_include_filters=""
-encryption_exclude_filters=""
-seed=0
-encrypt_pck=false
-encrypt_directory=false
-script_export_mode=2
-
-[preset.5.options]
-
-custom_template/debug=""
-custom_template/release=""
-debug/export_console_wrapper=0
-binary_format/embed_pck=false
-binary_format/architecture="x86_64"
-texture_format/s3tc_bptc=true
-texture_format/etc2_astc=false
-
-[preset.2]
-
-name="Android"
-platform="Android"
-runnable=true
-advanced_options=false
-dedicated_server=false
-custom_features=""
-export_filter="all_resources"
-include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
-exclude_filter="builds/*,scripts/*"
-export_path="builds/v0.20.2-alpha/android/straywild.apk"
-patches=PackedStringArray()
-encryption_include_filters=""
-encryption_exclude_filters=""
-seed=0
-encrypt_pck=false
-encrypt_directory=false
-script_export_mode=2
-
-[preset.2.options]
-
-custom_template/debug=""
-custom_template/release=""
-gradle_build/use_gradle_build=false
-gradle_build/export_format=0
-gradle_build/min_sdk=""
-gradle_build/target_sdk=""
-architectures/armeabi-v7a=false
-architectures/arm64-v8a=true
-architectures/x86=false
-architectures/x86_64=false
-version/code=200200
-version/name="v0.20.2-alpha"
-package/unique_name="io.woofmeow.netfishing"
-package/name="straywild"
-package/signed=true
-package/app_category=0
-package/retain_data_on_uninstall=false
-package/exclude_from_recents=false
-package/show_in_android_tv=false
-package/show_in_app_library=true
-package/show_as_launcher_app=true
-launcher_icons/main_192x192="res://art/exported/system_icons/straywild_256.png"
-graphics/opengl_debug=false
-screen/immersive_mode=true
-screen/edge_to_edge=false
-screen/support_small=false
-screen/support_normal=true
-screen/support_large=true
-screen/support_xlarge=true
-user_data_backup/allow=false
-gesture/swipe_to_dismiss=false
-permissions/internet=true
-permissions/access_network_state=true
-permissions/custom_permissions=PackedStringArray()
-texture_format/s3tc_bptc=false
-texture_format/etc2_astc=true
-
[preset.1]
name="Linux Desktop"
@@ -212,9 +60,9 @@ advanced_options=false
dedicated_server=false
custom_features=""
export_filter="all_resources"
-include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
-exclude_filter="builds/*,scripts/*"
-export_path="builds/v0.20.2-alpha/linux-x86_64/straywild.x86_64"
+include_filter=""
+exclude_filter="builds/*,playtest/*,scripts/*"
+export_path="builds/playtest-0.2.0-prealpha.2/linux-x86_64/netfishing"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
diff --git a/fish/day_availability.tres b/fish/day_availability.tres
deleted file mode 100644
index 6cf8205..0000000
--- a/fish/day_availability.tres
+++ /dev/null
@@ -1,10 +0,0 @@
-[gd_resource type="Resource" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="1_availability"]
-
-[resource]
-script = ExtResource("1_availability")
-allow_day = true
-allow_night = false
-preferred_bait_tags = Array[StringName]([&"worm"])
-preferred_bait_weight_multiplier = 1.35
diff --git a/fish/fish_availability.gd b/fish/fish_availability.gd
index f46a973..f593d75 100644
--- a/fish/fish_availability.gd
+++ b/fish/fish_availability.gd
@@ -10,35 +10,22 @@ const FishingContextType = preload("res://fishing/fishing_context.gd")
@export var allow_night: bool = true
@export var require_rain: bool = false
@export var forbid_rain: bool = false
-@export var require_fog: bool = false
-@export var forbid_fog: bool = false
@export var required_bait_tags: Array[StringName] = []
@export var preferred_bait_tags: Array[StringName] = []
@export_range(0.01, 10.0, 0.01) var preferred_bait_weight_multiplier: float = 1.25
func is_available(context: FishingContextType) -> bool:
- if (
- context == null
- or (require_rain and forbid_rain)
- or (require_fog and forbid_fog)
- ):
+ if context == null or (require_rain and forbid_rain):
return false
- if context.is_day_night_transition:
- if not allow_day and not allow_night:
- return false
- elif context.is_night and not allow_night:
+ if context.is_night and not allow_night:
return false
- elif not context.is_night and not allow_day:
+ if not context.is_night and not allow_day:
return false
if require_rain and not context.is_raining:
return false
if forbid_rain and context.is_raining:
return false
- if require_fog and not context.is_foggy:
- return false
- if forbid_fog and context.is_foggy:
- return false
if (
not allowed_location_tags.is_empty()
and not _has_any_match(allowed_location_tags, context.location_tags)
diff --git a/fish/fish_catch.gd b/fish/fish_catch.gd
index a0d3738..318e2bb 100644
--- a/fish/fish_catch.gd
+++ b/fish/fish_catch.gd
@@ -2,7 +2,6 @@ class_name FishCatch
extends Resource
const FishDataType = preload("res://fish/fish_data.gd")
-const FishQualityType = preload("res://fish/fish_quality.gd")
const MAX_SAFE_WEIGHT_LB: float = 1000000.0
const MAX_SAFE_DISPLAY_SCALE: float = 10000.0
@@ -15,7 +14,6 @@ const MAX_SAFE_SEQUENCE: int = 2147483647
@export var catch_sequence: int = 0
@export var weight_lb: float = 0.0
@export var display_scale: float = 1.0
-@export_range(0, 4, 1) var quality: int = FishQualityType.Tier.BORING
@export var sale_value: int = 0
@export var is_favorited: bool = false
@@ -50,7 +48,6 @@ func is_valid() -> bool:
and is_finite(display_scale)
and display_scale > 0.0
and display_scale <= MAX_SAFE_DISPLAY_SCALE
- and FishQualityType.is_valid(quality)
and sale_value >= 0
and sale_value <= MAX_SAFE_SALE_VALUE
)
@@ -63,19 +60,6 @@ func to_save_dict() -> Dictionary:
"fish_id": String(fish_id),
"weight_lb": weight_lb,
"display_scale": display_scale,
- "quality": quality,
- "sale_value": sale_value,
- "is_favorited": is_favorited,
- }
-
-
-func to_network_dict() -> Dictionary:
- return {
- "catch_id": String(catch_id),
- "fish_id": String(fish_id),
- "weight_lb": weight_lb,
- "display_scale": display_scale,
- "quality": quality,
"sale_value": sale_value,
"is_favorited": is_favorited,
}
@@ -93,7 +77,6 @@ static func from_save_dict(
"fish_id",
"weight_lb",
"display_scale",
- "quality",
"sale_value",
"is_favorited",
]:
@@ -117,17 +100,12 @@ static func from_save_dict(
-1,
MAX_SAFE_SALE_VALUE
)
- var loaded_quality: int = _read_safe_integer(
- data["quality"],
- -1,
- FishQualityType.TIER_COUNT - 1,
- )
var loaded_weight: float = _read_safe_float(
data["weight_lb"],
0.0,
MAX_SAFE_WEIGHT_LB
)
- var _loaded_scale: float = _read_safe_float(
+ var loaded_scale: float = _read_safe_float(
data["display_scale"],
0.0,
MAX_SAFE_DISPLAY_SCALE
@@ -139,9 +117,8 @@ static func from_save_dict(
or loaded_sequence <= 0
or loaded_sequence >= MAX_SAFE_SEQUENCE
or loaded_sale_value < 0
- or not FishQualityType.is_valid(loaded_quality)
or loaded_weight <= 0.0
- or _loaded_scale <= 0.0
+ or loaded_scale <= 0.0
or typeof(loaded_favorite) != TYPE_BOOL
):
return null
@@ -152,75 +129,12 @@ static func from_save_dict(
fish_catch.catch_id = loaded_catch_id
fish_catch.catch_sequence = loaded_sequence
fish_catch.weight_lb = loaded_weight
- # Recompute presentation size from authoritative weight. This keeps saves
- # written before the weight-based sizing rule from retaining oversized
- # per-species display values.
- fish_catch.display_scale = resolved_fish.get_display_scale_for_weight(
- loaded_weight
- )
- fish_catch.quality = loaded_quality
+ fish_catch.display_scale = loaded_scale
fish_catch.sale_value = loaded_sale_value
fish_catch.is_favorited = bool(loaded_favorite)
return fish_catch if fish_catch.is_valid() else null
-static func from_network_dict(
- data: Dictionary,
- resolved_fish: FishDataType,
-) -> FishCatch:
- if resolved_fish == null:
- return null
- for required_key: String in [
- "catch_id",
- "fish_id",
- "weight_lb",
- "display_scale",
- "quality",
- "sale_value",
- ]:
- if not data.has(required_key):
- return null
- var loaded_id := StringName(str(data.get("catch_id", "")))
- var loaded_fish_id := StringName(str(data.get("fish_id", "")))
- var loaded_weight: float = _read_safe_float(
- data.get("weight_lb"), 0.0, MAX_SAFE_WEIGHT_LB
- )
- var _loaded_scale: float = _read_safe_float(
- data.get("display_scale"), 0.0, MAX_SAFE_DISPLAY_SCALE
- )
- var loaded_value: int = _read_safe_integer(
- data.get("sale_value"), -1, MAX_SAFE_SALE_VALUE
- )
- var loaded_quality: int = _read_safe_integer(
- data.get("quality"),
- -1,
- FishQualityType.TIER_COUNT - 1,
- )
- if (
- loaded_id.is_empty()
- or loaded_id.length() > 160
- or loaded_fish_id != resolved_fish.id
- or loaded_weight <= 0.0
- or _loaded_scale <= 0.0
- or loaded_value < 0
- or not FishQualityType.is_valid(loaded_quality)
- ):
- return null
- var fish_catch := FishCatch.new()
- fish_catch.fish = resolved_fish
- fish_catch.fish_id = loaded_fish_id
- fish_catch.catch_id = loaded_id
- fish_catch.catch_sequence = 0
- fish_catch.weight_lb = loaded_weight
- fish_catch.display_scale = resolved_fish.get_display_scale_for_weight(
- loaded_weight
- )
- fish_catch.quality = loaded_quality
- fish_catch.sale_value = loaded_value
- fish_catch.is_favorited = false
- return fish_catch if fish_catch.is_valid() else null
-
-
static func _read_safe_integer(
value: Variant,
invalid_value: int,
diff --git a/fish/fish_data.gd b/fish/fish_data.gd
index 46d4675..aa73d2a 100644
--- a/fish/fish_data.gd
+++ b/fish/fish_data.gd
@@ -1,21 +1,10 @@
class_name FishData
extends Resource
-# Fish are displayed by their physical weight, rather than by per-species
-# texture or resource dimensions. A cube-root curve approximates the way a
-# fish's linear dimensions grow with mass while keeping large catches readable
-# beside the player.
-const DISPLAY_REFERENCE_WEIGHT_LB: float = 1.0
-const DISPLAY_REFERENCE_SCALE: float = 0.85
-const DISPLAY_WEIGHT_EXPONENT: float = 0.33333334
-const DISPLAY_MIN_SCALE: float = 0.45
-const DISPLAY_MAX_SCALE: float = 3.0
-
const CatchDifficultyProfileType = preload(
"res://fishing/catch_difficulty_profile.gd"
)
const FishAvailabilityType = preload("res://fish/fish_availability.gd")
-const CalendarSeasonType = preload("res://world/calendar_season.gd")
enum Rarity {
COMMON,
@@ -25,47 +14,14 @@ enum Rarity {
LEGENDARY,
}
-enum CollectionMethod {
- FISHING,
- NET,
- DIGGING,
-}
-
-enum LogbookSection {
- AUTOMATIC,
- SHELLFISH,
-}
-
-enum CreatureGroup {
- FISH,
- INSECT,
- SHELLFISH,
-}
-
@export var id: StringName
@export var display_name: String
-@export_category("Developer Catalog")
-## Disabled species remain resolvable for saves but cannot be newly selected.
-@export var active: bool = true
-@export_range(1, 999999, 1) var catalog_number: int = 1
-@export var collection_group: StringName
-@export_multiline var logbook_fact: String
-@export var collection_method: CollectionMethod = CollectionMethod.FISHING
-@export var logbook_section: LogbookSection = LogbookSection.AUTOMATIC
-@export var habitat_label: String = ""
-@export_flags("Spring", "Summer", "Fall", "Winter")
-var available_seasons: int = CalendarSeasonType.ALL_MASK
-@export_category("Fishing")
-@export_flags("Fresh Water", "Salt Water")
-var allowed_water_types: int = WaterType.ALL_FISHABLE_MASK
@export var rarity: Rarity = Rarity.COMMON
@export_range(0.0, 1000.0, 0.01) var base_catch_weight: float = 1.0
@export var catch_profile: CatchDifficultyProfileType
@export var availability: FishAvailabilityType
-@export_range(0.001, 500000.0, 0.001) var weight_min_lb: float = 0.5
-@export_range(0.001, 500000.0, 0.001) var weight_max_lb: float = 1.0
-# Retained in resource files for compatibility with existing authored data;
-# runtime presentation is derived from absolute weight below.
+@export_range(0.01, 1000.0, 0.01) var weight_min_lb: float = 0.5
+@export_range(0.01, 1000.0, 0.01) var weight_max_lb: float = 1.0
@export_range(0.01, 20.0, 0.01) var display_scale_min: float = 0.8
@export_range(0.01, 20.0, 0.01) var display_scale_max: float = 1.2
@export_range(0.01, 10.0, 0.01) var display_scale_curve: float = 1.0
@@ -76,10 +32,6 @@ var allowed_water_types: int = WaterType.ALL_FISHABLE_MASK
func is_selectable() -> bool:
- return active and is_valid_catalog_entry()
-
-
-func is_valid_catalog_entry() -> bool:
return (
not id.is_empty()
and base_catch_weight > 0.0
@@ -92,76 +44,34 @@ func is_valid_catalog_entry() -> bool:
and sell_value_min >= 0
and sell_value_max >= sell_value_min
and sell_value_curve > 0.0
- and CalendarSeasonType.is_valid_mask(available_seasons)
)
-func is_fishable() -> bool:
- return is_selectable() and collection_method == CollectionMethod.FISHING
-
-
-func get_creature_group() -> CreatureGroup:
- if logbook_section == LogbookSection.SHELLFISH:
- return CreatureGroup.SHELLFISH
- if collection_method == CollectionMethod.FISHING:
- return CreatureGroup.FISH
- return CreatureGroup.INSECT
-
-
-func get_habitat_label() -> String:
- if not habitat_label.strip_edges().is_empty():
- return habitat_label.strip_edges().to_lower()
- return WaterType.label(get_primary_water_type()).to_lower()
-
-
-func is_allowed_in_water(type: WaterType.Type) -> bool:
- return (allowed_water_types & WaterType.mask_for(type)) != 0
-
-
-func is_available_in_season(season: int) -> bool:
- return CalendarSeasonType.includes(available_seasons, season)
-
-
-func get_season_text() -> String:
- return CalendarSeasonType.format_mask(available_seasons)
-
-
-func get_primary_water_type() -> WaterType.Type:
- if allowed_water_types & WaterType.FRESH_WATER_MASK:
- return WaterType.Type.FRESH_WATER
- if allowed_water_types & WaterType.SALT_WATER_MASK:
- return WaterType.Type.SALT_WATER
- return WaterType.Type.OTHER
-
-
func get_minimum_weight() -> float:
- return maxf(weight_min_lb, 0.001)
+ return maxf(weight_min_lb, 0.01)
func get_maximum_weight() -> float:
return maxf(weight_max_lb, get_minimum_weight())
-func get_weight_percentile(weight_lb: float) -> float:
+func get_display_scale_for_weight(weight_lb: float) -> float:
var minimum_weight: float = get_minimum_weight()
var maximum_weight: float = get_maximum_weight()
- if maximum_weight <= minimum_weight:
- return 0.0
- return clampf(
- inverse_lerp(minimum_weight, maximum_weight, weight_lb),
- 0.0,
- 1.0,
+ var normalized_weight: float = 0.0
+ if maximum_weight > minimum_weight:
+ normalized_weight = inverse_lerp(
+ minimum_weight,
+ maximum_weight,
+ weight_lb
+ )
+ normalized_weight = pow(
+ clampf(normalized_weight, 0.0, 1.0),
+ maxf(display_scale_curve, 0.01)
)
-
-
-func get_display_scale_for_weight(weight_lb: float) -> float:
- var safe_weight_lb: float = maxf(weight_lb, 0.01)
- var weight_ratio: float = safe_weight_lb / DISPLAY_REFERENCE_WEIGHT_LB
- var weight_scale: float = DISPLAY_REFERENCE_SCALE * pow(
- weight_ratio,
- DISPLAY_WEIGHT_EXPONENT,
- )
- return clampf(weight_scale, DISPLAY_MIN_SCALE, DISPLAY_MAX_SCALE)
+ var minimum_scale: float = maxf(display_scale_min, 0.01)
+ var maximum_scale: float = maxf(display_scale_max, minimum_scale)
+ return lerpf(minimum_scale, maximum_scale, normalized_weight)
func get_sale_value_for_weight(weight_lb: float) -> int:
diff --git a/fish/fish_experience.gd b/fish/fish_experience.gd
deleted file mode 100644
index 592042f..0000000
--- a/fish/fish_experience.gd
+++ /dev/null
@@ -1,73 +0,0 @@
-class_name FishExperience
-extends RefCounted
-
-const FishCatchType = preload("res://fish/fish_catch.gd")
-const FishQualityType = preload("res://fish/fish_quality.gd")
-const CollectionLogType = preload("res://collection/collection_log.gd")
-
-const RARITY_BASE_EXPERIENCE: Array[int] = [10, 18, 32, 55, 90]
-const QUALITY_MULTIPLIERS: Array[float] = [1.0, 1.15, 1.4, 1.8, 2.5]
-const MAXIMUM_WEIGHT_BONUS: float = 0.25
-const FIRST_SPECIES_BONUS: int = 25
-const FIRST_QUALITY_BONUS: int = 15
-const SPECIES_MASTERY_BONUS: int = 100
-
-
-static func calculate_for_collection(
- fish_catch: FishCatchType,
- collection_log: CollectionLogType,
-) -> int:
- if fish_catch == null or collection_log == null:
- return 0
- return calculate_catch_experience(
- fish_catch,
- collection_log.has_discovered(fish_catch.fish_id),
- collection_log.get_quality_mask(fish_catch.fish_id),
- )
-
-
-static func calculate_catch_experience(
- fish_catch: FishCatchType,
- was_species_discovered: bool,
- previous_quality_mask: int,
-) -> int:
- if fish_catch == null or not fish_catch.is_valid():
- return 0
- var rarity: int = int(fish_catch.fish.rarity)
- if rarity < 0 or rarity >= RARITY_BASE_EXPERIENCE.size():
- return 0
- if not FishQualityType.is_valid(fish_catch.quality):
- return 0
- var weight_percentile: float = _get_weight_percentile(fish_catch)
- var weight_multiplier: float = (
- 1.0 + MAXIMUM_WEIGHT_BONUS * weight_percentile * weight_percentile
- )
- var catch_experience: int = roundi(
- float(RARITY_BASE_EXPERIENCE[rarity])
- * QUALITY_MULTIPLIERS[fish_catch.quality]
- * weight_multiplier
- )
- var quality_bit: int = FishQualityType.bit_for(fish_catch.quality)
- if not was_species_discovered:
- catch_experience += FIRST_SPECIES_BONUS
- if (previous_quality_mask & quality_bit) == 0:
- catch_experience += FIRST_QUALITY_BONUS
- var next_quality_mask: int = previous_quality_mask | quality_bit
- if (
- previous_quality_mask != FishQualityType.ALL_TIERS_MASK
- and next_quality_mask == FishQualityType.ALL_TIERS_MASK
- ):
- catch_experience += SPECIES_MASTERY_BONUS
- return maxi(catch_experience, 0)
-
-
-static func _get_weight_percentile(fish_catch: FishCatchType) -> float:
- var minimum_weight: float = fish_catch.fish.get_minimum_weight()
- var maximum_weight: float = fish_catch.fish.get_maximum_weight()
- if maximum_weight <= minimum_weight:
- return 0.0
- return clampf(
- inverse_lerp(minimum_weight, maximum_weight, fish_catch.weight_lb),
- 0.0,
- 1.0,
- )
diff --git a/fish/fish_experience.gd.uid b/fish/fish_experience.gd.uid
deleted file mode 100644
index aafa145..0000000
--- a/fish/fish_experience.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://d05mwat7tju1b
diff --git a/fish/fish_quality.gd b/fish/fish_quality.gd
deleted file mode 100644
index 61534e0..0000000
--- a/fish/fish_quality.gd
+++ /dev/null
@@ -1,175 +0,0 @@
-class_name FishQuality
-extends RefCounted
-
-enum Tier {
- BORING,
- AVERAGE,
- IMPRESSIVE,
- EXCEPTIONAL,
- SHINY,
-}
-
-const TIER_COUNT: int = 5
-const ALL_TIERS_MASK: int = (1 << TIER_COUNT) - 1
-
-# Starting distribution. Equipment and tackle can supply per-tier
-# multipliers later without changing catch serialization or tier identity.
-const BASE_ROLL_WEIGHTS: Array[float] = [40.0, 32.0, 18.0, 8.0, 2.0]
-const WORM_ROLL_WEIGHTS: Array[float] = [80.0, 10.0, 5.0, 4.0, 1.0]
-const SNAIL_ROLL_WEIGHTS: Array[float] = [68.0, 16.0, 8.0, 6.0, 2.0]
-const SHRIMP_ROLL_WEIGHTS: Array[float] = [56.0, 21.0, 12.0, 8.0, 3.0]
-const SQUID_ROLL_WEIGHTS: Array[float] = [44.0, 25.0, 16.0, 11.0, 4.0]
-const MINNOW_ROLL_WEIGHTS: Array[float] = [34.0, 27.0, 20.0, 13.0, 6.0]
-const SARDINE_ROLL_WEIGHTS: Array[float] = [25.0, 28.0, 23.0, 16.0, 8.0]
-const LUMINOUS_ROE_ROLL_WEIGHTS: Array[float] = [16.0, 26.0, 26.0, 20.0, 12.0]
-const SALE_MULTIPLIERS: Array[float] = [1.0, 1.1, 1.25, 1.5, 2.0]
-# Legacy multiplier helpers remain available for compatibility. New encounters
-# roll directly within the authored quality bands below.
-const BARRIER_HEALTH_MULTIPLIERS: Array[float] = [1.0, 1.25, 1.6, 2.2, 3.25]
-# Inclusive per-barrier health bands authored for each quality tier. Encounter
-# generation rolls uniformly across the selected tier's complete band.
-const BARRIER_HEALTH_MINIMUMS: Array[int] = [1, 10, 50, 100, 200]
-const BARRIER_HEALTH_MAXIMUMS: Array[int] = [9, 50, 100, 200, 400]
-const DISPLAY_NAMES: PackedStringArray = [
- "boring",
- "average",
- "impressive",
- "exceptional",
- "shiny",
-]
-
-
-static func is_valid(quality: int) -> bool:
- return quality >= 0 and quality < TIER_COUNT
-
-
-static func display_name(quality: int) -> String:
- return DISPLAY_NAMES[quality] if is_valid(quality) else "unknown"
-
-
-static func bit_for(quality: int) -> int:
- return 1 << quality if is_valid(quality) else 0
-
-
-static func sale_multiplier(quality: int) -> float:
- return SALE_MULTIPLIERS[quality] if is_valid(quality) else 1.0
-
-
-static func apply_sale_value(base_value: int, quality: int) -> int:
- if base_value <= 0 or not is_valid(quality):
- return maxi(base_value, 0)
- if quality == Tier.BORING:
- return base_value
- return maxi(
- base_value + quality,
- ceili(float(base_value) * sale_multiplier(quality)),
- )
-
-
-static func barrier_health_multiplier(quality: int) -> float:
- return (
- BARRIER_HEALTH_MULTIPLIERS[quality]
- if is_valid(quality)
- else BARRIER_HEALTH_MULTIPLIERS[Tier.BORING]
- )
-
-
-static func apply_barrier_health(base_health: int, quality: int) -> int:
- return maxi(
- ceili(
- float(maxi(base_health, 1))
- * barrier_health_multiplier(quality)
- ),
- 1,
- )
-
-
-static func barrier_health_range(quality: int) -> Vector2i:
- var safe_quality: int = (
- quality if is_valid(quality) else Tier.BORING
- )
- return Vector2i(
- BARRIER_HEALTH_MINIMUMS[safe_quality],
- BARRIER_HEALTH_MAXIMUMS[safe_quality],
- )
-
-
-static func roll_barrier_health(
- rng: RandomNumberGenerator,
- quality: int,
-) -> int:
- var health_range: Vector2i = barrier_health_range(quality)
- if rng == null:
- return health_range.x
- return rng.randi_range(health_range.x, health_range.y)
-
-
-static func roll(
- rng: RandomNumberGenerator,
- weight_multipliers: Array[float] = [],
-) -> int:
- if rng == null:
- return Tier.BORING
- var weights: Array[float] = []
- var total_weight: float = 0.0
- for quality: int in TIER_COUNT:
- var multiplier: float = 1.0
- if quality < weight_multipliers.size():
- multiplier = maxf(weight_multipliers[quality], 0.0)
- var weight: float = BASE_ROLL_WEIGHTS[quality] * multiplier
- weights.append(weight)
- total_weight += weight
- if total_weight <= 0.0:
- return Tier.BORING
- var rolled_weight: float = rng.randf() * total_weight
- var accumulated_weight: float = 0.0
- for quality: int in TIER_COUNT:
- accumulated_weight += weights[quality]
- if rolled_weight <= accumulated_weight:
- return quality
- return Tier.SHINY
-
-
-static func roll_weights_for_bait(active_bait_tags: Array[StringName]) -> Array[float]:
- var bait_weights: Array[float] = rarity_weights_for_bait(active_bait_tags)
- var weights: Array[float] = []
- for quality: int in TIER_COUNT:
- weights.append(bait_weights[quality] / BASE_ROLL_WEIGHTS[quality])
- return weights
-
-
-static func rarity_weights_for_bait(
- active_bait_tags: Array[StringName],
-) -> Array[float]:
- var weights: Array[float] = []
- if active_bait_tags.is_empty():
- weights.assign([100.0, 0.0, 0.0, 0.0, 0.0])
- elif active_bait_tags.has(&"bait_rarity_6"):
- weights.assign(LUMINOUS_ROE_ROLL_WEIGHTS)
- elif active_bait_tags.has(&"bait_rarity_5"):
- weights.assign(SARDINE_ROLL_WEIGHTS)
- elif active_bait_tags.has(&"bait_rarity_4"):
- weights.assign(MINNOW_ROLL_WEIGHTS)
- elif active_bait_tags.has(&"bait_rarity_3"):
- weights.assign(SQUID_ROLL_WEIGHTS)
- elif active_bait_tags.has(&"bait_rarity_2"):
- weights.assign(SHRIMP_ROLL_WEIGHTS)
- elif active_bait_tags.has(&"bait_rarity_1"):
- weights.assign(SNAIL_ROLL_WEIGHTS)
- else:
- # Unknown and legacy bait tags retain the original worms behavior.
- weights.assign(WORM_ROLL_WEIGHTS)
- return weights
-
-
-static func qualified_name(fish_name: String, quality: int) -> String:
- return "%s %s" % [display_name(quality), fish_name]
-
-
-static func qualified_name_with_article(
- fish_name: String,
- quality: int,
-) -> String:
- var quality_name: String = display_name(quality)
- var article: String = "an" if quality_name[0] in ["a", "e", "i", "o", "u"] else "a"
- return "%s %s %s" % [article, quality_name, fish_name]
diff --git a/fish/fish_quality.gd.uid b/fish/fish_quality.gd.uid
deleted file mode 100644
index 0875763..0000000
--- a/fish/fish_quality.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://csmvl1ciuux6a
diff --git a/fish/fish_selector.gd b/fish/fish_selector.gd
index c2413bf..3c1656b 100644
--- a/fish/fish_selector.gd
+++ b/fish/fish_selector.gd
@@ -5,18 +5,13 @@ const CollectionLogType = preload("res://collection/collection_log.gd")
const FishCatchType = preload("res://fish/fish_catch.gd")
const FishDataType = preload("res://fish/fish_data.gd")
const FishPoolType = preload("res://fish/fish_pool.gd")
-const FishQualityType = preload("res://fish/fish_quality.gd")
const FishingContextType = preload("res://fishing/fishing_context.gd")
-const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
var undiscovered_weight_multiplier: float = 1.5
var rarity_weight_multipliers: Array[float] = []
-var quality_weight_multipliers: Array[float] = []
-var active_rod: FishingRodDataType
var use_deterministic_test_seed: bool = false
var deterministic_test_seed: int = 24680
var selection_seed: int = 0
-var _active_bait_tags: Array[StringName] = []
var _rng := RandomNumberGenerator.new()
@@ -37,49 +32,27 @@ func select_fish(
) -> FishDataType:
if pool == null or context == null or collection_log == null:
return null
- _active_bait_tags = context.active_bait_tags.duplicate()
- var all_fish: Array[FishDataType] = []
- var all_weights: Array[float] = []
- var rarity_fish: Array[FishDataType] = []
- var rarity_weights: Array[float] = []
- var selected_rarity: int = _roll_rarity(context)
+
+ var eligible_fish: Array[FishDataType] = []
var weights: Array[float] = []
var total_weight: float = 0.0
for fish: FishDataType in pool.candidates:
- if fish == null or not fish.is_fishable():
- continue
- if not fish.is_available_in_season(context.season):
- continue
- if not fish.is_allowed_in_water(context.water_type):
+ if fish == null or not fish.is_selectable():
continue
if fish.availability != null and not fish.availability.is_available(context):
continue
var final_weight: float = fish.base_catch_weight
- # Species-specific preferred bait is deprecated. Bait now controls
- # quality and rarity bands globally; required tags remain authoritative.
+ if fish.availability != null:
+ final_weight *= fish.availability.get_bait_weight_multiplier(context)
if not collection_log.has_discovered(fish.id):
final_weight *= maxf(undiscovered_weight_multiplier, 0.0)
- if active_rod != null:
- final_weight *= maxf(
- active_rod.undiscovered_weight_multiplier,
- 0.0,
- )
if fish.rarity >= 0 and fish.rarity < rarity_weight_multipliers.size():
final_weight *= maxf(rarity_weight_multipliers[fish.rarity], 0.0)
- if active_rod != null:
- final_weight *= active_rod.get_rarity_multiplier(fish.rarity)
- final_weight *= active_rod.get_affinity_multiplier(fish, context)
if final_weight <= 0.0:
continue
- all_fish.append(fish)
- all_weights.append(final_weight)
- if int(fish.rarity) == selected_rarity:
- rarity_fish.append(fish)
- rarity_weights.append(final_weight)
- var eligible_fish: Array[FishDataType] = rarity_fish if not rarity_fish.is_empty() else all_fish
- weights = rarity_weights if not rarity_weights.is_empty() else all_weights
- for weight: float in weights:
- total_weight += weight
+ eligible_fish.append(fish)
+ weights.append(final_weight)
+ total_weight += final_weight
if eligible_fish.is_empty() or total_weight <= 0.0:
return null
@@ -92,72 +65,21 @@ func select_fish(
return eligible_fish.back()
-func _roll_rarity(context: FishingContextType) -> int:
- var weights: Array[float] = FishQualityType.rarity_weights_for_bait(
- context.active_bait_tags
- )
- for index: int in range(weights.size()):
- if index < rarity_weight_multipliers.size():
- weights[index] *= maxf(rarity_weight_multipliers[index], 0.0)
- if active_rod != null:
- weights[index] *= active_rod.get_rarity_multiplier(index)
- var total: float = 0.0
- for weight: float in weights:
- total += weight
- if total <= 0.0:
- return FishDataType.Rarity.COMMON
- var roll: float = _rng.randf() * total
- for index: int in range(weights.size()):
- roll -= weights[index]
- if roll <= 0.0:
- return index
- return FishDataType.Rarity.LEGENDARY
-
-
-func create_catch(fish: FishDataType, bait_tags: Array[StringName] = []) -> FishCatchType:
+func create_catch(fish: FishDataType) -> FishCatchType:
if fish == null or not fish.is_selectable():
return null
var caught_fish := FishCatchType.new()
caught_fish.fish = fish
caught_fish.fish_id = fish.id
caught_fish.ensure_identity()
- var weight_roll: float = _biased_unit_roll(
- active_rod.weight_roll_bias if active_rod != null else 0.0
- )
- caught_fish.weight_lb = lerpf(
+ caught_fish.weight_lb = _rng.randf_range(
fish.get_minimum_weight(),
- fish.get_maximum_weight(),
- weight_roll,
+ fish.get_maximum_weight()
)
caught_fish.display_scale = fish.get_display_scale_for_weight(
caught_fish.weight_lb
)
- var effective_bait: Array[StringName] = bait_tags if not bait_tags.is_empty() else _active_bait_tags
- var quality_multipliers: Array[float] = FishQualityType.roll_weights_for_bait(effective_bait)
- for quality: int in range(mini(quality_multipliers.size(), quality_weight_multipliers.size())):
- quality_multipliers[quality] *= maxf(quality_weight_multipliers[quality], 0.0)
- for quality: int in range(quality_multipliers.size()):
- if active_rod != null:
- quality_multipliers[quality] *= active_rod.get_quality_multiplier(
- quality
- )
- caught_fish.quality = FishQualityType.roll(
- _rng,
- quality_multipliers,
- )
- caught_fish.sale_value = FishQualityType.apply_sale_value(
- fish.get_sale_value_for_weight(caught_fish.weight_lb),
- caught_fish.quality,
+ caught_fish.sale_value = fish.get_sale_value_for_weight(
+ caught_fish.weight_lb
)
return caught_fish
-
-
-func _biased_unit_roll(bias: float) -> float:
- var roll: float = _rng.randf()
- var safe_bias: float = clampf(bias, -0.5, 0.5)
- if is_zero_approx(safe_bias):
- return roll
- var exponent: float = 1.0 / (1.0 + absf(safe_bias) * 2.0)
- if safe_bias > 0.0:
- return pow(roll, exponent)
- return 1.0 - pow(1.0 - roll, exponent)
diff --git a/fish/pools/fish_catalog.tres b/fish/pools/fish_catalog.tres
deleted file mode 100644
index 0f27f15..0000000
--- a/fish/pools/fish_catalog.tres
+++ /dev/null
@@ -1,323 +0,0 @@
-[gd_resource type="Resource" load_steps=317 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_pool.gd" id="1_pool"]
-[ext_resource type="Resource" path="res://fish/species/bluegill/bluegill.tres" id="2_bluegill"]
-[ext_resource type="Resource" path="res://fish/species/bass/bass.tres" id="3_bass"]
-[ext_resource type="Resource" path="res://fish/species/carp/carp.tres" id="4_carp"]
-[ext_resource type="Resource" path="res://fish/species/sunfish/sunfish.tres" id="5_sunfish"]
-[ext_resource type="Resource" path="res://fish/species/catfish_blue/catfish_blue.tres" id="6_catfish_blue"]
-[ext_resource type="Resource" path="res://fish/species/catfish_channel/catfish_channel.tres" id="7_catfish_channel"]
-[ext_resource type="Resource" path="res://fish/species/catfish_flathead/catfish_flathead.tres" id="8_catfish_flathead"]
-[ext_resource type="Resource" path="res://fish/species/catfish_white/catfish_white.tres" id="9_catfish_white"]
-[ext_resource type="Resource" path="res://fish/species/tuna_albacore/tuna_albacore.tres" id="10_tuna_albacore"]
-[ext_resource type="Resource" path="res://fish/species/tuna_bigeye/tuna_bigeye.tres" id="11_tuna_bigeye"]
-[ext_resource type="Resource" path="res://fish/species/tuna_bluefin/tuna_bluefin.tres" id="12_tuna_bluefin"]
-[ext_resource type="Resource" path="res://fish/species/tuna_skipjack/tuna_skipjack.tres" id="13_tuna_skipjack"]
-[ext_resource type="Resource" path="res://fish/species/tuna_yellowfin/tuna_yellowfin.tres" id="14_tuna_yellowfin"]
-[ext_resource type="Resource" path="res://fish/species/goby_round/goby_round.tres" id="15_goby_round"]
-[ext_resource type="Resource" path="res://fish/species/salmon_atlantic/salmon_atlantic.tres" id="16_salmon_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/salmon_chum/salmon_chum.tres" id="17_salmon_chum"]
-[ext_resource type="Resource" path="res://fish/species/salmon_coho/salmon_coho.tres" id="18_salmon_coho"]
-[ext_resource type="Resource" path="res://fish/species/salmon_pink/salmon_pink.tres" id="19_salmon_pink"]
-[ext_resource type="Resource" path="res://fish/species/salmon_sockeye/salmon_sockeye.tres" id="20_salmon_sockeye"]
-[ext_resource type="Resource" path="res://fish/species/anchovy_european/anchovy_european.tres" id="21_anchovy_european"]
-[ext_resource type="Resource" path="res://fish/species/anchovy_northern/anchovy_northern.tres" id="22_anchovy_northern"]
-[ext_resource type="Resource" path="res://fish/species/chub_european/chub_european.tres" id="23_chub_european"]
-[ext_resource type="Resource" path="res://fish/species/chub_flame/chub_flame.tres" id="24_chub_flame"]
-[ext_resource type="Resource" path="res://fish/species/chub_lake/chub_lake.tres" id="25_chub_lake"]
-[ext_resource type="Resource" path="res://fish/species/goldfish/goldfish.tres" id="26_goldfish"]
-[ext_resource type="Resource" path="res://fish/species/goldfish_bubbleeye/goldfish_bubbleeye.tres" id="27_goldfish_bubbleeye"]
-[ext_resource type="Resource" path="res://fish/species/grouper_gulf/grouper_gulf.tres" id="28_grouper_gulf"]
-[ext_resource type="Resource" path="res://fish/species/grouper_red/grouper_red.tres" id="29_grouper_red"]
-[ext_resource type="Resource" path="res://fish/species/mackerel_atlantic/mackerel_atlantic.tres" id="30_mackerel_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/mackerel_cero/mackerel_cero.tres" id="31_mackerel_cero"]
-[ext_resource type="Resource" path="res://fish/species/mackerel_chub/mackerel_chub.tres" id="32_mackerel_chub"]
-[ext_resource type="Resource" path="res://fish/species/mackerel_king/mackerel_king.tres" id="33_mackerel_king"]
-[ext_resource type="Resource" path="res://fish/species/mackerel_spanish/mackerel_spanish.tres" id="34_mackerel_spanish"]
-[ext_resource type="Resource" path="res://fish/species/marlin_black/marlin_black.tres" id="35_marlin_black"]
-[ext_resource type="Resource" path="res://fish/species/marlin_blue/marlin_blue.tres" id="36_marlin_blue"]
-[ext_resource type="Resource" path="res://fish/species/marlin_white/marlin_white.tres" id="37_marlin_white"]
-[ext_resource type="Resource" path="res://fish/species/pomfret_black/pomfret_black.tres" id="38_pomfret_black"]
-[ext_resource type="Resource" path="res://fish/species/pomfret_chinese/pomfret_chinese.tres" id="39_pomfret_chinese"]
-[ext_resource type="Resource" path="res://fish/species/pomfret_golden/pomfret_golden.tres" id="40_pomfret_golden"]
-[ext_resource type="Resource" path="res://fish/species/pomfret_white/pomfret_white.tres" id="41_pomfret_white"]
-[ext_resource type="Resource" path="res://fish/species/sailfish/sailfish.tres" id="42_sailfish"]
-[ext_resource type="Resource" path="res://fish/species/sauger/sauger.tres" id="43_sauger"]
-[ext_resource type="Resource" path="res://fish/species/saugeye/saugeye.tres" id="44_saugeye"]
-[ext_resource type="Resource" path="res://fish/species/snapper_lane/snapper_lane.tres" id="45_snapper_lane"]
-[ext_resource type="Resource" path="res://fish/species/snapper_mangrove/snapper_mangrove.tres" id="46_snapper_mangrove"]
-[ext_resource type="Resource" path="res://fish/species/snapper_mutton/snapper_mutton.tres" id="47_snapper_mutton"]
-[ext_resource type="Resource" path="res://fish/species/snapper_red/snapper_red.tres" id="48_snapper_red"]
-[ext_resource type="Resource" path="res://fish/species/swordfish/swordfish.tres" id="49_swordfish"]
-[ext_resource type="Resource" path="res://fish/species/trout_cutthroat/trout_cutthroat.tres" id="50_trout_cutthroat"]
-[ext_resource type="Resource" path="res://fish/species/trout_golden/trout_golden.tres" id="51_trout_golden"]
-[ext_resource type="Resource" path="res://fish/species/trout_rainbow/trout_rainbow.tres" id="52_trout_rainbow"]
-[ext_resource type="Resource" path="res://fish/species/trout_steelhead/trout_steelhead.tres" id="53_trout_steelhead"]
-[ext_resource type="Resource" path="res://fish/species/walleye/walleye.tres" id="54_walleye"]
-[ext_resource type="Resource" path="res://fish/species/bowfin/bowfin.tres" id="55_bowfin"]
-[ext_resource type="Resource" path="res://fish/species/sturgeon_lake/sturgeon_lake.tres" id="56_sturgeon_lake"]
-[ext_resource type="Resource" path="res://fish/species/gar_longnose/gar_longnose.tres" id="57_gar_longnose"]
-[ext_resource type="Resource" path="res://fish/species/paddlefish/paddlefish.tres" id="58_paddlefish"]
-[ext_resource type="Resource" path="res://fish/species/sturgeon_shovelnose/sturgeon_shovelnose.tres" id="59_sturgeon_shovelnose"]
-[ext_resource type="Resource" path="res://fish/species/gar_spotted/gar_spotted.tres" id="60_gar_spotted"]
-[ext_resource type="Resource" path="res://fish/species/lungfish_west_african/lungfish_west_african.tres" id="61_lungfish_west_african"]
-[ext_resource type="Resource" path="res://fish/species/mudskipper_atlantic/mudskipper_atlantic.tres" id="62_mudskipper_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/foureyes_largescale/foureyes_largescale.tres" id="63_foureyes_largescale"]
-[ext_resource type="Resource" path="res://fish/species/mullet_red/mullet_red.tres" id="64_mullet_red"]
-[ext_resource type="Resource" path="res://fish/species/plaice_european/plaice_european.tres" id="65_plaice_european"]
-[ext_resource type="Resource" path="res://fish/species/flounder_summer/flounder_summer.tres" id="66_flounder_summer"]
-[ext_resource type="Resource" path="res://fish/species/flounder_winter/flounder_winter.tres" id="67_flounder_winter"]
-[ext_resource type="Resource" path="res://fish/species/dugong/dugong.tres" id="68_dugong"]
-[ext_resource type="Resource" path="res://fish/species/seal_harbor/seal_harbor.tres" id="69_seal_harbor"]
-[ext_resource type="Resource" path="res://fish/species/sea_otter/sea_otter.tres" id="70_sea_otter"]
-[ext_resource type="Resource" path="res://fish/species/walrus/walrus.tres" id="71_walrus"]
-[ext_resource type="Resource" path="res://fish/species/manatee_west_indian/manatee_west_indian.tres" id="72_manatee_west_indian"]
-[ext_resource type="Resource" path="res://fish/species/pipefish_bay/pipefish_bay.tres" id="73_pipefish_bay"]
-[ext_resource type="Resource" path="res://fish/species/seahorse_lined/seahorse_lined.tres" id="74_seahorse_lined"]
-[ext_resource type="Resource" path="res://fish/species/monkfish/monkfish.tres" id="75_monkfish"]
-[ext_resource type="Resource" path="res://fish/species/tripletail/tripletail.tres" id="76_tripletail"]
-[ext_resource type="Resource" path="res://fish/species/bluefish/bluefish.tres" id="77_bluefish"]
-[ext_resource type="Resource" path="res://fish/species/jack_crevalle/jack_crevalle.tres" id="78_jack_crevalle"]
-[ext_resource type="Resource" path="res://fish/species/queenfish_talang/queenfish_talang.tres" id="79_queenfish_talang"]
-[ext_resource type="Resource" path="res://fish/species/sand_lance_american/sand_lance_american.tres" id="80_sand_lance_american"]
-[ext_resource type="Resource" path="res://fish/species/menhaden_atlantic/menhaden_atlantic.tres" id="81_menhaden_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/sardine_european/sardine_european.tres" id="82_sardine_european"]
-[ext_resource type="Resource" path="res://fish/species/sardine_pacific/sardine_pacific.tres" id="83_sardine_pacific"]
-[ext_resource type="Resource" path="res://fish/species/shark_blacktip/shark_blacktip.tres" id="84_shark_blacktip"]
-[ext_resource type="Resource" path="res://fish/species/shark_great_hammerhead/shark_great_hammerhead.tres" id="85_shark_great_hammerhead"]
-[ext_resource type="Resource" path="res://fish/species/shark_great_white/shark_great_white.tres" id="86_shark_great_white"]
-[ext_resource type="Resource" path="res://fish/species/shark_tiger/shark_tiger.tres" id="87_shark_tiger"]
-[ext_resource type="Resource" path="res://fish/species/shark_whale/shark_whale.tres" id="88_shark_whale"]
-[ext_resource type="Resource" path="res://fish/species/burbot/burbot.tres" id="89_burbot"]
-[ext_resource type="Resource" path="res://fish/species/cisco/cisco.tres" id="90_cisco"]
-[ext_resource type="Resource" path="res://fish/species/whitefish_lake/whitefish_lake.tres" id="91_whitefish_lake"]
-[ext_resource type="Resource" path="res://fish/species/char_arctic/char_arctic.tres" id="92_char_arctic"]
-[ext_resource type="Resource" path="res://fish/species/grayling_arctic/grayling_arctic.tres" id="93_grayling_arctic"]
-[ext_resource type="Resource" path="res://fish/species/trout_brown/trout_brown.tres" id="94_trout_brown"]
-[ext_resource type="Resource" path="res://fish/species/wolffish_atlantic/wolffish_atlantic.tres" id="95_wolffish_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/cusk/cusk.tres" id="96_cusk"]
-[ext_resource type="Resource" path="res://fish/species/wolffish_spotted/wolffish_spotted.tres" id="97_wolffish_spotted"]
-[ext_resource type="Resource" path="res://fish/species/herring_atlantic/herring_atlantic.tres" id="98_herring_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/capelin/capelin.tres" id="99_capelin"]
-[ext_resource type="Resource" path="res://fish/species/sprat_european/sprat_european.tres" id="100_sprat_european"]
-[ext_resource type="Resource" path="res://fish/species/herring_pacific/herring_pacific.tres" id="101_herring_pacific"]
-[ext_resource type="Resource" path="res://fish/species/pollock_alaska/pollock_alaska.tres" id="102_pollock_alaska"]
-[ext_resource type="Resource" path="res://fish/species/cod_atlantic/cod_atlantic.tres" id="103_cod_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/pollock_atlantic/pollock_atlantic.tres" id="104_pollock_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/sole_dover/sole_dover.tres" id="105_sole_dover"]
-[ext_resource type="Resource" path="res://fish/species/haddock/haddock.tres" id="106_haddock"]
-[ext_resource type="Resource" path="res://fish/species/cod_pacific/cod_pacific.tres" id="107_cod_pacific"]
-[ext_resource type="Resource" path="res://fish/species/hake_pacific/hake_pacific.tres" id="108_hake_pacific"]
-[ext_resource type="Resource" path="res://fish/species/hake_silver/hake_silver.tres" id="109_hake_silver"]
-[ext_resource type="Resource" path="res://fish/species/nautilus_chambered/nautilus_chambered.tres" id="110_nautilus_chambered"]
-[ext_resource type="Resource" path="res://fish/species/cuttlefish_common/cuttlefish_common.tres" id="111_cuttlefish_common"]
-[ext_resource type="Resource" path="res://fish/species/cuttlefish_flamboyant/cuttlefish_flamboyant.tres" id="112_cuttlefish_flamboyant"]
-[ext_resource type="Resource" path="res://fish/species/tilefish_blueline/tilefish_blueline.tres" id="113_tilefish_blueline"]
-[ext_resource type="Resource" path="res://fish/species/tilefish_golden/tilefish_golden.tres" id="114_tilefish_golden"]
-[ext_resource type="Resource" path="res://fish/species/ocean_perch_pacific/ocean_perch_pacific.tres" id="115_ocean_perch_pacific"]
-[ext_resource type="Resource" path="res://fish/species/sea_bass_chilean/sea_bass_chilean.tres" id="116_sea_bass_chilean"]
-[ext_resource type="Resource" path="res://fish/species/sablefish/sablefish.tres" id="117_sablefish"]
-[ext_resource type="Resource" path="res://fish/species/wreckfish/wreckfish.tres" id="118_wreckfish"]
-[ext_resource type="Resource" path="res://fish/species/oarfish_giant/oarfish_giant.tres" id="119_oarfish_giant"]
-[ext_resource type="Resource" path="res://fish/species/coelacanth_west_indian/coelacanth_west_indian.tres" id="120_coelacanth_west_indian"]
-[ext_resource type="Resource" path="res://fish/species/anglerfish_black_seadevil/anglerfish_black_seadevil.tres" id="121_anglerfish_black_seadevil"]
-[ext_resource type="Resource" path="res://fish/species/fangtooth_common/fangtooth_common.tres" id="122_fangtooth_common"]
-[ext_resource type="Resource" path="res://fish/species/gulper_eel/gulper_eel.tres" id="123_gulper_eel"]
-[ext_resource type="Resource" path="res://fish/species/barreleye_pacific/barreleye_pacific.tres" id="124_barreleye_pacific"]
-[ext_resource type="Resource" path="res://fish/species/blobfish_smooth_head/blobfish_smooth_head.tres" id="125_blobfish_smooth_head"]
-[ext_resource type="Resource" path="res://fish/species/lanternfish_spotted/lanternfish_spotted.tres" id="126_lanternfish_spotted"]
-[ext_resource type="Resource" path="res://fish/species/sea_star_crown_of_thorns/sea_star_crown_of_thorns.tres" id="127_sea_star_crown_of_thorns"]
-[ext_resource type="Resource" path="res://fish/species/sea_cucumber_giant_california/sea_cucumber_giant_california.tres" id="128_sea_cucumber_giant_california"]
-[ext_resource type="Resource" path="res://fish/species/urchin_purple/urchin_purple.tres" id="129_urchin_purple"]
-[ext_resource type="Resource" path="res://fish/species/sand_dollar/sand_dollar.tres" id="130_sand_dollar"]
-[ext_resource type="Resource" path="res://fish/species/sea_star_sunflower/sea_star_sunflower.tres" id="131_sea_star_sunflower"]
-[ext_resource type="Resource" path="res://fish/species/axolotl/axolotl.tres" id="132_axolotl"]
-[ext_resource type="Resource" path="res://fish/species/hellbender/hellbender.tres" id="133_hellbender"]
-[ext_resource type="Resource" path="res://fish/species/guppy/guppy.tres" id="134_guppy"]
-[ext_resource type="Resource" path="res://fish/species/tetra_neon/tetra_neon.tres" id="135_tetra_neon"]
-[ext_resource type="Resource" path="res://fish/species/danio_zebra/danio_zebra.tres" id="136_danio_zebra"]
-[ext_resource type="Resource" path="res://fish/species/largemouth_bass/largemouth_bass.tres" id="137_largemouth_bass"]
-[ext_resource type="Resource" path="res://fish/species/smallmouth_bass/smallmouth_bass.tres" id="138_smallmouth_bass"]
-[ext_resource type="Resource" path="res://fish/species/buffalo_bigmouth/buffalo_bigmouth.tres" id="139_buffalo_bigmouth"]
-[ext_resource type="Resource" path="res://fish/species/redhorse_golden/redhorse_golden.tres" id="140_redhorse_golden"]
-[ext_resource type="Resource" path="res://fish/species/quillback/quillback.tres" id="141_quillback"]
-[ext_resource type="Resource" path="res://fish/species/buffalo_smallmouth/buffalo_smallmouth.tres" id="142_buffalo_smallmouth"]
-[ext_resource type="Resource" path="res://fish/species/sucker_white/sucker_white.tres" id="143_sucker_white"]
-[ext_resource type="Resource" path="res://fish/species/bullhead_black/bullhead_black.tres" id="144_bullhead_black"]
-[ext_resource type="Resource" path="res://fish/species/bullhead_brown/bullhead_brown.tres" id="145_bullhead_brown"]
-[ext_resource type="Resource" path="res://fish/species/madtom_tadpole/madtom_tadpole.tres" id="146_madtom_tadpole"]
-[ext_resource type="Resource" path="res://fish/species/bullhead_yellow/bullhead_yellow.tres" id="147_bullhead_yellow"]
-[ext_resource type="Resource" path="res://fish/species/barbel_common/barbel_common.tres" id="148_barbel_common"]
-[ext_resource type="Resource" path="res://fish/species/bream_common/bream_common.tres" id="149_bream_common"]
-[ext_resource type="Resource" path="res://fish/species/roach_common/roach_common.tres" id="150_roach_common"]
-[ext_resource type="Resource" path="res://fish/species/fallfish/fallfish.tres" id="151_fallfish"]
-[ext_resource type="Resource" path="res://fish/species/ide/ide.tres" id="152_ide"]
-[ext_resource type="Resource" path="res://fish/species/rudd/rudd.tres" id="153_rudd"]
-[ext_resource type="Resource" path="res://fish/species/tench/tench.tres" id="154_tench"]
-[ext_resource type="Resource" path="res://fish/species/crayfish_red_swamp/crayfish_red_swamp.tres" id="155_crayfish_red_swamp"]
-[ext_resource type="Resource" path="res://fish/species/gar_alligator/gar_alligator.tres" id="156_gar_alligator"]
-[ext_resource type="Resource" path="res://fish/species/arapaima/arapaima.tres" id="157_arapaima"]
-[ext_resource type="Resource" path="res://fish/species/tigerfish_goliath/tigerfish_goliath.tres" id="158_tigerfish_goliath"]
-[ext_resource type="Resource" path="res://fish/species/muskellunge/muskellunge.tres" id="159_muskellunge"]
-[ext_resource type="Resource" path="res://fish/species/shiner_common/shiner_common.tres" id="160_shiner_common"]
-[ext_resource type="Resource" path="res://fish/species/chub_creek/chub_creek.tres" id="161_chub_creek"]
-[ext_resource type="Resource" path="res://fish/species/shiner_emerald/shiner_emerald.tres" id="162_shiner_emerald"]
-[ext_resource type="Resource" path="res://fish/species/minnow_fathead/minnow_fathead.tres" id="163_minnow_fathead"]
-[ext_resource type="Resource" path="res://fish/species/shiner_golden/shiner_golden.tres" id="164_shiner_golden"]
-[ext_resource type="Resource" path="res://fish/species/electric_eel/electric_eel.tres" id="165_electric_eel"]
-[ext_resource type="Resource" path="res://fish/species/freshwater_drum/freshwater_drum.tres" id="166_freshwater_drum"]
-[ext_resource type="Resource" path="res://fish/species/goldeye/goldeye.tres" id="167_goldeye"]
-[ext_resource type="Resource" path="res://fish/species/mooneye/mooneye.tres" id="168_mooneye"]
-[ext_resource type="Resource" path="res://fish/species/loach_weather/loach_weather.tres" id="169_loach_weather"]
-[ext_resource type="Resource" path="res://fish/species/koi/koi.tres" id="170_koi"]
-[ext_resource type="Resource" path="res://fish/species/crappie_black/crappie_black.tres" id="171_crappie_black"]
-[ext_resource type="Resource" path="res://fish/species/green_sunfish/green_sunfish.tres" id="172_green_sunfish"]
-[ext_resource type="Resource" path="res://fish/species/pumpkinseed/pumpkinseed.tres" id="173_pumpkinseed"]
-[ext_resource type="Resource" path="res://fish/species/redear_sunfish/redear_sunfish.tres" id="174_redear_sunfish"]
-[ext_resource type="Resource" path="res://fish/species/warmouth/warmouth.tres" id="175_warmouth"]
-[ext_resource type="Resource" path="res://fish/species/crappie_white/crappie_white.tres" id="176_crappie_white"]
-[ext_resource type="Resource" path="res://fish/species/yellow_perch/yellow_perch.tres" id="177_yellow_perch"]
-[ext_resource type="Resource" path="res://fish/species/pickerel_chain/pickerel_chain.tres" id="178_pickerel_chain"]
-[ext_resource type="Resource" path="res://fish/species/pickerel_grass/pickerel_grass.tres" id="179_pickerel_grass"]
-[ext_resource type="Resource" path="res://fish/species/pike_northern/pike_northern.tres" id="180_pike_northern"]
-[ext_resource type="Resource" path="res://fish/species/jelly_crystal/jelly_crystal.tres" id="181_jelly_crystal"]
-[ext_resource type="Resource" path="res://fish/species/jelly_lions_mane/jelly_lions_mane.tres" id="182_jelly_lions_mane"]
-[ext_resource type="Resource" path="res://fish/species/jelly_moon/jelly_moon.tres" id="183_jelly_moon"]
-[ext_resource type="Resource" path="res://fish/species/jelly_pacific_sea_nettle/jelly_pacific_sea_nettle.tres" id="184_jelly_pacific_sea_nettle"]
-[ext_resource type="Resource" path="res://fish/species/jelly_sea_wasp/jelly_sea_wasp.tres" id="185_jelly_sea_wasp"]
-[ext_resource type="Resource" path="res://fish/species/pompano_african/pompano_african.tres" id="186_pompano_african"]
-[ext_resource type="Resource" path="res://fish/species/cobia/cobia.tres" id="187_cobia"]
-[ext_resource type="Resource" path="res://fish/species/trevally_giant/trevally_giant.tres" id="188_trevally_giant"]
-[ext_resource type="Resource" path="res://fish/species/amberjack_greater/amberjack_greater.tres" id="189_amberjack_greater"]
-[ext_resource type="Resource" path="res://fish/species/amberjack_lesser/amberjack_lesser.tres" id="190_amberjack_lesser"]
-[ext_resource type="Resource" path="res://fish/species/halibut_atlantic/halibut_atlantic.tres" id="191_halibut_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/halibut_pacific/halibut_pacific.tres" id="192_halibut_pacific"]
-[ext_resource type="Resource" path="res://fish/species/turbot/turbot.tres" id="193_turbot"]
-[ext_resource type="Resource" path="res://fish/species/lobster_american/lobster_american.tres" id="194_lobster_american"]
-[ext_resource type="Resource" path="res://fish/species/krill_antarctic/krill_antarctic.tres" id="195_krill_antarctic"]
-[ext_resource type="Resource" path="res://fish/species/lobster_caribbean_spiny/lobster_caribbean_spiny.tres" id="196_lobster_caribbean_spiny"]
-[ext_resource type="Resource" path="res://fish/species/shrimp_peacock_mantis/shrimp_peacock_mantis.tres" id="197_shrimp_peacock_mantis"]
-[ext_resource type="Resource" path="res://fish/species/shrimp_skunk_cleaner/shrimp_skunk_cleaner.tres" id="198_shrimp_skunk_cleaner"]
-[ext_resource type="Resource" path="res://fish/species/crab_ghost/crab_ghost.tres" id="199_crab_ghost"]
-[ext_resource type="Resource" path="res://fish/species/crab_blue/crab_blue.tres" id="200_crab_blue"]
-[ext_resource type="Resource" path="res://fish/species/crab_dungeness/crab_dungeness.tres" id="201_crab_dungeness"]
-[ext_resource type="Resource" path="res://fish/species/crab_japanese_spider/crab_japanese_spider.tres" id="202_crab_japanese_spider"]
-[ext_resource type="Resource" path="res://fish/species/crab_red_king/crab_red_king.tres" id="203_crab_red_king"]
-[ext_resource type="Resource" path="res://fish/species/eel_american/eel_american.tres" id="204_eel_american"]
-[ext_resource type="Resource" path="res://fish/species/eel_european/eel_european.tres" id="205_eel_european"]
-[ext_resource type="Resource" path="res://fish/species/smelt_rainbow/smelt_rainbow.tres" id="206_smelt_rainbow"]
-[ext_resource type="Resource" path="res://fish/species/smelt_eulachon/smelt_eulachon.tres" id="207_smelt_eulachon"]
-[ext_resource type="Resource" path="res://fish/species/sturgeon_white/sturgeon_white.tres" id="208_sturgeon_white"]
-[ext_resource type="Resource" path="res://fish/species/mahi_mahi/mahi_mahi.tres" id="209_mahi_mahi"]
-[ext_resource type="Resource" path="res://fish/species/opah/opah.tres" id="210_opah"]
-[ext_resource type="Resource" path="res://fish/species/wahoo/wahoo.tres" id="211_wahoo"]
-[ext_resource type="Resource" path="res://fish/species/octopus_common/octopus_common.tres" id="212_octopus_common"]
-[ext_resource type="Resource" path="res://fish/species/octopus_day/octopus_day.tres" id="213_octopus_day"]
-[ext_resource type="Resource" path="res://fish/species/octopus_giant_pacific/octopus_giant_pacific.tres" id="214_octopus_giant_pacific"]
-[ext_resource type="Resource" path="res://fish/species/octopus_greater_blue_ringed/octopus_greater_blue_ringed.tres" id="215_octopus_greater_blue_ringed"]
-[ext_resource type="Resource" path="res://fish/species/skate_barndoor/skate_barndoor.tres" id="216_skate_barndoor"]
-[ext_resource type="Resource" path="res://fish/species/manta_ray_giant/manta_ray_giant.tres" id="217_manta_ray_giant"]
-[ext_resource type="Resource" path="res://fish/species/sawfish_largetooth/sawfish_largetooth.tres" id="218_sawfish_largetooth"]
-[ext_resource type="Resource" path="res://fish/species/stingray_ocellate_river/stingray_ocellate_river.tres" id="219_stingray_ocellate_river"]
-[ext_resource type="Resource" path="res://fish/species/stingray_southern/stingray_southern.tres" id="220_stingray_southern"]
-[ext_resource type="Resource" path="res://fish/species/lionfish_red/lionfish_red.tres" id="221_lionfish_red"]
-[ext_resource type="Resource" path="res://fish/species/scorpionfish_red/scorpionfish_red.tres" id="222_scorpionfish_red"]
-[ext_resource type="Resource" path="res://fish/species/moray_green/moray_green.tres" id="223_moray_green"]
-[ext_resource type="Resource" path="res://fish/species/flounder_peacock/flounder_peacock.tres" id="224_flounder_peacock"]
-[ext_resource type="Resource" path="res://fish/species/surgeonfish_blue/surgeonfish_blue.tres" id="225_surgeonfish_blue"]
-[ext_resource type="Resource" path="res://fish/species/parrotfish_rainbow/parrotfish_rainbow.tres" id="226_parrotfish_rainbow"]
-[ext_resource type="Resource" path="res://fish/species/parrotfish_stoplight/parrotfish_stoplight.tres" id="227_parrotfish_stoplight"]
-[ext_resource type="Resource" path="res://fish/species/tang_yellow/tang_yellow.tres" id="228_tang_yellow"]
-[ext_resource type="Resource" path="res://fish/species/trumpetfish_atlantic/trumpetfish_atlantic.tres" id="229_trumpetfish_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/triggerfish_gray/triggerfish_gray.tres" id="230_triggerfish_gray"]
-[ext_resource type="Resource" path="res://fish/species/pufferfish_guineafowl/pufferfish_guineafowl.tres" id="231_pufferfish_guineafowl"]
-[ext_resource type="Resource" path="res://fish/species/cowfish_longhorn/cowfish_longhorn.tres" id="232_cowfish_longhorn"]
-[ext_resource type="Resource" path="res://fish/species/triggerfish_queen/triggerfish_queen.tres" id="233_triggerfish_queen"]
-[ext_resource type="Resource" path="res://fish/species/cornetfish_red/cornetfish_red.tres" id="234_cornetfish_red"]
-[ext_resource type="Resource" path="res://fish/species/porcupinefish_spotted/porcupinefish_spotted.tres" id="235_porcupinefish_spotted"]
-[ext_resource type="Resource" path="res://fish/species/boxfish_yellow/boxfish_yellow.tres" id="236_boxfish_yellow"]
-[ext_resource type="Resource" path="res://fish/species/butterflyfish_copperband/butterflyfish_copperband.tres" id="237_butterflyfish_copperband"]
-[ext_resource type="Resource" path="res://fish/species/clownfish_ocellaris/clownfish_ocellaris.tres" id="238_clownfish_ocellaris"]
-[ext_resource type="Resource" path="res://fish/species/angelfish_queen/angelfish_queen.tres" id="239_angelfish_queen"]
-[ext_resource type="Resource" path="res://fish/species/grouper_atlantic_goliath/grouper_atlantic_goliath.tres" id="240_grouper_atlantic_goliath"]
-[ext_resource type="Resource" path="res://fish/species/trevally_bluefin/trevally_bluefin.tres" id="241_trevally_bluefin"]
-[ext_resource type="Resource" path="res://fish/species/grouper_giant/grouper_giant.tres" id="242_grouper_giant"]
-[ext_resource type="Resource" path="res://fish/species/barracuda_great/barracuda_great.tres" id="243_barracuda_great"]
-[ext_resource type="Resource" path="res://fish/species/grouper_nassau/grouper_nassau.tres" id="244_grouper_nassau"]
-[ext_resource type="Resource" path="res://fish/species/chromis_blue_green/chromis_blue_green.tres" id="245_chromis_blue_green"]
-[ext_resource type="Resource" path="res://fish/species/grunt_bluestriped/grunt_bluestriped.tres" id="246_grunt_bluestriped"]
-[ext_resource type="Resource" path="res://fish/species/grunt_french/grunt_french.tres" id="247_grunt_french"]
-[ext_resource type="Resource" path="res://fish/species/damselfish_sergeant_major/damselfish_sergeant_major.tres" id="248_damselfish_sergeant_major"]
-[ext_resource type="Resource" path="res://fish/species/wrasse_ballan/wrasse_ballan.tres" id="249_wrasse_ballan"]
-[ext_resource type="Resource" path="res://fish/species/wrasse_cleaner/wrasse_cleaner.tres" id="250_wrasse_cleaner"]
-[ext_resource type="Resource" path="res://fish/species/hogfish/hogfish.tres" id="251_hogfish"]
-[ext_resource type="Resource" path="res://fish/species/turtle_green/turtle_green.tres" id="252_turtle_green"]
-[ext_resource type="Resource" path="res://fish/species/turtle_hawksbill/turtle_hawksbill.tres" id="253_turtle_hawksbill"]
-[ext_resource type="Resource" path="res://fish/species/turtle_leatherback/turtle_leatherback.tres" id="254_turtle_leatherback"]
-[ext_resource type="Resource" path="res://fish/species/turtle_loggerhead/turtle_loggerhead.tres" id="255_turtle_loggerhead"]
-[ext_resource type="Resource" path="res://fish/species/seadragon_leafy/seadragon_leafy.tres" id="256_seadragon_leafy"]
-[ext_resource type="Resource" path="res://fish/species/seadragon_weedy/seadragon_weedy.tres" id="257_seadragon_weedy"]
-[ext_resource type="Resource" path="res://fish/species/oyster_black_lip_pearl/oyster_black_lip_pearl.tres" id="258_oyster_black_lip_pearl"]
-[ext_resource type="Resource" path="res://fish/species/clam_giant/clam_giant.tres" id="259_clam_giant"]
-[ext_resource type="Resource" path="res://fish/species/clam_geoduck/clam_geoduck.tres" id="260_clam_geoduck"]
-[ext_resource type="Resource" path="res://fish/species/conch_queen/conch_queen.tres" id="261_conch_queen"]
-[ext_resource type="Resource" path="res://fish/species/abalone_red/abalone_red.tres" id="262_abalone_red"]
-[ext_resource type="Resource" path="res://fish/species/squid_bigfin_reef/squid_bigfin_reef.tres" id="263_squid_bigfin_reef"]
-[ext_resource type="Resource" path="res://fish/species/squid_colossal/squid_colossal.tres" id="264_squid_colossal"]
-[ext_resource type="Resource" path="res://fish/species/squid_giant/squid_giant.tres" id="265_squid_giant"]
-[ext_resource type="Resource" path="res://fish/species/squid_humboldt/squid_humboldt.tres" id="266_squid_humboldt"]
-[ext_resource type="Resource" path="res://fish/species/halfbeak_ballyhoo/halfbeak_ballyhoo.tres" id="267_halfbeak_ballyhoo"]
-[ext_resource type="Resource" path="res://fish/species/needlefish_hound/needlefish_hound.tres" id="268_needlefish_hound"]
-[ext_resource type="Resource" path="res://fish/species/flyingfish_tropical/flyingfish_tropical.tres" id="269_flyingfish_tropical"]
-[ext_resource type="Resource" path="res://fish/species/croaker_atlantic/croaker_atlantic.tres" id="270_croaker_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/black_drum/black_drum.tres" id="271_black_drum"]
-[ext_resource type="Resource" path="res://fish/species/red_drum/red_drum.tres" id="272_red_drum"]
-[ext_resource type="Resource" path="res://fish/species/mullet_striped/mullet_striped.tres" id="273_mullet_striped"]
-[ext_resource type="Resource" path="res://fish/species/weakfish/weakfish.tres" id="274_weakfish"]
-[ext_resource type="Resource" path="res://fish/species/white_perch/white_perch.tres" id="275_white_perch"]
-[ext_resource type="Resource" path="res://fish/species/croaker_yellowfin/croaker_yellowfin.tres" id="276_croaker_yellowfin"]
-[ext_resource type="Resource" path="res://fish/species/rockfish_black/rockfish_black.tres" id="277_rockfish_black"]
-[ext_resource type="Resource" path="res://fish/species/sea_bass_black/sea_bass_black.tres" id="278_sea_bass_black"]
-[ext_resource type="Resource" path="res://fish/species/rockfish_canary/rockfish_canary.tres" id="279_rockfish_canary"]
-[ext_resource type="Resource" path="res://fish/species/porgy_scup/porgy_scup.tres" id="280_porgy_scup"]
-[ext_resource type="Resource" path="res://fish/species/sheepshead/sheepshead.tres" id="281_sheepshead"]
-[ext_resource type="Resource" path="res://fish/species/tautog/tautog.tres" id="282_tautog"]
-[ext_resource type="Resource" path="res://fish/species/rockfish_yelloweye/rockfish_yelloweye.tres" id="283_rockfish_yelloweye"]
-[ext_resource type="Resource" path="res://fish/species/lingcod/lingcod.tres" id="284_lingcod"]
-[ext_resource type="Resource" path="res://fish/species/milkfish/milkfish.tres" id="285_milkfish"]
-[ext_resource type="Resource" path="res://fish/species/roosterfish/roosterfish.tres" id="286_roosterfish"]
-[ext_resource type="Resource" path="res://fish/species/tarpon_atlantic/tarpon_atlantic.tres" id="287_tarpon_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/barramundi/barramundi.tres" id="288_barramundi"]
-[ext_resource type="Resource" path="res://fish/species/snook_common/snook_common.tres" id="289_snook_common"]
-[ext_resource type="Resource" path="res://fish/species/bonefish/bonefish.tres" id="290_bonefish"]
-[ext_resource type="Resource" path="res://fish/species/pompano_florida/pompano_florida.tres" id="291_pompano_florida"]
-[ext_resource type="Resource" path="res://fish/species/ladyfish/ladyfish.tres" id="292_ladyfish"]
-[ext_resource type="Resource" path="res://fish/species/permit/permit.tres" id="293_permit"]
-[ext_resource type="Resource" path="res://fish/species/knifefish_clown/knifefish_clown.tres" id="294_knifefish_clown"]
-[ext_resource type="Resource" path="res://fish/species/loach_clown/loach_clown.tres" id="295_loach_clown"]
-[ext_resource type="Resource" path="res://fish/species/discus/discus.tres" id="296_discus"]
-[ext_resource type="Resource" path="res://fish/species/angelfish_freshwater/angelfish_freshwater.tres" id="297_angelfish_freshwater"]
-[ext_resource type="Resource" path="res://fish/species/gourami_giant/gourami_giant.tres" id="298_gourami_giant"]
-[ext_resource type="Resource" path="res://fish/species/tilapia_nile/tilapia_nile.tres" id="299_tilapia_nile"]
-[ext_resource type="Resource" path="res://fish/species/oscar/oscar.tres" id="300_oscar"]
-[ext_resource type="Resource" path="res://fish/species/betta_siamese/betta_siamese.tres" id="301_betta_siamese"]
-[ext_resource type="Resource" path="res://fish/species/tambaqui/tambaqui.tres" id="302_tambaqui"]
-[ext_resource type="Resource" path="res://fish/species/catfish_walking/catfish_walking.tres" id="303_catfish_walking"]
-[ext_resource type="Resource" path="res://fish/species/snakehead_giant/snakehead_giant.tres" id="304_snakehead_giant"]
-[ext_resource type="Resource" path="res://fish/species/dorado_golden/dorado_golden.tres" id="305_dorado_golden"]
-[ext_resource type="Resource" path="res://fish/species/mahseer_golden/mahseer_golden.tres" id="306_mahseer_golden"]
-[ext_resource type="Resource" path="res://fish/species/peacock_bass/peacock_bass.tres" id="307_peacock_bass"]
-[ext_resource type="Resource" path="res://fish/species/piranha_red_bellied/piranha_red_bellied.tres" id="308_piranha_red_bellied"]
-[ext_resource type="Resource" path="res://fish/species/arowana_silver/arowana_silver.tres" id="309_arowana_silver"]
-[ext_resource type="Resource" path="res://fish/species/whale_blue/whale_blue.tres" id="310_whale_blue"]
-[ext_resource type="Resource" path="res://fish/species/dolphin_bottlenose/dolphin_bottlenose.tres" id="311_dolphin_bottlenose"]
-[ext_resource type="Resource" path="res://fish/species/whale_humpback/whale_humpback.tres" id="312_whale_humpback"]
-[ext_resource type="Resource" path="res://fish/species/whale_killer/whale_killer.tres" id="313_whale_killer"]
-[ext_resource type="Resource" path="res://fish/species/whale_sperm/whale_sperm.tres" id="314_whale_sperm"]
-[ext_resource type="Resource" path="res://fish/species/crab_brown/crab_brown.tres" id="315_crab_brown"]
-[ext_resource type="Resource" path="res://fish/species/clam_manila/clam_manila.tres" id="316_clam_manila"]
-[ext_resource type="Resource" path="res://fish/species/beetle_stag_common/beetle_stag_common.tres" id="317_beetle_stag_common"]
-
-[resource]
-script = ExtResource("1_pool")
-candidates = [ExtResource("2_bluegill"), ExtResource("3_bass"), ExtResource("4_carp"), ExtResource("5_sunfish"), ExtResource("6_catfish_blue"), ExtResource("7_catfish_channel"), ExtResource("8_catfish_flathead"), ExtResource("9_catfish_white"), ExtResource("10_tuna_albacore"), ExtResource("11_tuna_bigeye"), ExtResource("12_tuna_bluefin"), ExtResource("13_tuna_skipjack"), ExtResource("14_tuna_yellowfin"), ExtResource("15_goby_round"), ExtResource("16_salmon_atlantic"), ExtResource("17_salmon_chum"), ExtResource("18_salmon_coho"), ExtResource("19_salmon_pink"), ExtResource("20_salmon_sockeye"), ExtResource("21_anchovy_european"), ExtResource("22_anchovy_northern"), ExtResource("23_chub_european"), ExtResource("24_chub_flame"), ExtResource("25_chub_lake"), ExtResource("26_goldfish"), ExtResource("27_goldfish_bubbleeye"), ExtResource("28_grouper_gulf"), ExtResource("29_grouper_red"), ExtResource("30_mackerel_atlantic"), ExtResource("31_mackerel_cero"), ExtResource("32_mackerel_chub"), ExtResource("33_mackerel_king"), ExtResource("34_mackerel_spanish"), ExtResource("35_marlin_black"), ExtResource("36_marlin_blue"), ExtResource("37_marlin_white"), ExtResource("38_pomfret_black"), ExtResource("39_pomfret_chinese"), ExtResource("40_pomfret_golden"), ExtResource("41_pomfret_white"), ExtResource("42_sailfish"), ExtResource("43_sauger"), ExtResource("44_saugeye"), ExtResource("45_snapper_lane"), ExtResource("46_snapper_mangrove"), ExtResource("47_snapper_mutton"), ExtResource("48_snapper_red"), ExtResource("49_swordfish"), ExtResource("50_trout_cutthroat"), ExtResource("51_trout_golden"), ExtResource("52_trout_rainbow"), ExtResource("53_trout_steelhead"), ExtResource("54_walleye"), ExtResource("55_bowfin"), ExtResource("56_sturgeon_lake"), ExtResource("57_gar_longnose"), ExtResource("58_paddlefish"), ExtResource("59_sturgeon_shovelnose"), ExtResource("60_gar_spotted"), ExtResource("61_lungfish_west_african"), ExtResource("62_mudskipper_atlantic"), ExtResource("63_foureyes_largescale"), ExtResource("64_mullet_red"), ExtResource("65_plaice_european"), ExtResource("66_flounder_summer"), ExtResource("67_flounder_winter"), ExtResource("68_dugong"), ExtResource("69_seal_harbor"), ExtResource("70_sea_otter"), ExtResource("71_walrus"), ExtResource("72_manatee_west_indian"), ExtResource("73_pipefish_bay"), ExtResource("74_seahorse_lined"), ExtResource("75_monkfish"), ExtResource("76_tripletail"), ExtResource("77_bluefish"), ExtResource("78_jack_crevalle"), ExtResource("79_queenfish_talang"), ExtResource("80_sand_lance_american"), ExtResource("81_menhaden_atlantic"), ExtResource("82_sardine_european"), ExtResource("83_sardine_pacific"), ExtResource("84_shark_blacktip"), ExtResource("85_shark_great_hammerhead"), ExtResource("86_shark_great_white"), ExtResource("87_shark_tiger"), ExtResource("88_shark_whale"), ExtResource("89_burbot"), ExtResource("90_cisco"), ExtResource("91_whitefish_lake"), ExtResource("92_char_arctic"), ExtResource("93_grayling_arctic"), ExtResource("94_trout_brown"), ExtResource("95_wolffish_atlantic"), ExtResource("96_cusk"), ExtResource("97_wolffish_spotted"), ExtResource("98_herring_atlantic"), ExtResource("99_capelin"), ExtResource("100_sprat_european"), ExtResource("101_herring_pacific"), ExtResource("102_pollock_alaska"), ExtResource("103_cod_atlantic"), ExtResource("104_pollock_atlantic"), ExtResource("105_sole_dover"), ExtResource("106_haddock"), ExtResource("107_cod_pacific"), ExtResource("108_hake_pacific"), ExtResource("109_hake_silver"), ExtResource("110_nautilus_chambered"), ExtResource("111_cuttlefish_common"), ExtResource("112_cuttlefish_flamboyant"), ExtResource("113_tilefish_blueline"), ExtResource("114_tilefish_golden"), ExtResource("115_ocean_perch_pacific"), ExtResource("116_sea_bass_chilean"), ExtResource("117_sablefish"), ExtResource("118_wreckfish"), ExtResource("119_oarfish_giant"), ExtResource("120_coelacanth_west_indian"), ExtResource("121_anglerfish_black_seadevil"), ExtResource("122_fangtooth_common"), ExtResource("123_gulper_eel"), ExtResource("124_barreleye_pacific"), ExtResource("125_blobfish_smooth_head"), ExtResource("126_lanternfish_spotted"), ExtResource("127_sea_star_crown_of_thorns"), ExtResource("128_sea_cucumber_giant_california"), ExtResource("129_urchin_purple"), ExtResource("130_sand_dollar"), ExtResource("131_sea_star_sunflower"), ExtResource("132_axolotl"), ExtResource("133_hellbender"), ExtResource("134_guppy"), ExtResource("135_tetra_neon"), ExtResource("136_danio_zebra"), ExtResource("137_largemouth_bass"), ExtResource("138_smallmouth_bass"), ExtResource("139_buffalo_bigmouth"), ExtResource("140_redhorse_golden"), ExtResource("141_quillback"), ExtResource("142_buffalo_smallmouth"), ExtResource("143_sucker_white"), ExtResource("144_bullhead_black"), ExtResource("145_bullhead_brown"), ExtResource("146_madtom_tadpole"), ExtResource("147_bullhead_yellow"), ExtResource("148_barbel_common"), ExtResource("149_bream_common"), ExtResource("150_roach_common"), ExtResource("151_fallfish"), ExtResource("152_ide"), ExtResource("153_rudd"), ExtResource("154_tench"), ExtResource("155_crayfish_red_swamp"), ExtResource("156_gar_alligator"), ExtResource("157_arapaima"), ExtResource("158_tigerfish_goliath"), ExtResource("159_muskellunge"), ExtResource("160_shiner_common"), ExtResource("161_chub_creek"), ExtResource("162_shiner_emerald"), ExtResource("163_minnow_fathead"), ExtResource("164_shiner_golden"), ExtResource("165_electric_eel"), ExtResource("166_freshwater_drum"), ExtResource("167_goldeye"), ExtResource("168_mooneye"), ExtResource("169_loach_weather"), ExtResource("170_koi"), ExtResource("171_crappie_black"), ExtResource("172_green_sunfish"), ExtResource("173_pumpkinseed"), ExtResource("174_redear_sunfish"), ExtResource("175_warmouth"), ExtResource("176_crappie_white"), ExtResource("177_yellow_perch"), ExtResource("178_pickerel_chain"), ExtResource("179_pickerel_grass"), ExtResource("180_pike_northern"), ExtResource("181_jelly_crystal"), ExtResource("182_jelly_lions_mane"), ExtResource("183_jelly_moon"), ExtResource("184_jelly_pacific_sea_nettle"), ExtResource("185_jelly_sea_wasp"), ExtResource("186_pompano_african"), ExtResource("187_cobia"), ExtResource("188_trevally_giant"), ExtResource("189_amberjack_greater"), ExtResource("190_amberjack_lesser"), ExtResource("191_halibut_atlantic"), ExtResource("192_halibut_pacific"), ExtResource("193_turbot"), ExtResource("194_lobster_american"), ExtResource("195_krill_antarctic"), ExtResource("196_lobster_caribbean_spiny"), ExtResource("197_shrimp_peacock_mantis"), ExtResource("198_shrimp_skunk_cleaner"), ExtResource("199_crab_ghost"), ExtResource("200_crab_blue"), ExtResource("201_crab_dungeness"), ExtResource("202_crab_japanese_spider"), ExtResource("203_crab_red_king"), ExtResource("204_eel_american"), ExtResource("205_eel_european"), ExtResource("206_smelt_rainbow"), ExtResource("207_smelt_eulachon"), ExtResource("208_sturgeon_white"), ExtResource("209_mahi_mahi"), ExtResource("210_opah"), ExtResource("211_wahoo"), ExtResource("212_octopus_common"), ExtResource("213_octopus_day"), ExtResource("214_octopus_giant_pacific"), ExtResource("215_octopus_greater_blue_ringed"), ExtResource("216_skate_barndoor"), ExtResource("217_manta_ray_giant"), ExtResource("218_sawfish_largetooth"), ExtResource("219_stingray_ocellate_river"), ExtResource("220_stingray_southern"), ExtResource("221_lionfish_red"), ExtResource("222_scorpionfish_red"), ExtResource("223_moray_green"), ExtResource("224_flounder_peacock"), ExtResource("225_surgeonfish_blue"), ExtResource("226_parrotfish_rainbow"), ExtResource("227_parrotfish_stoplight"), ExtResource("228_tang_yellow"), ExtResource("229_trumpetfish_atlantic"), ExtResource("230_triggerfish_gray"), ExtResource("231_pufferfish_guineafowl"), ExtResource("232_cowfish_longhorn"), ExtResource("233_triggerfish_queen"), ExtResource("234_cornetfish_red"), ExtResource("235_porcupinefish_spotted"), ExtResource("236_boxfish_yellow"), ExtResource("237_butterflyfish_copperband"), ExtResource("238_clownfish_ocellaris"), ExtResource("239_angelfish_queen"), ExtResource("240_grouper_atlantic_goliath"), ExtResource("241_trevally_bluefin"), ExtResource("242_grouper_giant"), ExtResource("243_barracuda_great"), ExtResource("244_grouper_nassau"), ExtResource("245_chromis_blue_green"), ExtResource("246_grunt_bluestriped"), ExtResource("247_grunt_french"), ExtResource("248_damselfish_sergeant_major"), ExtResource("249_wrasse_ballan"), ExtResource("250_wrasse_cleaner"), ExtResource("251_hogfish"), ExtResource("252_turtle_green"), ExtResource("253_turtle_hawksbill"), ExtResource("254_turtle_leatherback"), ExtResource("255_turtle_loggerhead"), ExtResource("256_seadragon_leafy"), ExtResource("257_seadragon_weedy"), ExtResource("258_oyster_black_lip_pearl"), ExtResource("259_clam_giant"), ExtResource("260_clam_geoduck"), ExtResource("261_conch_queen"), ExtResource("262_abalone_red"), ExtResource("263_squid_bigfin_reef"), ExtResource("264_squid_colossal"), ExtResource("265_squid_giant"), ExtResource("266_squid_humboldt"), ExtResource("267_halfbeak_ballyhoo"), ExtResource("268_needlefish_hound"), ExtResource("269_flyingfish_tropical"), ExtResource("270_croaker_atlantic"), ExtResource("271_black_drum"), ExtResource("272_red_drum"), ExtResource("273_mullet_striped"), ExtResource("274_weakfish"), ExtResource("275_white_perch"), ExtResource("276_croaker_yellowfin"), ExtResource("277_rockfish_black"), ExtResource("278_sea_bass_black"), ExtResource("279_rockfish_canary"), ExtResource("280_porgy_scup"), ExtResource("281_sheepshead"), ExtResource("282_tautog"), ExtResource("283_rockfish_yelloweye"), ExtResource("284_lingcod"), ExtResource("285_milkfish"), ExtResource("286_roosterfish"), ExtResource("287_tarpon_atlantic"), ExtResource("288_barramundi"), ExtResource("289_snook_common"), ExtResource("290_bonefish"), ExtResource("291_pompano_florida"), ExtResource("292_ladyfish"), ExtResource("293_permit"), ExtResource("294_knifefish_clown"), ExtResource("295_loach_clown"), ExtResource("296_discus"), ExtResource("297_angelfish_freshwater"), ExtResource("298_gourami_giant"), ExtResource("299_tilapia_nile"), ExtResource("300_oscar"), ExtResource("301_betta_siamese"), ExtResource("302_tambaqui"), ExtResource("303_catfish_walking"), ExtResource("304_snakehead_giant"), ExtResource("305_dorado_golden"), ExtResource("306_mahseer_golden"), ExtResource("307_peacock_bass"), ExtResource("308_piranha_red_bellied"), ExtResource("309_arowana_silver"), ExtResource("310_whale_blue"), ExtResource("311_dolphin_bottlenose"), ExtResource("312_whale_humpback"), ExtResource("313_whale_killer"), ExtResource("314_whale_sperm"), ExtResource("315_crab_brown"), ExtResource("316_clam_manila"), ExtResource("317_beetle_stag_common")]
diff --git a/fish/pools/generated_lake_pool.tres b/fish/pools/generated_lake_pool.tres
deleted file mode 100644
index c0ff343..0000000
--- a/fish/pools/generated_lake_pool.tres
+++ /dev/null
@@ -1,27 +0,0 @@
-[gd_resource type="Resource" load_steps=22 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_pool.gd" id="1_pool"]
-[ext_resource type="Resource" path="res://fish/species/sturgeon_lake/sturgeon_lake.tres" id="2_sturgeon_lake"]
-[ext_resource type="Resource" path="res://fish/species/burbot/burbot.tres" id="3_burbot"]
-[ext_resource type="Resource" path="res://fish/species/cisco/cisco.tres" id="4_cisco"]
-[ext_resource type="Resource" path="res://fish/species/whitefish_lake/whitefish_lake.tres" id="5_whitefish_lake"]
-[ext_resource type="Resource" path="res://fish/species/char_arctic/char_arctic.tres" id="6_char_arctic"]
-[ext_resource type="Resource" path="res://fish/species/trout_golden/trout_golden.tres" id="7_trout_golden"]
-[ext_resource type="Resource" path="res://fish/species/axolotl/axolotl.tres" id="8_axolotl"]
-[ext_resource type="Resource" path="res://fish/species/bream_common/bream_common.tres" id="9_bream_common"]
-[ext_resource type="Resource" path="res://fish/species/muskellunge/muskellunge.tres" id="10_muskellunge"]
-[ext_resource type="Resource" path="res://fish/species/shiner_emerald/shiner_emerald.tres" id="11_shiner_emerald"]
-[ext_resource type="Resource" path="res://fish/species/chub_lake/chub_lake.tres" id="12_chub_lake"]
-[ext_resource type="Resource" path="res://fish/species/crappie_white/crappie_white.tres" id="13_crappie_white"]
-[ext_resource type="Resource" path="res://fish/species/yellow_perch/yellow_perch.tres" id="14_yellow_perch"]
-[ext_resource type="Resource" path="res://fish/species/pike_northern/pike_northern.tres" id="15_pike_northern"]
-[ext_resource type="Resource" path="res://fish/species/saugeye/saugeye.tres" id="16_saugeye"]
-[ext_resource type="Resource" path="res://fish/species/walleye/walleye.tres" id="17_walleye"]
-[ext_resource type="Resource" path="res://fish/species/goby_round/goby_round.tres" id="18_goby_round"]
-[ext_resource type="Resource" path="res://fish/species/smelt_rainbow/smelt_rainbow.tres" id="19_smelt_rainbow"]
-[ext_resource type="Resource" path="res://fish/species/gourami_giant/gourami_giant.tres" id="20_gourami_giant"]
-[ext_resource type="Resource" path="res://fish/species/tilapia_nile/tilapia_nile.tres" id="21_tilapia_nile"]
-
-[resource]
-script = ExtResource("1_pool")
-candidates = [ExtResource("2_sturgeon_lake"), ExtResource("3_burbot"), ExtResource("4_cisco"), ExtResource("5_whitefish_lake"), ExtResource("6_char_arctic"), ExtResource("7_trout_golden"), ExtResource("8_axolotl"), ExtResource("9_bream_common"), ExtResource("10_muskellunge"), ExtResource("11_shiner_emerald"), ExtResource("12_chub_lake"), ExtResource("13_crappie_white"), ExtResource("14_yellow_perch"), ExtResource("15_pike_northern"), ExtResource("16_saugeye"), ExtResource("17_walleye"), ExtResource("18_goby_round"), ExtResource("19_smelt_rainbow"), ExtResource("20_gourami_giant"), ExtResource("21_tilapia_nile")]
diff --git a/fish/pools/generated_pond_pool.tres b/fish/pools/generated_pond_pool.tres
deleted file mode 100644
index 9da4ed5..0000000
--- a/fish/pools/generated_pond_pool.tres
+++ /dev/null
@@ -1,40 +0,0 @@
-[gd_resource type="Resource" load_steps=35 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_pool.gd" id="1_pool"]
-[ext_resource type="Resource" path="res://fish/species/bowfin/bowfin.tres" id="2_bowfin"]
-[ext_resource type="Resource" path="res://fish/species/gar_spotted/gar_spotted.tres" id="3_gar_spotted"]
-[ext_resource type="Resource" path="res://fish/species/lungfish_west_african/lungfish_west_african.tres" id="4_lungfish_west_african"]
-[ext_resource type="Resource" path="res://fish/species/guppy/guppy.tres" id="5_guppy"]
-[ext_resource type="Resource" path="res://fish/species/tetra_neon/tetra_neon.tres" id="6_tetra_neon"]
-[ext_resource type="Resource" path="res://fish/species/danio_zebra/danio_zebra.tres" id="7_danio_zebra"]
-[ext_resource type="Resource" path="res://fish/species/largemouth_bass/largemouth_bass.tres" id="8_largemouth_bass"]
-[ext_resource type="Resource" path="res://fish/species/bullhead_black/bullhead_black.tres" id="9_bullhead_black"]
-[ext_resource type="Resource" path="res://fish/species/bullhead_brown/bullhead_brown.tres" id="10_bullhead_brown"]
-[ext_resource type="Resource" path="res://fish/species/catfish_channel/catfish_channel.tres" id="11_catfish_channel"]
-[ext_resource type="Resource" path="res://fish/species/catfish_white/catfish_white.tres" id="12_catfish_white"]
-[ext_resource type="Resource" path="res://fish/species/bullhead_yellow/bullhead_yellow.tres" id="13_bullhead_yellow"]
-[ext_resource type="Resource" path="res://fish/species/carp/carp.tres" id="14_carp"]
-[ext_resource type="Resource" path="res://fish/species/roach_common/roach_common.tres" id="15_roach_common"]
-[ext_resource type="Resource" path="res://fish/species/rudd/rudd.tres" id="16_rudd"]
-[ext_resource type="Resource" path="res://fish/species/tench/tench.tres" id="17_tench"]
-[ext_resource type="Resource" path="res://fish/species/crayfish_red_swamp/crayfish_red_swamp.tres" id="18_crayfish_red_swamp"]
-[ext_resource type="Resource" path="res://fish/species/minnow_fathead/minnow_fathead.tres" id="19_minnow_fathead"]
-[ext_resource type="Resource" path="res://fish/species/shiner_golden/shiner_golden.tres" id="20_shiner_golden"]
-[ext_resource type="Resource" path="res://fish/species/loach_weather/loach_weather.tres" id="21_loach_weather"]
-[ext_resource type="Resource" path="res://fish/species/goldfish_bubbleeye/goldfish_bubbleeye.tres" id="22_goldfish_bubbleeye"]
-[ext_resource type="Resource" path="res://fish/species/goldfish/goldfish.tres" id="23_goldfish"]
-[ext_resource type="Resource" path="res://fish/species/koi/koi.tres" id="24_koi"]
-[ext_resource type="Resource" path="res://fish/species/crappie_black/crappie_black.tres" id="25_crappie_black"]
-[ext_resource type="Resource" path="res://fish/species/bluegill/bluegill.tres" id="26_bluegill"]
-[ext_resource type="Resource" path="res://fish/species/green_sunfish/green_sunfish.tres" id="27_green_sunfish"]
-[ext_resource type="Resource" path="res://fish/species/pumpkinseed/pumpkinseed.tres" id="28_pumpkinseed"]
-[ext_resource type="Resource" path="res://fish/species/redear_sunfish/redear_sunfish.tres" id="29_redear_sunfish"]
-[ext_resource type="Resource" path="res://fish/species/warmouth/warmouth.tres" id="30_warmouth"]
-[ext_resource type="Resource" path="res://fish/species/pickerel_chain/pickerel_chain.tres" id="31_pickerel_chain"]
-[ext_resource type="Resource" path="res://fish/species/pickerel_grass/pickerel_grass.tres" id="32_pickerel_grass"]
-[ext_resource type="Resource" path="res://fish/species/betta_siamese/betta_siamese.tres" id="33_betta_siamese"]
-[ext_resource type="Resource" path="res://fish/species/catfish_walking/catfish_walking.tres" id="34_catfish_walking"]
-
-[resource]
-script = ExtResource("1_pool")
-candidates = [ExtResource("2_bowfin"), ExtResource("3_gar_spotted"), ExtResource("4_lungfish_west_african"), ExtResource("5_guppy"), ExtResource("6_tetra_neon"), ExtResource("7_danio_zebra"), ExtResource("8_largemouth_bass"), ExtResource("9_bullhead_black"), ExtResource("10_bullhead_brown"), ExtResource("11_catfish_channel"), ExtResource("12_catfish_white"), ExtResource("13_bullhead_yellow"), ExtResource("14_carp"), ExtResource("15_roach_common"), ExtResource("16_rudd"), ExtResource("17_tench"), ExtResource("18_crayfish_red_swamp"), ExtResource("19_minnow_fathead"), ExtResource("20_shiner_golden"), ExtResource("21_loach_weather"), ExtResource("22_goldfish_bubbleeye"), ExtResource("23_goldfish"), ExtResource("24_koi"), ExtResource("25_crappie_black"), ExtResource("26_bluegill"), ExtResource("27_green_sunfish"), ExtResource("28_pumpkinseed"), ExtResource("29_redear_sunfish"), ExtResource("30_warmouth"), ExtResource("31_pickerel_chain"), ExtResource("32_pickerel_grass"), ExtResource("33_betta_siamese"), ExtResource("34_catfish_walking")]
diff --git a/fish/pools/generated_river_pool.tres b/fish/pools/generated_river_pool.tres
deleted file mode 100644
index 66102ef..0000000
--- a/fish/pools/generated_river_pool.tres
+++ /dev/null
@@ -1,60 +0,0 @@
-[gd_resource type="Resource" load_steps=55 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_pool.gd" id="1_pool"]
-[ext_resource type="Resource" path="res://fish/species/gar_longnose/gar_longnose.tres" id="2_gar_longnose"]
-[ext_resource type="Resource" path="res://fish/species/paddlefish/paddlefish.tres" id="3_paddlefish"]
-[ext_resource type="Resource" path="res://fish/species/sturgeon_shovelnose/sturgeon_shovelnose.tres" id="4_sturgeon_shovelnose"]
-[ext_resource type="Resource" path="res://fish/species/foureyes_largescale/foureyes_largescale.tres" id="5_foureyes_largescale"]
-[ext_resource type="Resource" path="res://fish/species/grayling_arctic/grayling_arctic.tres" id="6_grayling_arctic"]
-[ext_resource type="Resource" path="res://fish/species/trout_brown/trout_brown.tres" id="7_trout_brown"]
-[ext_resource type="Resource" path="res://fish/species/trout_cutthroat/trout_cutthroat.tres" id="8_trout_cutthroat"]
-[ext_resource type="Resource" path="res://fish/species/trout_rainbow/trout_rainbow.tres" id="9_trout_rainbow"]
-[ext_resource type="Resource" path="res://fish/species/hellbender/hellbender.tres" id="10_hellbender"]
-[ext_resource type="Resource" path="res://fish/species/smallmouth_bass/smallmouth_bass.tres" id="11_smallmouth_bass"]
-[ext_resource type="Resource" path="res://fish/species/buffalo_bigmouth/buffalo_bigmouth.tres" id="12_buffalo_bigmouth"]
-[ext_resource type="Resource" path="res://fish/species/redhorse_golden/redhorse_golden.tres" id="13_redhorse_golden"]
-[ext_resource type="Resource" path="res://fish/species/quillback/quillback.tres" id="14_quillback"]
-[ext_resource type="Resource" path="res://fish/species/buffalo_smallmouth/buffalo_smallmouth.tres" id="15_buffalo_smallmouth"]
-[ext_resource type="Resource" path="res://fish/species/sucker_white/sucker_white.tres" id="16_sucker_white"]
-[ext_resource type="Resource" path="res://fish/species/catfish_blue/catfish_blue.tres" id="17_catfish_blue"]
-[ext_resource type="Resource" path="res://fish/species/catfish_flathead/catfish_flathead.tres" id="18_catfish_flathead"]
-[ext_resource type="Resource" path="res://fish/species/madtom_tadpole/madtom_tadpole.tres" id="19_madtom_tadpole"]
-[ext_resource type="Resource" path="res://fish/species/barbel_common/barbel_common.tres" id="20_barbel_common"]
-[ext_resource type="Resource" path="res://fish/species/chub_european/chub_european.tres" id="21_chub_european"]
-[ext_resource type="Resource" path="res://fish/species/fallfish/fallfish.tres" id="22_fallfish"]
-[ext_resource type="Resource" path="res://fish/species/ide/ide.tres" id="23_ide"]
-[ext_resource type="Resource" path="res://fish/species/gar_alligator/gar_alligator.tres" id="24_gar_alligator"]
-[ext_resource type="Resource" path="res://fish/species/arapaima/arapaima.tres" id="25_arapaima"]
-[ext_resource type="Resource" path="res://fish/species/tigerfish_goliath/tigerfish_goliath.tres" id="26_tigerfish_goliath"]
-[ext_resource type="Resource" path="res://fish/species/shiner_common/shiner_common.tres" id="27_shiner_common"]
-[ext_resource type="Resource" path="res://fish/species/chub_creek/chub_creek.tres" id="28_chub_creek"]
-[ext_resource type="Resource" path="res://fish/species/chub_flame/chub_flame.tres" id="29_chub_flame"]
-[ext_resource type="Resource" path="res://fish/species/electric_eel/electric_eel.tres" id="30_electric_eel"]
-[ext_resource type="Resource" path="res://fish/species/freshwater_drum/freshwater_drum.tres" id="31_freshwater_drum"]
-[ext_resource type="Resource" path="res://fish/species/goldeye/goldeye.tres" id="32_goldeye"]
-[ext_resource type="Resource" path="res://fish/species/mooneye/mooneye.tres" id="33_mooneye"]
-[ext_resource type="Resource" path="res://fish/species/sauger/sauger.tres" id="34_sauger"]
-[ext_resource type="Resource" path="res://fish/species/eel_american/eel_american.tres" id="35_eel_american"]
-[ext_resource type="Resource" path="res://fish/species/eel_european/eel_european.tres" id="36_eel_european"]
-[ext_resource type="Resource" path="res://fish/species/smelt_eulachon/smelt_eulachon.tres" id="37_smelt_eulachon"]
-[ext_resource type="Resource" path="res://fish/species/sturgeon_white/sturgeon_white.tres" id="38_sturgeon_white"]
-[ext_resource type="Resource" path="res://fish/species/trout_steelhead/trout_steelhead.tres" id="39_trout_steelhead"]
-[ext_resource type="Resource" path="res://fish/species/stingray_ocellate_river/stingray_ocellate_river.tres" id="40_stingray_ocellate_river"]
-[ext_resource type="Resource" path="res://fish/species/white_perch/white_perch.tres" id="41_white_perch"]
-[ext_resource type="Resource" path="res://fish/species/barramundi/barramundi.tres" id="42_barramundi"]
-[ext_resource type="Resource" path="res://fish/species/knifefish_clown/knifefish_clown.tres" id="43_knifefish_clown"]
-[ext_resource type="Resource" path="res://fish/species/loach_clown/loach_clown.tres" id="44_loach_clown"]
-[ext_resource type="Resource" path="res://fish/species/discus/discus.tres" id="45_discus"]
-[ext_resource type="Resource" path="res://fish/species/angelfish_freshwater/angelfish_freshwater.tres" id="46_angelfish_freshwater"]
-[ext_resource type="Resource" path="res://fish/species/oscar/oscar.tres" id="47_oscar"]
-[ext_resource type="Resource" path="res://fish/species/tambaqui/tambaqui.tres" id="48_tambaqui"]
-[ext_resource type="Resource" path="res://fish/species/snakehead_giant/snakehead_giant.tres" id="49_snakehead_giant"]
-[ext_resource type="Resource" path="res://fish/species/dorado_golden/dorado_golden.tres" id="50_dorado_golden"]
-[ext_resource type="Resource" path="res://fish/species/mahseer_golden/mahseer_golden.tres" id="51_mahseer_golden"]
-[ext_resource type="Resource" path="res://fish/species/peacock_bass/peacock_bass.tres" id="52_peacock_bass"]
-[ext_resource type="Resource" path="res://fish/species/piranha_red_bellied/piranha_red_bellied.tres" id="53_piranha_red_bellied"]
-[ext_resource type="Resource" path="res://fish/species/arowana_silver/arowana_silver.tres" id="54_arowana_silver"]
-
-[resource]
-script = ExtResource("1_pool")
-candidates = [ExtResource("2_gar_longnose"), ExtResource("3_paddlefish"), ExtResource("4_sturgeon_shovelnose"), ExtResource("5_foureyes_largescale"), ExtResource("6_grayling_arctic"), ExtResource("7_trout_brown"), ExtResource("8_trout_cutthroat"), ExtResource("9_trout_rainbow"), ExtResource("10_hellbender"), ExtResource("11_smallmouth_bass"), ExtResource("12_buffalo_bigmouth"), ExtResource("13_redhorse_golden"), ExtResource("14_quillback"), ExtResource("15_buffalo_smallmouth"), ExtResource("16_sucker_white"), ExtResource("17_catfish_blue"), ExtResource("18_catfish_flathead"), ExtResource("19_madtom_tadpole"), ExtResource("20_barbel_common"), ExtResource("21_chub_european"), ExtResource("22_fallfish"), ExtResource("23_ide"), ExtResource("24_gar_alligator"), ExtResource("25_arapaima"), ExtResource("26_tigerfish_goliath"), ExtResource("27_shiner_common"), ExtResource("28_chub_creek"), ExtResource("29_chub_flame"), ExtResource("30_electric_eel"), ExtResource("31_freshwater_drum"), ExtResource("32_goldeye"), ExtResource("33_mooneye"), ExtResource("34_sauger"), ExtResource("35_eel_american"), ExtResource("36_eel_european"), ExtResource("37_smelt_eulachon"), ExtResource("38_sturgeon_white"), ExtResource("39_trout_steelhead"), ExtResource("40_stingray_ocellate_river"), ExtResource("41_white_perch"), ExtResource("42_barramundi"), ExtResource("43_knifefish_clown"), ExtResource("44_loach_clown"), ExtResource("45_discus"), ExtResource("46_angelfish_freshwater"), ExtResource("47_oscar"), ExtResource("48_tambaqui"), ExtResource("49_snakehead_giant"), ExtResource("50_dorado_golden"), ExtResource("51_mahseer_golden"), ExtResource("52_peacock_bass"), ExtResource("53_piranha_red_bellied"), ExtResource("54_arowana_silver")]
diff --git a/fish/pools/starter_ocean_pool.tres b/fish/pools/starter_ocean_pool.tres
deleted file mode 100644
index 274b58d..0000000
--- a/fish/pools/starter_ocean_pool.tres
+++ /dev/null
@@ -1,211 +0,0 @@
-[gd_resource type="Resource" load_steps=206 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_pool.gd" id="1_pool"]
-[ext_resource type="Resource" path="res://fish/species/mudskipper_atlantic/mudskipper_atlantic.tres" id="2_mudskipper_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/mullet_red/mullet_red.tres" id="3_mullet_red"]
-[ext_resource type="Resource" path="res://fish/species/plaice_european/plaice_european.tres" id="4_plaice_european"]
-[ext_resource type="Resource" path="res://fish/species/flounder_summer/flounder_summer.tres" id="5_flounder_summer"]
-[ext_resource type="Resource" path="res://fish/species/flounder_winter/flounder_winter.tres" id="6_flounder_winter"]
-[ext_resource type="Resource" path="res://fish/species/dugong/dugong.tres" id="7_dugong"]
-[ext_resource type="Resource" path="res://fish/species/seal_harbor/seal_harbor.tres" id="8_seal_harbor"]
-[ext_resource type="Resource" path="res://fish/species/sea_otter/sea_otter.tres" id="9_sea_otter"]
-[ext_resource type="Resource" path="res://fish/species/walrus/walrus.tres" id="10_walrus"]
-[ext_resource type="Resource" path="res://fish/species/manatee_west_indian/manatee_west_indian.tres" id="11_manatee_west_indian"]
-[ext_resource type="Resource" path="res://fish/species/pipefish_bay/pipefish_bay.tres" id="12_pipefish_bay"]
-[ext_resource type="Resource" path="res://fish/species/seahorse_lined/seahorse_lined.tres" id="13_seahorse_lined"]
-[ext_resource type="Resource" path="res://fish/species/monkfish/monkfish.tres" id="14_monkfish"]
-[ext_resource type="Resource" path="res://fish/species/tripletail/tripletail.tres" id="15_tripletail"]
-[ext_resource type="Resource" path="res://fish/species/mackerel_atlantic/mackerel_atlantic.tres" id="16_mackerel_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/bluefish/bluefish.tres" id="17_bluefish"]
-[ext_resource type="Resource" path="res://fish/species/mackerel_cero/mackerel_cero.tres" id="18_mackerel_cero"]
-[ext_resource type="Resource" path="res://fish/species/mackerel_chub/mackerel_chub.tres" id="19_mackerel_chub"]
-[ext_resource type="Resource" path="res://fish/species/jack_crevalle/jack_crevalle.tres" id="20_jack_crevalle"]
-[ext_resource type="Resource" path="res://fish/species/mackerel_king/mackerel_king.tres" id="21_mackerel_king"]
-[ext_resource type="Resource" path="res://fish/species/mackerel_spanish/mackerel_spanish.tres" id="22_mackerel_spanish"]
-[ext_resource type="Resource" path="res://fish/species/queenfish_talang/queenfish_talang.tres" id="23_queenfish_talang"]
-[ext_resource type="Resource" path="res://fish/species/sand_lance_american/sand_lance_american.tres" id="24_sand_lance_american"]
-[ext_resource type="Resource" path="res://fish/species/menhaden_atlantic/menhaden_atlantic.tres" id="25_menhaden_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/pomfret_black/pomfret_black.tres" id="26_pomfret_black"]
-[ext_resource type="Resource" path="res://fish/species/pomfret_chinese/pomfret_chinese.tres" id="27_pomfret_chinese"]
-[ext_resource type="Resource" path="res://fish/species/anchovy_european/anchovy_european.tres" id="28_anchovy_european"]
-[ext_resource type="Resource" path="res://fish/species/sardine_european/sardine_european.tres" id="29_sardine_european"]
-[ext_resource type="Resource" path="res://fish/species/pomfret_golden/pomfret_golden.tres" id="30_pomfret_golden"]
-[ext_resource type="Resource" path="res://fish/species/anchovy_northern/anchovy_northern.tres" id="31_anchovy_northern"]
-[ext_resource type="Resource" path="res://fish/species/sardine_pacific/sardine_pacific.tres" id="32_sardine_pacific"]
-[ext_resource type="Resource" path="res://fish/species/pomfret_white/pomfret_white.tres" id="33_pomfret_white"]
-[ext_resource type="Resource" path="res://fish/species/shark_blacktip/shark_blacktip.tres" id="34_shark_blacktip"]
-[ext_resource type="Resource" path="res://fish/species/shark_great_hammerhead/shark_great_hammerhead.tres" id="35_shark_great_hammerhead"]
-[ext_resource type="Resource" path="res://fish/species/shark_great_white/shark_great_white.tres" id="36_shark_great_white"]
-[ext_resource type="Resource" path="res://fish/species/shark_tiger/shark_tiger.tres" id="37_shark_tiger"]
-[ext_resource type="Resource" path="res://fish/species/shark_whale/shark_whale.tres" id="38_shark_whale"]
-[ext_resource type="Resource" path="res://fish/species/wolffish_atlantic/wolffish_atlantic.tres" id="39_wolffish_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/cusk/cusk.tres" id="40_cusk"]
-[ext_resource type="Resource" path="res://fish/species/wolffish_spotted/wolffish_spotted.tres" id="41_wolffish_spotted"]
-[ext_resource type="Resource" path="res://fish/species/herring_atlantic/herring_atlantic.tres" id="42_herring_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/capelin/capelin.tres" id="43_capelin"]
-[ext_resource type="Resource" path="res://fish/species/sprat_european/sprat_european.tres" id="44_sprat_european"]
-[ext_resource type="Resource" path="res://fish/species/herring_pacific/herring_pacific.tres" id="45_herring_pacific"]
-[ext_resource type="Resource" path="res://fish/species/pollock_alaska/pollock_alaska.tres" id="46_pollock_alaska"]
-[ext_resource type="Resource" path="res://fish/species/cod_atlantic/cod_atlantic.tres" id="47_cod_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/pollock_atlantic/pollock_atlantic.tres" id="48_pollock_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/sole_dover/sole_dover.tres" id="49_sole_dover"]
-[ext_resource type="Resource" path="res://fish/species/haddock/haddock.tres" id="50_haddock"]
-[ext_resource type="Resource" path="res://fish/species/cod_pacific/cod_pacific.tres" id="51_cod_pacific"]
-[ext_resource type="Resource" path="res://fish/species/hake_pacific/hake_pacific.tres" id="52_hake_pacific"]
-[ext_resource type="Resource" path="res://fish/species/hake_silver/hake_silver.tres" id="53_hake_silver"]
-[ext_resource type="Resource" path="res://fish/species/nautilus_chambered/nautilus_chambered.tres" id="54_nautilus_chambered"]
-[ext_resource type="Resource" path="res://fish/species/cuttlefish_common/cuttlefish_common.tres" id="55_cuttlefish_common"]
-[ext_resource type="Resource" path="res://fish/species/cuttlefish_flamboyant/cuttlefish_flamboyant.tres" id="56_cuttlefish_flamboyant"]
-[ext_resource type="Resource" path="res://fish/species/tilefish_blueline/tilefish_blueline.tres" id="57_tilefish_blueline"]
-[ext_resource type="Resource" path="res://fish/species/tilefish_golden/tilefish_golden.tres" id="58_tilefish_golden"]
-[ext_resource type="Resource" path="res://fish/species/ocean_perch_pacific/ocean_perch_pacific.tres" id="59_ocean_perch_pacific"]
-[ext_resource type="Resource" path="res://fish/species/sea_bass_chilean/sea_bass_chilean.tres" id="60_sea_bass_chilean"]
-[ext_resource type="Resource" path="res://fish/species/sablefish/sablefish.tres" id="61_sablefish"]
-[ext_resource type="Resource" path="res://fish/species/wreckfish/wreckfish.tres" id="62_wreckfish"]
-[ext_resource type="Resource" path="res://fish/species/oarfish_giant/oarfish_giant.tres" id="63_oarfish_giant"]
-[ext_resource type="Resource" path="res://fish/species/coelacanth_west_indian/coelacanth_west_indian.tres" id="64_coelacanth_west_indian"]
-[ext_resource type="Resource" path="res://fish/species/anglerfish_black_seadevil/anglerfish_black_seadevil.tres" id="65_anglerfish_black_seadevil"]
-[ext_resource type="Resource" path="res://fish/species/fangtooth_common/fangtooth_common.tres" id="66_fangtooth_common"]
-[ext_resource type="Resource" path="res://fish/species/gulper_eel/gulper_eel.tres" id="67_gulper_eel"]
-[ext_resource type="Resource" path="res://fish/species/barreleye_pacific/barreleye_pacific.tres" id="68_barreleye_pacific"]
-[ext_resource type="Resource" path="res://fish/species/blobfish_smooth_head/blobfish_smooth_head.tres" id="69_blobfish_smooth_head"]
-[ext_resource type="Resource" path="res://fish/species/lanternfish_spotted/lanternfish_spotted.tres" id="70_lanternfish_spotted"]
-[ext_resource type="Resource" path="res://fish/species/sea_star_crown_of_thorns/sea_star_crown_of_thorns.tres" id="71_sea_star_crown_of_thorns"]
-[ext_resource type="Resource" path="res://fish/species/sea_cucumber_giant_california/sea_cucumber_giant_california.tres" id="72_sea_cucumber_giant_california"]
-[ext_resource type="Resource" path="res://fish/species/urchin_purple/urchin_purple.tres" id="73_urchin_purple"]
-[ext_resource type="Resource" path="res://fish/species/sand_dollar/sand_dollar.tres" id="74_sand_dollar"]
-[ext_resource type="Resource" path="res://fish/species/sea_star_sunflower/sea_star_sunflower.tres" id="75_sea_star_sunflower"]
-[ext_resource type="Resource" path="res://fish/species/jelly_crystal/jelly_crystal.tres" id="76_jelly_crystal"]
-[ext_resource type="Resource" path="res://fish/species/jelly_lions_mane/jelly_lions_mane.tres" id="77_jelly_lions_mane"]
-[ext_resource type="Resource" path="res://fish/species/jelly_moon/jelly_moon.tres" id="78_jelly_moon"]
-[ext_resource type="Resource" path="res://fish/species/jelly_pacific_sea_nettle/jelly_pacific_sea_nettle.tres" id="79_jelly_pacific_sea_nettle"]
-[ext_resource type="Resource" path="res://fish/species/jelly_sea_wasp/jelly_sea_wasp.tres" id="80_jelly_sea_wasp"]
-[ext_resource type="Resource" path="res://fish/species/pompano_african/pompano_african.tres" id="81_pompano_african"]
-[ext_resource type="Resource" path="res://fish/species/cobia/cobia.tres" id="82_cobia"]
-[ext_resource type="Resource" path="res://fish/species/trevally_giant/trevally_giant.tres" id="83_trevally_giant"]
-[ext_resource type="Resource" path="res://fish/species/amberjack_greater/amberjack_greater.tres" id="84_amberjack_greater"]
-[ext_resource type="Resource" path="res://fish/species/amberjack_lesser/amberjack_lesser.tres" id="85_amberjack_lesser"]
-[ext_resource type="Resource" path="res://fish/species/halibut_atlantic/halibut_atlantic.tres" id="86_halibut_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/halibut_pacific/halibut_pacific.tres" id="87_halibut_pacific"]
-[ext_resource type="Resource" path="res://fish/species/turbot/turbot.tres" id="88_turbot"]
-[ext_resource type="Resource" path="res://fish/species/lobster_american/lobster_american.tres" id="89_lobster_american"]
-[ext_resource type="Resource" path="res://fish/species/krill_antarctic/krill_antarctic.tres" id="90_krill_antarctic"]
-[ext_resource type="Resource" path="res://fish/species/lobster_caribbean_spiny/lobster_caribbean_spiny.tres" id="91_lobster_caribbean_spiny"]
-[ext_resource type="Resource" path="res://fish/species/shrimp_peacock_mantis/shrimp_peacock_mantis.tres" id="92_shrimp_peacock_mantis"]
-[ext_resource type="Resource" path="res://fish/species/shrimp_skunk_cleaner/shrimp_skunk_cleaner.tres" id="93_shrimp_skunk_cleaner"]
-[ext_resource type="Resource" path="res://fish/species/crab_japanese_spider/crab_japanese_spider.tres" id="94_crab_japanese_spider"]
-[ext_resource type="Resource" path="res://fish/species/crab_red_king/crab_red_king.tres" id="95_crab_red_king"]
-[ext_resource type="Resource" path="res://fish/species/salmon_atlantic/salmon_atlantic.tres" id="96_salmon_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/salmon_chum/salmon_chum.tres" id="97_salmon_chum"]
-[ext_resource type="Resource" path="res://fish/species/salmon_coho/salmon_coho.tres" id="98_salmon_coho"]
-[ext_resource type="Resource" path="res://fish/species/salmon_pink/salmon_pink.tres" id="99_salmon_pink"]
-[ext_resource type="Resource" path="res://fish/species/salmon_sockeye/salmon_sockeye.tres" id="100_salmon_sockeye"]
-[ext_resource type="Resource" path="res://fish/species/marlin_black/marlin_black.tres" id="101_marlin_black"]
-[ext_resource type="Resource" path="res://fish/species/marlin_blue/marlin_blue.tres" id="102_marlin_blue"]
-[ext_resource type="Resource" path="res://fish/species/sailfish/sailfish.tres" id="103_sailfish"]
-[ext_resource type="Resource" path="res://fish/species/swordfish/swordfish.tres" id="104_swordfish"]
-[ext_resource type="Resource" path="res://fish/species/marlin_white/marlin_white.tres" id="105_marlin_white"]
-[ext_resource type="Resource" path="res://fish/species/mahi_mahi/mahi_mahi.tres" id="106_mahi_mahi"]
-[ext_resource type="Resource" path="res://fish/species/opah/opah.tres" id="107_opah"]
-[ext_resource type="Resource" path="res://fish/species/wahoo/wahoo.tres" id="108_wahoo"]
-[ext_resource type="Resource" path="res://fish/species/tuna_albacore/tuna_albacore.tres" id="109_tuna_albacore"]
-[ext_resource type="Resource" path="res://fish/species/tuna_bigeye/tuna_bigeye.tres" id="110_tuna_bigeye"]
-[ext_resource type="Resource" path="res://fish/species/tuna_bluefin/tuna_bluefin.tres" id="111_tuna_bluefin"]
-[ext_resource type="Resource" path="res://fish/species/tuna_skipjack/tuna_skipjack.tres" id="112_tuna_skipjack"]
-[ext_resource type="Resource" path="res://fish/species/tuna_yellowfin/tuna_yellowfin.tres" id="113_tuna_yellowfin"]
-[ext_resource type="Resource" path="res://fish/species/octopus_common/octopus_common.tres" id="114_octopus_common"]
-[ext_resource type="Resource" path="res://fish/species/octopus_day/octopus_day.tres" id="115_octopus_day"]
-[ext_resource type="Resource" path="res://fish/species/octopus_giant_pacific/octopus_giant_pacific.tres" id="116_octopus_giant_pacific"]
-[ext_resource type="Resource" path="res://fish/species/octopus_greater_blue_ringed/octopus_greater_blue_ringed.tres" id="117_octopus_greater_blue_ringed"]
-[ext_resource type="Resource" path="res://fish/species/sunfish/sunfish.tres" id="118_sunfish"]
-[ext_resource type="Resource" path="res://fish/species/skate_barndoor/skate_barndoor.tres" id="119_skate_barndoor"]
-[ext_resource type="Resource" path="res://fish/species/manta_ray_giant/manta_ray_giant.tres" id="120_manta_ray_giant"]
-[ext_resource type="Resource" path="res://fish/species/sawfish_largetooth/sawfish_largetooth.tres" id="121_sawfish_largetooth"]
-[ext_resource type="Resource" path="res://fish/species/stingray_southern/stingray_southern.tres" id="122_stingray_southern"]
-[ext_resource type="Resource" path="res://fish/species/lionfish_red/lionfish_red.tres" id="123_lionfish_red"]
-[ext_resource type="Resource" path="res://fish/species/scorpionfish_red/scorpionfish_red.tres" id="124_scorpionfish_red"]
-[ext_resource type="Resource" path="res://fish/species/moray_green/moray_green.tres" id="125_moray_green"]
-[ext_resource type="Resource" path="res://fish/species/flounder_peacock/flounder_peacock.tres" id="126_flounder_peacock"]
-[ext_resource type="Resource" path="res://fish/species/surgeonfish_blue/surgeonfish_blue.tres" id="127_surgeonfish_blue"]
-[ext_resource type="Resource" path="res://fish/species/parrotfish_rainbow/parrotfish_rainbow.tres" id="128_parrotfish_rainbow"]
-[ext_resource type="Resource" path="res://fish/species/parrotfish_stoplight/parrotfish_stoplight.tres" id="129_parrotfish_stoplight"]
-[ext_resource type="Resource" path="res://fish/species/tang_yellow/tang_yellow.tres" id="130_tang_yellow"]
-[ext_resource type="Resource" path="res://fish/species/trumpetfish_atlantic/trumpetfish_atlantic.tres" id="131_trumpetfish_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/triggerfish_gray/triggerfish_gray.tres" id="132_triggerfish_gray"]
-[ext_resource type="Resource" path="res://fish/species/pufferfish_guineafowl/pufferfish_guineafowl.tres" id="133_pufferfish_guineafowl"]
-[ext_resource type="Resource" path="res://fish/species/cowfish_longhorn/cowfish_longhorn.tres" id="134_cowfish_longhorn"]
-[ext_resource type="Resource" path="res://fish/species/triggerfish_queen/triggerfish_queen.tres" id="135_triggerfish_queen"]
-[ext_resource type="Resource" path="res://fish/species/cornetfish_red/cornetfish_red.tres" id="136_cornetfish_red"]
-[ext_resource type="Resource" path="res://fish/species/porcupinefish_spotted/porcupinefish_spotted.tres" id="137_porcupinefish_spotted"]
-[ext_resource type="Resource" path="res://fish/species/boxfish_yellow/boxfish_yellow.tres" id="138_boxfish_yellow"]
-[ext_resource type="Resource" path="res://fish/species/butterflyfish_copperband/butterflyfish_copperband.tres" id="139_butterflyfish_copperband"]
-[ext_resource type="Resource" path="res://fish/species/clownfish_ocellaris/clownfish_ocellaris.tres" id="140_clownfish_ocellaris"]
-[ext_resource type="Resource" path="res://fish/species/angelfish_queen/angelfish_queen.tres" id="141_angelfish_queen"]
-[ext_resource type="Resource" path="res://fish/species/grouper_atlantic_goliath/grouper_atlantic_goliath.tres" id="142_grouper_atlantic_goliath"]
-[ext_resource type="Resource" path="res://fish/species/trevally_bluefin/trevally_bluefin.tres" id="143_trevally_bluefin"]
-[ext_resource type="Resource" path="res://fish/species/grouper_giant/grouper_giant.tres" id="144_grouper_giant"]
-[ext_resource type="Resource" path="res://fish/species/barracuda_great/barracuda_great.tres" id="145_barracuda_great"]
-[ext_resource type="Resource" path="res://fish/species/grouper_gulf/grouper_gulf.tres" id="146_grouper_gulf"]
-[ext_resource type="Resource" path="res://fish/species/grouper_nassau/grouper_nassau.tres" id="147_grouper_nassau"]
-[ext_resource type="Resource" path="res://fish/species/grouper_red/grouper_red.tres" id="148_grouper_red"]
-[ext_resource type="Resource" path="res://fish/species/chromis_blue_green/chromis_blue_green.tres" id="149_chromis_blue_green"]
-[ext_resource type="Resource" path="res://fish/species/grunt_bluestriped/grunt_bluestriped.tres" id="150_grunt_bluestriped"]
-[ext_resource type="Resource" path="res://fish/species/grunt_french/grunt_french.tres" id="151_grunt_french"]
-[ext_resource type="Resource" path="res://fish/species/damselfish_sergeant_major/damselfish_sergeant_major.tres" id="152_damselfish_sergeant_major"]
-[ext_resource type="Resource" path="res://fish/species/snapper_lane/snapper_lane.tres" id="153_snapper_lane"]
-[ext_resource type="Resource" path="res://fish/species/snapper_mangrove/snapper_mangrove.tres" id="154_snapper_mangrove"]
-[ext_resource type="Resource" path="res://fish/species/snapper_mutton/snapper_mutton.tres" id="155_snapper_mutton"]
-[ext_resource type="Resource" path="res://fish/species/snapper_red/snapper_red.tres" id="156_snapper_red"]
-[ext_resource type="Resource" path="res://fish/species/wrasse_ballan/wrasse_ballan.tres" id="157_wrasse_ballan"]
-[ext_resource type="Resource" path="res://fish/species/wrasse_cleaner/wrasse_cleaner.tres" id="158_wrasse_cleaner"]
-[ext_resource type="Resource" path="res://fish/species/hogfish/hogfish.tres" id="159_hogfish"]
-[ext_resource type="Resource" path="res://fish/species/turtle_green/turtle_green.tres" id="160_turtle_green"]
-[ext_resource type="Resource" path="res://fish/species/turtle_hawksbill/turtle_hawksbill.tres" id="161_turtle_hawksbill"]
-[ext_resource type="Resource" path="res://fish/species/turtle_leatherback/turtle_leatherback.tres" id="162_turtle_leatherback"]
-[ext_resource type="Resource" path="res://fish/species/turtle_loggerhead/turtle_loggerhead.tres" id="163_turtle_loggerhead"]
-[ext_resource type="Resource" path="res://fish/species/seadragon_leafy/seadragon_leafy.tres" id="164_seadragon_leafy"]
-[ext_resource type="Resource" path="res://fish/species/seadragon_weedy/seadragon_weedy.tres" id="165_seadragon_weedy"]
-[ext_resource type="Resource" path="res://fish/species/oyster_black_lip_pearl/oyster_black_lip_pearl.tres" id="166_oyster_black_lip_pearl"]
-[ext_resource type="Resource" path="res://fish/species/clam_giant/clam_giant.tres" id="167_clam_giant"]
-[ext_resource type="Resource" path="res://fish/species/clam_geoduck/clam_geoduck.tres" id="168_clam_geoduck"]
-[ext_resource type="Resource" path="res://fish/species/conch_queen/conch_queen.tres" id="169_conch_queen"]
-[ext_resource type="Resource" path="res://fish/species/abalone_red/abalone_red.tres" id="170_abalone_red"]
-[ext_resource type="Resource" path="res://fish/species/squid_bigfin_reef/squid_bigfin_reef.tres" id="171_squid_bigfin_reef"]
-[ext_resource type="Resource" path="res://fish/species/squid_colossal/squid_colossal.tres" id="172_squid_colossal"]
-[ext_resource type="Resource" path="res://fish/species/squid_giant/squid_giant.tres" id="173_squid_giant"]
-[ext_resource type="Resource" path="res://fish/species/squid_humboldt/squid_humboldt.tres" id="174_squid_humboldt"]
-[ext_resource type="Resource" path="res://fish/species/halfbeak_ballyhoo/halfbeak_ballyhoo.tres" id="175_halfbeak_ballyhoo"]
-[ext_resource type="Resource" path="res://fish/species/needlefish_hound/needlefish_hound.tres" id="176_needlefish_hound"]
-[ext_resource type="Resource" path="res://fish/species/flyingfish_tropical/flyingfish_tropical.tres" id="177_flyingfish_tropical"]
-[ext_resource type="Resource" path="res://fish/species/bass/bass.tres" id="178_bass"]
-[ext_resource type="Resource" path="res://fish/species/croaker_atlantic/croaker_atlantic.tres" id="179_croaker_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/black_drum/black_drum.tres" id="180_black_drum"]
-[ext_resource type="Resource" path="res://fish/species/red_drum/red_drum.tres" id="181_red_drum"]
-[ext_resource type="Resource" path="res://fish/species/mullet_striped/mullet_striped.tres" id="182_mullet_striped"]
-[ext_resource type="Resource" path="res://fish/species/weakfish/weakfish.tres" id="183_weakfish"]
-[ext_resource type="Resource" path="res://fish/species/croaker_yellowfin/croaker_yellowfin.tres" id="184_croaker_yellowfin"]
-[ext_resource type="Resource" path="res://fish/species/rockfish_black/rockfish_black.tres" id="185_rockfish_black"]
-[ext_resource type="Resource" path="res://fish/species/sea_bass_black/sea_bass_black.tres" id="186_sea_bass_black"]
-[ext_resource type="Resource" path="res://fish/species/rockfish_canary/rockfish_canary.tres" id="187_rockfish_canary"]
-[ext_resource type="Resource" path="res://fish/species/porgy_scup/porgy_scup.tres" id="188_porgy_scup"]
-[ext_resource type="Resource" path="res://fish/species/sheepshead/sheepshead.tres" id="189_sheepshead"]
-[ext_resource type="Resource" path="res://fish/species/tautog/tautog.tres" id="190_tautog"]
-[ext_resource type="Resource" path="res://fish/species/rockfish_yelloweye/rockfish_yelloweye.tres" id="191_rockfish_yelloweye"]
-[ext_resource type="Resource" path="res://fish/species/lingcod/lingcod.tres" id="192_lingcod"]
-[ext_resource type="Resource" path="res://fish/species/milkfish/milkfish.tres" id="193_milkfish"]
-[ext_resource type="Resource" path="res://fish/species/roosterfish/roosterfish.tres" id="194_roosterfish"]
-[ext_resource type="Resource" path="res://fish/species/tarpon_atlantic/tarpon_atlantic.tres" id="195_tarpon_atlantic"]
-[ext_resource type="Resource" path="res://fish/species/snook_common/snook_common.tres" id="196_snook_common"]
-[ext_resource type="Resource" path="res://fish/species/bonefish/bonefish.tres" id="197_bonefish"]
-[ext_resource type="Resource" path="res://fish/species/pompano_florida/pompano_florida.tres" id="198_pompano_florida"]
-[ext_resource type="Resource" path="res://fish/species/ladyfish/ladyfish.tres" id="199_ladyfish"]
-[ext_resource type="Resource" path="res://fish/species/permit/permit.tres" id="200_permit"]
-[ext_resource type="Resource" path="res://fish/species/whale_blue/whale_blue.tres" id="201_whale_blue"]
-[ext_resource type="Resource" path="res://fish/species/dolphin_bottlenose/dolphin_bottlenose.tres" id="202_dolphin_bottlenose"]
-[ext_resource type="Resource" path="res://fish/species/whale_humpback/whale_humpback.tres" id="203_whale_humpback"]
-[ext_resource type="Resource" path="res://fish/species/whale_killer/whale_killer.tres" id="204_whale_killer"]
-[ext_resource type="Resource" path="res://fish/species/whale_sperm/whale_sperm.tres" id="205_whale_sperm"]
-
-[resource]
-script = ExtResource("1_pool")
-candidates = [ExtResource("2_mudskipper_atlantic"), ExtResource("3_mullet_red"), ExtResource("4_plaice_european"), ExtResource("5_flounder_summer"), ExtResource("6_flounder_winter"), ExtResource("7_dugong"), ExtResource("8_seal_harbor"), ExtResource("9_sea_otter"), ExtResource("10_walrus"), ExtResource("11_manatee_west_indian"), ExtResource("12_pipefish_bay"), ExtResource("13_seahorse_lined"), ExtResource("14_monkfish"), ExtResource("15_tripletail"), ExtResource("16_mackerel_atlantic"), ExtResource("17_bluefish"), ExtResource("18_mackerel_cero"), ExtResource("19_mackerel_chub"), ExtResource("20_jack_crevalle"), ExtResource("21_mackerel_king"), ExtResource("22_mackerel_spanish"), ExtResource("23_queenfish_talang"), ExtResource("24_sand_lance_american"), ExtResource("25_menhaden_atlantic"), ExtResource("26_pomfret_black"), ExtResource("27_pomfret_chinese"), ExtResource("28_anchovy_european"), ExtResource("29_sardine_european"), ExtResource("30_pomfret_golden"), ExtResource("31_anchovy_northern"), ExtResource("32_sardine_pacific"), ExtResource("33_pomfret_white"), ExtResource("34_shark_blacktip"), ExtResource("35_shark_great_hammerhead"), ExtResource("36_shark_great_white"), ExtResource("37_shark_tiger"), ExtResource("38_shark_whale"), ExtResource("39_wolffish_atlantic"), ExtResource("40_cusk"), ExtResource("41_wolffish_spotted"), ExtResource("42_herring_atlantic"), ExtResource("43_capelin"), ExtResource("44_sprat_european"), ExtResource("45_herring_pacific"), ExtResource("46_pollock_alaska"), ExtResource("47_cod_atlantic"), ExtResource("48_pollock_atlantic"), ExtResource("49_sole_dover"), ExtResource("50_haddock"), ExtResource("51_cod_pacific"), ExtResource("52_hake_pacific"), ExtResource("53_hake_silver"), ExtResource("54_nautilus_chambered"), ExtResource("55_cuttlefish_common"), ExtResource("56_cuttlefish_flamboyant"), ExtResource("57_tilefish_blueline"), ExtResource("58_tilefish_golden"), ExtResource("59_ocean_perch_pacific"), ExtResource("60_sea_bass_chilean"), ExtResource("61_sablefish"), ExtResource("62_wreckfish"), ExtResource("63_oarfish_giant"), ExtResource("64_coelacanth_west_indian"), ExtResource("65_anglerfish_black_seadevil"), ExtResource("66_fangtooth_common"), ExtResource("67_gulper_eel"), ExtResource("68_barreleye_pacific"), ExtResource("69_blobfish_smooth_head"), ExtResource("70_lanternfish_spotted"), ExtResource("71_sea_star_crown_of_thorns"), ExtResource("72_sea_cucumber_giant_california"), ExtResource("73_urchin_purple"), ExtResource("74_sand_dollar"), ExtResource("75_sea_star_sunflower"), ExtResource("76_jelly_crystal"), ExtResource("77_jelly_lions_mane"), ExtResource("78_jelly_moon"), ExtResource("79_jelly_pacific_sea_nettle"), ExtResource("80_jelly_sea_wasp"), ExtResource("81_pompano_african"), ExtResource("82_cobia"), ExtResource("83_trevally_giant"), ExtResource("84_amberjack_greater"), ExtResource("85_amberjack_lesser"), ExtResource("86_halibut_atlantic"), ExtResource("87_halibut_pacific"), ExtResource("88_turbot"), ExtResource("89_lobster_american"), ExtResource("90_krill_antarctic"), ExtResource("91_lobster_caribbean_spiny"), ExtResource("92_shrimp_peacock_mantis"), ExtResource("93_shrimp_skunk_cleaner"), ExtResource("94_crab_japanese_spider"), ExtResource("95_crab_red_king"), ExtResource("96_salmon_atlantic"), ExtResource("97_salmon_chum"), ExtResource("98_salmon_coho"), ExtResource("99_salmon_pink"), ExtResource("100_salmon_sockeye"), ExtResource("101_marlin_black"), ExtResource("102_marlin_blue"), ExtResource("103_sailfish"), ExtResource("104_swordfish"), ExtResource("105_marlin_white"), ExtResource("106_mahi_mahi"), ExtResource("107_opah"), ExtResource("108_wahoo"), ExtResource("109_tuna_albacore"), ExtResource("110_tuna_bigeye"), ExtResource("111_tuna_bluefin"), ExtResource("112_tuna_skipjack"), ExtResource("113_tuna_yellowfin"), ExtResource("114_octopus_common"), ExtResource("115_octopus_day"), ExtResource("116_octopus_giant_pacific"), ExtResource("117_octopus_greater_blue_ringed"), ExtResource("118_sunfish"), ExtResource("119_skate_barndoor"), ExtResource("120_manta_ray_giant"), ExtResource("121_sawfish_largetooth"), ExtResource("122_stingray_southern"), ExtResource("123_lionfish_red"), ExtResource("124_scorpionfish_red"), ExtResource("125_moray_green"), ExtResource("126_flounder_peacock"), ExtResource("127_surgeonfish_blue"), ExtResource("128_parrotfish_rainbow"), ExtResource("129_parrotfish_stoplight"), ExtResource("130_tang_yellow"), ExtResource("131_trumpetfish_atlantic"), ExtResource("132_triggerfish_gray"), ExtResource("133_pufferfish_guineafowl"), ExtResource("134_cowfish_longhorn"), ExtResource("135_triggerfish_queen"), ExtResource("136_cornetfish_red"), ExtResource("137_porcupinefish_spotted"), ExtResource("138_boxfish_yellow"), ExtResource("139_butterflyfish_copperband"), ExtResource("140_clownfish_ocellaris"), ExtResource("141_angelfish_queen"), ExtResource("142_grouper_atlantic_goliath"), ExtResource("143_trevally_bluefin"), ExtResource("144_grouper_giant"), ExtResource("145_barracuda_great"), ExtResource("146_grouper_gulf"), ExtResource("147_grouper_nassau"), ExtResource("148_grouper_red"), ExtResource("149_chromis_blue_green"), ExtResource("150_grunt_bluestriped"), ExtResource("151_grunt_french"), ExtResource("152_damselfish_sergeant_major"), ExtResource("153_snapper_lane"), ExtResource("154_snapper_mangrove"), ExtResource("155_snapper_mutton"), ExtResource("156_snapper_red"), ExtResource("157_wrasse_ballan"), ExtResource("158_wrasse_cleaner"), ExtResource("159_hogfish"), ExtResource("160_turtle_green"), ExtResource("161_turtle_hawksbill"), ExtResource("162_turtle_leatherback"), ExtResource("163_turtle_loggerhead"), ExtResource("164_seadragon_leafy"), ExtResource("165_seadragon_weedy"), ExtResource("166_oyster_black_lip_pearl"), ExtResource("167_clam_giant"), ExtResource("168_clam_geoduck"), ExtResource("169_conch_queen"), ExtResource("170_abalone_red"), ExtResource("171_squid_bigfin_reef"), ExtResource("172_squid_colossal"), ExtResource("173_squid_giant"), ExtResource("174_squid_humboldt"), ExtResource("175_halfbeak_ballyhoo"), ExtResource("176_needlefish_hound"), ExtResource("177_flyingfish_tropical"), ExtResource("178_bass"), ExtResource("179_croaker_atlantic"), ExtResource("180_black_drum"), ExtResource("181_red_drum"), ExtResource("182_mullet_striped"), ExtResource("183_weakfish"), ExtResource("184_croaker_yellowfin"), ExtResource("185_rockfish_black"), ExtResource("186_sea_bass_black"), ExtResource("187_rockfish_canary"), ExtResource("188_porgy_scup"), ExtResource("189_sheepshead"), ExtResource("190_tautog"), ExtResource("191_rockfish_yelloweye"), ExtResource("192_lingcod"), ExtResource("193_milkfish"), ExtResource("194_roosterfish"), ExtResource("195_tarpon_atlantic"), ExtResource("196_snook_common"), ExtResource("197_bonefish"), ExtResource("198_pompano_florida"), ExtResource("199_ladyfish"), ExtResource("200_permit"), ExtResource("201_whale_blue"), ExtResource("202_dolphin_bottlenose"), ExtResource("203_whale_humpback"), ExtResource("204_whale_killer"), ExtResource("205_whale_sperm")]
diff --git a/fish/pools/starter_pond_pool.tres b/fish/pools/starter_pond_pool.tres
deleted file mode 100644
index e9718d9..0000000
--- a/fish/pools/starter_pond_pool.tres
+++ /dev/null
@@ -1,113 +0,0 @@
-[gd_resource type="Resource" load_steps=108 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_pool.gd" id="1_pool"]
-[ext_resource type="Resource" path="res://fish/species/bowfin/bowfin.tres" id="2_bowfin"]
-[ext_resource type="Resource" path="res://fish/species/gar_spotted/gar_spotted.tres" id="3_gar_spotted"]
-[ext_resource type="Resource" path="res://fish/species/lungfish_west_african/lungfish_west_african.tres" id="4_lungfish_west_african"]
-[ext_resource type="Resource" path="res://fish/species/guppy/guppy.tres" id="5_guppy"]
-[ext_resource type="Resource" path="res://fish/species/tetra_neon/tetra_neon.tres" id="6_tetra_neon"]
-[ext_resource type="Resource" path="res://fish/species/danio_zebra/danio_zebra.tres" id="7_danio_zebra"]
-[ext_resource type="Resource" path="res://fish/species/largemouth_bass/largemouth_bass.tres" id="8_largemouth_bass"]
-[ext_resource type="Resource" path="res://fish/species/bullhead_black/bullhead_black.tres" id="9_bullhead_black"]
-[ext_resource type="Resource" path="res://fish/species/bullhead_brown/bullhead_brown.tres" id="10_bullhead_brown"]
-[ext_resource type="Resource" path="res://fish/species/catfish_channel/catfish_channel.tres" id="11_catfish_channel"]
-[ext_resource type="Resource" path="res://fish/species/catfish_white/catfish_white.tres" id="12_catfish_white"]
-[ext_resource type="Resource" path="res://fish/species/bullhead_yellow/bullhead_yellow.tres" id="13_bullhead_yellow"]
-[ext_resource type="Resource" path="res://fish/species/carp/carp.tres" id="14_carp"]
-[ext_resource type="Resource" path="res://fish/species/roach_common/roach_common.tres" id="15_roach_common"]
-[ext_resource type="Resource" path="res://fish/species/rudd/rudd.tres" id="16_rudd"]
-[ext_resource type="Resource" path="res://fish/species/tench/tench.tres" id="17_tench"]
-[ext_resource type="Resource" path="res://fish/species/crayfish_red_swamp/crayfish_red_swamp.tres" id="18_crayfish_red_swamp"]
-[ext_resource type="Resource" path="res://fish/species/minnow_fathead/minnow_fathead.tres" id="19_minnow_fathead"]
-[ext_resource type="Resource" path="res://fish/species/shiner_golden/shiner_golden.tres" id="20_shiner_golden"]
-[ext_resource type="Resource" path="res://fish/species/loach_weather/loach_weather.tres" id="21_loach_weather"]
-[ext_resource type="Resource" path="res://fish/species/goldfish_bubbleeye/goldfish_bubbleeye.tres" id="22_goldfish_bubbleeye"]
-[ext_resource type="Resource" path="res://fish/species/goldfish/goldfish.tres" id="23_goldfish"]
-[ext_resource type="Resource" path="res://fish/species/koi/koi.tres" id="24_koi"]
-[ext_resource type="Resource" path="res://fish/species/crappie_black/crappie_black.tres" id="25_crappie_black"]
-[ext_resource type="Resource" path="res://fish/species/bluegill/bluegill.tres" id="26_bluegill"]
-[ext_resource type="Resource" path="res://fish/species/green_sunfish/green_sunfish.tres" id="27_green_sunfish"]
-[ext_resource type="Resource" path="res://fish/species/pumpkinseed/pumpkinseed.tres" id="28_pumpkinseed"]
-[ext_resource type="Resource" path="res://fish/species/redear_sunfish/redear_sunfish.tres" id="29_redear_sunfish"]
-[ext_resource type="Resource" path="res://fish/species/warmouth/warmouth.tres" id="30_warmouth"]
-[ext_resource type="Resource" path="res://fish/species/pickerel_chain/pickerel_chain.tres" id="31_pickerel_chain"]
-[ext_resource type="Resource" path="res://fish/species/pickerel_grass/pickerel_grass.tres" id="32_pickerel_grass"]
-[ext_resource type="Resource" path="res://fish/species/betta_siamese/betta_siamese.tres" id="33_betta_siamese"]
-[ext_resource type="Resource" path="res://fish/species/catfish_walking/catfish_walking.tres" id="34_catfish_walking"]
-[ext_resource type="Resource" path="res://fish/species/sturgeon_lake/sturgeon_lake.tres" id="35_sturgeon_lake"]
-[ext_resource type="Resource" path="res://fish/species/burbot/burbot.tres" id="36_burbot"]
-[ext_resource type="Resource" path="res://fish/species/cisco/cisco.tres" id="37_cisco"]
-[ext_resource type="Resource" path="res://fish/species/whitefish_lake/whitefish_lake.tres" id="38_whitefish_lake"]
-[ext_resource type="Resource" path="res://fish/species/char_arctic/char_arctic.tres" id="39_char_arctic"]
-[ext_resource type="Resource" path="res://fish/species/trout_golden/trout_golden.tres" id="40_trout_golden"]
-[ext_resource type="Resource" path="res://fish/species/axolotl/axolotl.tres" id="41_axolotl"]
-[ext_resource type="Resource" path="res://fish/species/bream_common/bream_common.tres" id="42_bream_common"]
-[ext_resource type="Resource" path="res://fish/species/muskellunge/muskellunge.tres" id="43_muskellunge"]
-[ext_resource type="Resource" path="res://fish/species/shiner_emerald/shiner_emerald.tres" id="44_shiner_emerald"]
-[ext_resource type="Resource" path="res://fish/species/chub_lake/chub_lake.tres" id="45_chub_lake"]
-[ext_resource type="Resource" path="res://fish/species/crappie_white/crappie_white.tres" id="46_crappie_white"]
-[ext_resource type="Resource" path="res://fish/species/yellow_perch/yellow_perch.tres" id="47_yellow_perch"]
-[ext_resource type="Resource" path="res://fish/species/pike_northern/pike_northern.tres" id="48_pike_northern"]
-[ext_resource type="Resource" path="res://fish/species/saugeye/saugeye.tres" id="49_saugeye"]
-[ext_resource type="Resource" path="res://fish/species/walleye/walleye.tres" id="50_walleye"]
-[ext_resource type="Resource" path="res://fish/species/goby_round/goby_round.tres" id="51_goby_round"]
-[ext_resource type="Resource" path="res://fish/species/smelt_rainbow/smelt_rainbow.tres" id="52_smelt_rainbow"]
-[ext_resource type="Resource" path="res://fish/species/gourami_giant/gourami_giant.tres" id="53_gourami_giant"]
-[ext_resource type="Resource" path="res://fish/species/tilapia_nile/tilapia_nile.tres" id="54_tilapia_nile"]
-[ext_resource type="Resource" path="res://fish/species/gar_longnose/gar_longnose.tres" id="55_gar_longnose"]
-[ext_resource type="Resource" path="res://fish/species/paddlefish/paddlefish.tres" id="56_paddlefish"]
-[ext_resource type="Resource" path="res://fish/species/sturgeon_shovelnose/sturgeon_shovelnose.tres" id="57_sturgeon_shovelnose"]
-[ext_resource type="Resource" path="res://fish/species/foureyes_largescale/foureyes_largescale.tres" id="58_foureyes_largescale"]
-[ext_resource type="Resource" path="res://fish/species/grayling_arctic/grayling_arctic.tres" id="59_grayling_arctic"]
-[ext_resource type="Resource" path="res://fish/species/trout_brown/trout_brown.tres" id="60_trout_brown"]
-[ext_resource type="Resource" path="res://fish/species/trout_cutthroat/trout_cutthroat.tres" id="61_trout_cutthroat"]
-[ext_resource type="Resource" path="res://fish/species/trout_rainbow/trout_rainbow.tres" id="62_trout_rainbow"]
-[ext_resource type="Resource" path="res://fish/species/hellbender/hellbender.tres" id="63_hellbender"]
-[ext_resource type="Resource" path="res://fish/species/smallmouth_bass/smallmouth_bass.tres" id="64_smallmouth_bass"]
-[ext_resource type="Resource" path="res://fish/species/buffalo_bigmouth/buffalo_bigmouth.tres" id="65_buffalo_bigmouth"]
-[ext_resource type="Resource" path="res://fish/species/redhorse_golden/redhorse_golden.tres" id="66_redhorse_golden"]
-[ext_resource type="Resource" path="res://fish/species/quillback/quillback.tres" id="67_quillback"]
-[ext_resource type="Resource" path="res://fish/species/buffalo_smallmouth/buffalo_smallmouth.tres" id="68_buffalo_smallmouth"]
-[ext_resource type="Resource" path="res://fish/species/sucker_white/sucker_white.tres" id="69_sucker_white"]
-[ext_resource type="Resource" path="res://fish/species/catfish_blue/catfish_blue.tres" id="70_catfish_blue"]
-[ext_resource type="Resource" path="res://fish/species/catfish_flathead/catfish_flathead.tres" id="71_catfish_flathead"]
-[ext_resource type="Resource" path="res://fish/species/madtom_tadpole/madtom_tadpole.tres" id="72_madtom_tadpole"]
-[ext_resource type="Resource" path="res://fish/species/barbel_common/barbel_common.tres" id="73_barbel_common"]
-[ext_resource type="Resource" path="res://fish/species/chub_european/chub_european.tres" id="74_chub_european"]
-[ext_resource type="Resource" path="res://fish/species/fallfish/fallfish.tres" id="75_fallfish"]
-[ext_resource type="Resource" path="res://fish/species/ide/ide.tres" id="76_ide"]
-[ext_resource type="Resource" path="res://fish/species/gar_alligator/gar_alligator.tres" id="77_gar_alligator"]
-[ext_resource type="Resource" path="res://fish/species/arapaima/arapaima.tres" id="78_arapaima"]
-[ext_resource type="Resource" path="res://fish/species/tigerfish_goliath/tigerfish_goliath.tres" id="79_tigerfish_goliath"]
-[ext_resource type="Resource" path="res://fish/species/shiner_common/shiner_common.tres" id="80_shiner_common"]
-[ext_resource type="Resource" path="res://fish/species/chub_creek/chub_creek.tres" id="81_chub_creek"]
-[ext_resource type="Resource" path="res://fish/species/chub_flame/chub_flame.tres" id="82_chub_flame"]
-[ext_resource type="Resource" path="res://fish/species/electric_eel/electric_eel.tres" id="83_electric_eel"]
-[ext_resource type="Resource" path="res://fish/species/freshwater_drum/freshwater_drum.tres" id="84_freshwater_drum"]
-[ext_resource type="Resource" path="res://fish/species/goldeye/goldeye.tres" id="85_goldeye"]
-[ext_resource type="Resource" path="res://fish/species/mooneye/mooneye.tres" id="86_mooneye"]
-[ext_resource type="Resource" path="res://fish/species/sauger/sauger.tres" id="87_sauger"]
-[ext_resource type="Resource" path="res://fish/species/eel_american/eel_american.tres" id="88_eel_american"]
-[ext_resource type="Resource" path="res://fish/species/eel_european/eel_european.tres" id="89_eel_european"]
-[ext_resource type="Resource" path="res://fish/species/smelt_eulachon/smelt_eulachon.tres" id="90_smelt_eulachon"]
-[ext_resource type="Resource" path="res://fish/species/sturgeon_white/sturgeon_white.tres" id="91_sturgeon_white"]
-[ext_resource type="Resource" path="res://fish/species/trout_steelhead/trout_steelhead.tres" id="92_trout_steelhead"]
-[ext_resource type="Resource" path="res://fish/species/stingray_ocellate_river/stingray_ocellate_river.tres" id="93_stingray_ocellate_river"]
-[ext_resource type="Resource" path="res://fish/species/white_perch/white_perch.tres" id="94_white_perch"]
-[ext_resource type="Resource" path="res://fish/species/barramundi/barramundi.tres" id="95_barramundi"]
-[ext_resource type="Resource" path="res://fish/species/knifefish_clown/knifefish_clown.tres" id="96_knifefish_clown"]
-[ext_resource type="Resource" path="res://fish/species/loach_clown/loach_clown.tres" id="97_loach_clown"]
-[ext_resource type="Resource" path="res://fish/species/discus/discus.tres" id="98_discus"]
-[ext_resource type="Resource" path="res://fish/species/angelfish_freshwater/angelfish_freshwater.tres" id="99_angelfish_freshwater"]
-[ext_resource type="Resource" path="res://fish/species/oscar/oscar.tres" id="100_oscar"]
-[ext_resource type="Resource" path="res://fish/species/tambaqui/tambaqui.tres" id="101_tambaqui"]
-[ext_resource type="Resource" path="res://fish/species/snakehead_giant/snakehead_giant.tres" id="102_snakehead_giant"]
-[ext_resource type="Resource" path="res://fish/species/dorado_golden/dorado_golden.tres" id="103_dorado_golden"]
-[ext_resource type="Resource" path="res://fish/species/mahseer_golden/mahseer_golden.tres" id="104_mahseer_golden"]
-[ext_resource type="Resource" path="res://fish/species/peacock_bass/peacock_bass.tres" id="105_peacock_bass"]
-[ext_resource type="Resource" path="res://fish/species/piranha_red_bellied/piranha_red_bellied.tres" id="106_piranha_red_bellied"]
-[ext_resource type="Resource" path="res://fish/species/arowana_silver/arowana_silver.tres" id="107_arowana_silver"]
-
-[resource]
-script = ExtResource("1_pool")
-candidates = [ExtResource("2_bowfin"), ExtResource("3_gar_spotted"), ExtResource("4_lungfish_west_african"), ExtResource("5_guppy"), ExtResource("6_tetra_neon"), ExtResource("7_danio_zebra"), ExtResource("8_largemouth_bass"), ExtResource("9_bullhead_black"), ExtResource("10_bullhead_brown"), ExtResource("11_catfish_channel"), ExtResource("12_catfish_white"), ExtResource("13_bullhead_yellow"), ExtResource("14_carp"), ExtResource("15_roach_common"), ExtResource("16_rudd"), ExtResource("17_tench"), ExtResource("18_crayfish_red_swamp"), ExtResource("19_minnow_fathead"), ExtResource("20_shiner_golden"), ExtResource("21_loach_weather"), ExtResource("22_goldfish_bubbleeye"), ExtResource("23_goldfish"), ExtResource("24_koi"), ExtResource("25_crappie_black"), ExtResource("26_bluegill"), ExtResource("27_green_sunfish"), ExtResource("28_pumpkinseed"), ExtResource("29_redear_sunfish"), ExtResource("30_warmouth"), ExtResource("31_pickerel_chain"), ExtResource("32_pickerel_grass"), ExtResource("33_betta_siamese"), ExtResource("34_catfish_walking"), ExtResource("35_sturgeon_lake"), ExtResource("36_burbot"), ExtResource("37_cisco"), ExtResource("38_whitefish_lake"), ExtResource("39_char_arctic"), ExtResource("40_trout_golden"), ExtResource("41_axolotl"), ExtResource("42_bream_common"), ExtResource("43_muskellunge"), ExtResource("44_shiner_emerald"), ExtResource("45_chub_lake"), ExtResource("46_crappie_white"), ExtResource("47_yellow_perch"), ExtResource("48_pike_northern"), ExtResource("49_saugeye"), ExtResource("50_walleye"), ExtResource("51_goby_round"), ExtResource("52_smelt_rainbow"), ExtResource("53_gourami_giant"), ExtResource("54_tilapia_nile"), ExtResource("55_gar_longnose"), ExtResource("56_paddlefish"), ExtResource("57_sturgeon_shovelnose"), ExtResource("58_foureyes_largescale"), ExtResource("59_grayling_arctic"), ExtResource("60_trout_brown"), ExtResource("61_trout_cutthroat"), ExtResource("62_trout_rainbow"), ExtResource("63_hellbender"), ExtResource("64_smallmouth_bass"), ExtResource("65_buffalo_bigmouth"), ExtResource("66_redhorse_golden"), ExtResource("67_quillback"), ExtResource("68_buffalo_smallmouth"), ExtResource("69_sucker_white"), ExtResource("70_catfish_blue"), ExtResource("71_catfish_flathead"), ExtResource("72_madtom_tadpole"), ExtResource("73_barbel_common"), ExtResource("74_chub_european"), ExtResource("75_fallfish"), ExtResource("76_ide"), ExtResource("77_gar_alligator"), ExtResource("78_arapaima"), ExtResource("79_tigerfish_goliath"), ExtResource("80_shiner_common"), ExtResource("81_chub_creek"), ExtResource("82_chub_flame"), ExtResource("83_electric_eel"), ExtResource("84_freshwater_drum"), ExtResource("85_goldeye"), ExtResource("86_mooneye"), ExtResource("87_sauger"), ExtResource("88_eel_american"), ExtResource("89_eel_european"), ExtResource("90_smelt_eulachon"), ExtResource("91_sturgeon_white"), ExtResource("92_trout_steelhead"), ExtResource("93_stingray_ocellate_river"), ExtResource("94_white_perch"), ExtResource("95_barramundi"), ExtResource("96_knifefish_clown"), ExtResource("97_loach_clown"), ExtResource("98_discus"), ExtResource("99_angelfish_freshwater"), ExtResource("100_oscar"), ExtResource("101_tambaqui"), ExtResource("102_snakehead_giant"), ExtResource("103_dorado_golden"), ExtResource("104_mahseer_golden"), ExtResource("105_peacock_bass"), ExtResource("106_piranha_red_bellied"), ExtResource("107_arowana_silver")]
diff --git a/fish/species/abalone_red/abalone_red.tres b/fish/species/abalone_red/abalone_red.tres
deleted file mode 100644
index 6efe784..0000000
--- a/fish/species/abalone_red/abalone_red.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"abalone_red"
-display_name = "red abalone"
-active = true
-catalog_number = 6405
-collection_group = &"Shellfish"
-logbook_fact = "red abalones cling to pacific rocks with broad muscular feet. rows of shell holes release water that has passed over the gills."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.09
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 12.0
-sell_value_min = 24
-sell_value_max = 56
-sell_value_curve = 0.88
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/amberjack_greater/amberjack_greater.tres b/fish/species/amberjack_greater/amberjack_greater.tres
deleted file mode 100644
index b0e40b8..0000000
--- a/fish/species/amberjack_greater/amberjack_greater.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"amberjack_greater"
-display_name = "greater amberjack"
-active = true
-catalog_number = 3604
-collection_group = &"Large coastal predators"
-logbook_fact = "greater amberjacks circle reefs, wrecks, and offshore platforms. a dark stripe crosses the eye of their sturdy torpedo-shaped bodies."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.25
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 5.0
-weight_max_lb = 90.0
-sell_value_min = 15
-sell_value_max = 42
-sell_value_curve = 0.82
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/amberjack_lesser/amberjack_lesser.tres b/fish/species/amberjack_lesser/amberjack_lesser.tres
deleted file mode 100644
index 1d5983c..0000000
--- a/fish/species/amberjack_lesser/amberjack_lesser.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"amberjack_lesser"
-display_name = "lesser amberjack"
-active = true
-catalog_number = 3605
-collection_group = &"Large coastal predators"
-logbook_fact = "lesser amberjacks school around deep reefs and floating objects. an enlarged eye suits their preference for dimmer water."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.75
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 20.0
-sell_value_min = 7
-sell_value_max = 18
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/anchovy_european/anchovy_european.tres b/fish/species/anchovy_european/anchovy_european.tres
deleted file mode 100644
index 0a7d122..0000000
--- a/fish/species/anchovy_european/anchovy_european.tres
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://6a063go0dcqk"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/805.png" id="4_texture"]
-
-[sub_resource type="Resource" id="AnchovyEuropeanAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"anchovy_european"
-display_name = "european anchovy"
-active = true
-catalog_number = 805
-collection_group = &"Coastal schooling fish"
-logbook_fact = "european anchovies form dense schools and feed by filtering tiny plankton from coastal water."
-available_seasons = 7
-allowed_water_types = 2
-base_catch_weight = 0.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("AnchovyEuropeanAvailability")
-weight_min_lb = 0.05
-weight_max_lb = 0.25
-display_scale_min = 0.85
-display_scale_max = 1.0
-display_scale_curve = 0.9
-sell_value_min = 2
-sell_value_max = 4
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/anchovy_european/fish_anchovy_european.png b/fish/species/anchovy_european/fish_anchovy_european.png
deleted file mode 100644
index d691c72..0000000
Binary files a/fish/species/anchovy_european/fish_anchovy_european.png and /dev/null differ
diff --git a/fish/species/anchovy_european/fish_anchovy_european.png.import b/fish/species/anchovy_european/fish_anchovy_european.png.import
deleted file mode 100644
index c4fd63a..0000000
--- a/fish/species/anchovy_european/fish_anchovy_european.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://debgohfsg4ec7"
-path="res://.godot/imported/fish_anchovy_european.png-67d070da34e29a3123fed0030af0d694.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/anchovy_european/fish_anchovy_european.png"
-dest_files=["res://.godot/imported/fish_anchovy_european.png-67d070da34e29a3123fed0030af0d694.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/anchovy_northern/anchovy_northern.tres b/fish/species/anchovy_northern/anchovy_northern.tres
deleted file mode 100644
index e2c7fa0..0000000
--- a/fish/species/anchovy_northern/anchovy_northern.tres
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://c30colwbajqf8"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/808.png" id="4_texture"]
-
-[sub_resource type="Resource" id="AnchovyNorthernAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"anchovy_northern"
-display_name = "northern anchovy"
-active = true
-catalog_number = 808
-collection_group = &"Coastal schooling fish"
-logbook_fact = "northern anchovies gather in huge schools along the pacific coast and filter plankton with their fine gill rakers."
-available_seasons = 15
-allowed_water_types = 2
-base_catch_weight = 0.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("AnchovyNorthernAvailability")
-weight_min_lb = 0.05
-weight_max_lb = 0.35
-display_scale_min = 0.85
-display_scale_max = 1.0
-display_scale_curve = 0.9
-sell_value_min = 2
-sell_value_max = 4
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/anchovy_northern/fish_anchovy_northern.png b/fish/species/anchovy_northern/fish_anchovy_northern.png
deleted file mode 100644
index f8749fb..0000000
Binary files a/fish/species/anchovy_northern/fish_anchovy_northern.png and /dev/null differ
diff --git a/fish/species/anchovy_northern/fish_anchovy_northern.png.import b/fish/species/anchovy_northern/fish_anchovy_northern.png.import
deleted file mode 100644
index 68ae1ea..0000000
--- a/fish/species/anchovy_northern/fish_anchovy_northern.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://6sikqc8eivta"
-path="res://.godot/imported/fish_anchovy_northern.png-b1d3cbc1c8208168c212589a431f1227.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/anchovy_northern/fish_anchovy_northern.png"
-dest_files=["res://.godot/imported/fish_anchovy_northern.png-b1d3cbc1c8208168c212589a431f1227.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/angelfish_freshwater/angelfish_freshwater.tres b/fish/species/angelfish_freshwater/angelfish_freshwater.tres
deleted file mode 100644
index 7a58450..0000000
--- a/fish/species/angelfish_freshwater/angelfish_freshwater.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"angelfish_freshwater"
-display_name = "freshwater angelfish"
-active = true
-catalog_number = 7504
-collection_group = &"Tropical freshwater fish"
-logbook_fact = "freshwater angelfish slip between tall plants on narrow triangular bodies. long fins and vertical bars resemble stems and shadows."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 0.5
-sell_value_min = 10
-sell_value_max = 22
-sell_value_curve = 0.95
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/angelfish_queen/angelfish_queen.tres b/fish/species/angelfish_queen/angelfish_queen.tres
deleted file mode 100644
index 5a15f5b..0000000
--- a/fish/species/angelfish_queen/angelfish_queen.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"angelfish_queen"
-display_name = "queen angelfish"
-active = true
-catalog_number = 5703
-collection_group = &"Reef ornamentals"
-logbook_fact = "queen angelfish graze sponges from warm atlantic reefs. a blue-ringed spot on the forehead resembles a tiny crown."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 2.0
-sell_value_min = 11
-sell_value_max = 25
-sell_value_curve = 0.94
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/anglerfish_black_seadevil/anglerfish_black_seadevil.tres b/fish/species/anglerfish_black_seadevil/anglerfish_black_seadevil.tres
deleted file mode 100644
index 83b1fb3..0000000
--- a/fish/species/anglerfish_black_seadevil/anglerfish_black_seadevil.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"anglerfish_black_seadevil"
-display_name = "black seadevil"
-active = true
-catalog_number = 1801
-collection_group = &"Deep-sea oddities"
-logbook_fact = "black seadevils lure prey with a glowing organ suspended above the mouth. tiny males permanently attach to much larger females."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.05
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 2.0
-sell_value_min = 48
-sell_value_max = 105
-sell_value_curve = 0.92
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/arapaima/arapaima.tres b/fish/species/arapaima/arapaima.tres
deleted file mode 100644
index b43e3bd..0000000
--- a/fish/species/arapaima/arapaima.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"arapaima"
-display_name = "arapaima"
-active = true
-catalog_number = 2702
-collection_group = &"Freshwater giants"
-logbook_fact = "arapaima must surface frequently to gulp air through a specialized swim bladder. large armored scales protect their long river-going bodies."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 4
-base_catch_weight = 0.04
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 40.0
-weight_max_lb = 400.0
-sell_value_min = 60
-sell_value_max = 140
-sell_value_curve = 0.78
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/arowana_silver/arowana_silver.tres b/fish/species/arowana_silver/arowana_silver.tres
deleted file mode 100644
index 42baf33..0000000
--- a/fish/species/arowana_silver/arowana_silver.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"arowana_silver"
-display_name = "silver arowana"
-active = true
-catalog_number = 7606
-collection_group = &"Tropical freshwater predators"
-logbook_fact = "silver arowanas patrol the surface with upturned mouths and long bodies. powerful jumps capture insects and small animals above the water."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 3
-base_catch_weight = 0.11
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 15.0
-sell_value_min = 25
-sell_value_max = 56
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/axolotl/axolotl.tres b/fish/species/axolotl/axolotl.tres
deleted file mode 100644
index c201f9c..0000000
--- a/fish/species/axolotl/axolotl.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"axolotl"
-display_name = "axolotl"
-active = true
-catalog_number = 2001
-collection_group = &"Freshwater amphibians"
-logbook_fact = "axolotls remain aquatic and keep feathery external gills throughout adulthood. lost limbs and parts of several organs can regenerate."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 4
-base_catch_weight = 0.04
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 0.7
-sell_value_min = 36
-sell_value_max = 78
-sell_value_curve = 0.96
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/barbel_common/barbel_common.tres b/fish/species/barbel_common/barbel_common.tres
deleted file mode 100644
index 9cbafff..0000000
--- a/fish/species/barbel_common/barbel_common.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"barbel_common"
-display_name = "common barbel"
-active = true
-catalog_number = 2501
-collection_group = &"Freshwater coarse fish"
-logbook_fact = "common barbels hold close to fast river bottoms with streamlined bodies and broad fins. four mouth barbels locate food among stones."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.28
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 25.0
-sell_value_min = 13
-sell_value_max = 31
-sell_value_curve = 0.85
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/barracuda_great/barracuda_great.tres b/fish/species/barracuda_great/barracuda_great.tres
deleted file mode 100644
index f15092c..0000000
--- a/fish/species/barracuda_great/barracuda_great.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"barracuda_great"
-display_name = "great barracuda"
-active = true
-catalog_number = 5804
-collection_group = &"Reef predators"
-logbook_fact = "great barracudas hover motionless before accelerating toward prey. long jaws carry two distinct rows of sharp teeth."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.12
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 5.0
-weight_max_lb = 80.0
-sell_value_min = 28
-sell_value_max = 65
-sell_value_curve = 0.82
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/barramundi/barramundi.tres b/fish/species/barramundi/barramundi.tres
deleted file mode 100644
index 705001d..0000000
--- a/fish/species/barramundi/barramundi.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"barramundi"
-display_name = "barramundi"
-active = true
-catalog_number = 7302
-collection_group = &"Tropical estuary"
-logbook_fact = "barramundi move between rivers and coasts and can breathe in low-oxygen water. many mature first as males and later become females."
-available_seasons = 15
-allowed_water_types = 3
-rarity = 2
-base_catch_weight = 0.24
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 2.0
-weight_max_lb = 45.0
-sell_value_min = 15
-sell_value_max = 38
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/barreleye_pacific/barreleye_pacific.tres b/fish/species/barreleye_pacific/barreleye_pacific.tres
deleted file mode 100644
index c0705dd..0000000
--- a/fish/species/barreleye_pacific/barreleye_pacific.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"barreleye_pacific"
-display_name = "Pacific barreleye"
-active = true
-catalog_number = 1804
-collection_group = &"Deep-sea oddities"
-logbook_fact = "pacific barreleyes peer upward through transparent shields covering the head. tubular eyes rotate forward when the fish turns to feed."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.045
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 2.5
-sell_value_min = 42
-sell_value_max = 88
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/bass/bass.tres b/fish/species/bass/bass.tres
index 9d52609..bf7d261 100644
--- a/fish/species/bass/bass.tres
+++ b/fish/species/bass/bass.tres
@@ -1,12 +1,14 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://f1cibmajx3lo"]
+[gd_resource type="Resource" load_steps=5 format=3]
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
+[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
+[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability_script"]
+[ext_resource type="Texture2D" path="res://fish/species/bass/fish_bass_striped.png" id="4_texture"]
[sub_resource type="Resource" id="BassAvailability"]
script = ExtResource("3_availability_script")
+allow_day = true
+allow_night = true
preferred_bait_tags = Array[StringName]([&"worm", &"minnow"])
preferred_bait_weight_multiplier = 1.4
@@ -14,12 +16,6 @@ preferred_bait_weight_multiplier = 1.4
script = ExtResource("1_fish_data")
id = &"bass"
display_name = "striped bass"
-active = true
-catalog_number = 6701
-collection_group = &"Temperate coastal"
-logbook_fact = "striped bass split their lives between salt and fresh water, returning upriver to spawn. a long-lived striper may see three decades."
-available_seasons = 5
-allowed_water_types = 2
rarity = 1
base_catch_weight = 4.0
catch_profile = ExtResource("2_profile")
diff --git a/fish/species/bass/fish_bass_striped.png b/fish/species/bass/fish_bass_striped.png
index e39f48f..89b5b5e 100644
Binary files a/fish/species/bass/fish_bass_striped.png and b/fish/species/bass/fish_bass_striped.png differ
diff --git a/fish/species/beetle_stag_common/beetle_stag_common.png b/fish/species/beetle_stag_common/beetle_stag_common.png
deleted file mode 100644
index 5ada780..0000000
Binary files a/fish/species/beetle_stag_common/beetle_stag_common.png and /dev/null differ
diff --git a/fish/species/beetle_stag_common/beetle_stag_common.png.import b/fish/species/beetle_stag_common/beetle_stag_common.png.import
deleted file mode 100644
index 4ad3482..0000000
--- a/fish/species/beetle_stag_common/beetle_stag_common.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://blw6lydoba8kd"
-path="res://.godot/imported/beetle_stag_common.png-5eb7426d7dbda5083e39efc4cb980c8d.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/beetle_stag_common/beetle_stag_common.png"
-dest_files=["res://.godot/imported/beetle_stag_common.png-5eb7426d7dbda5083e39efc4cb980c8d.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/beetle_stag_common/beetle_stag_common.tres b/fish/species/beetle_stag_common/beetle_stag_common.tres
deleted file mode 100644
index f0f0e6d..0000000
--- a/fish/species/beetle_stag_common/beetle_stag_common.tres
+++ /dev/null
@@ -1,27 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=4 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Texture2D" path="res://fish/species/beetle_stag_common/beetle_stag_common.png" id="3_texture"]
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"beetle_stag_common"
-display_name = "common stag beetle"
-active = true
-catalog_number = 8001
-collection_group = &"Beetles"
-logbook_fact = "common stag beetles use broad jaws to wrestle rivals and cling to tree bark. adults feed on sap and other sweet plant juices."
-collection_method = 1
-habitat_label = "tree trunks"
-available_seasons = 2
-allowed_water_types = 0
-rarity = 0
-base_catch_weight = 1.0
-catch_profile = ExtResource("2_profile")
-weight_min_lb = 0.02
-weight_max_lb = 0.12
-sell_value_min = 4
-sell_value_max = 12
-sell_value_curve = 1.0
-display_texture = ExtResource("3_texture")
diff --git a/fish/species/betta_siamese/betta_siamese.tres b/fish/species/betta_siamese/betta_siamese.tres
deleted file mode 100644
index 4c9ca3c..0000000
--- a/fish/species/betta_siamese/betta_siamese.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/7508.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"betta_siamese"
-display_name = "Siamese fighting fish"
-active = true
-catalog_number = 7508
-collection_group = &"Tropical freshwater fish"
-logbook_fact = "siamese fighting fish breathe surface air with a labyrinth organ. males build bubble nests and guard the eggs beneath them."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 1.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.01
-weight_max_lb = 0.15
-sell_value_min = 5
-sell_value_max = 10
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/black_drum/black_drum.tres b/fish/species/black_drum/black_drum.tres
deleted file mode 100644
index b542327..0000000
--- a/fish/species/black_drum/black_drum.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"black_drum"
-display_name = "black drum"
-active = true
-catalog_number = 6802
-collection_group = &"Temperate estuary"
-logbook_fact = "black drum crush oysters and crabs with rounded throat teeth. large adults develop dark vertical bars that fade with age."
-available_seasons = 7
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.7
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 70.0
-sell_value_min = 8
-sell_value_max = 27
-sell_value_curve = 0.82
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/blobfish_smooth_head/blobfish_smooth_head.tres b/fish/species/blobfish_smooth_head/blobfish_smooth_head.tres
deleted file mode 100644
index 4245304..0000000
--- a/fish/species/blobfish_smooth_head/blobfish_smooth_head.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"blobfish_smooth_head"
-display_name = "smooth-head blobfish"
-active = true
-catalog_number = 1805
-collection_group = &"Deep-sea oddities"
-logbook_fact = "smooth-head blobfish rest on deep slopes where pressure supports their soft bodies. they conserve energy by waiting for edible matter nearby."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.04
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 5.0
-sell_value_min = 42
-sell_value_max = 90
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/bluefish/bluefish.tres b/fish/species/bluefish/bluefish.tres
deleted file mode 100644
index d8888f7..0000000
--- a/fish/species/bluefish/bluefish.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"bluefish"
-display_name = "bluefish"
-active = true
-catalog_number = 702
-collection_group = &"Coastal pelagic fish"
-logbook_fact = "bluefish race in coordinated schools while pursuing smaller fish. sharp teeth and powerful jaws make their feeding bursts especially fierce."
-available_seasons = 7
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.85
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 25.0
-sell_value_min = 7
-sell_value_max = 19
-sell_value_curve = 0.85
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/bluegill/bluegill.tres b/fish/species/bluegill/bluegill.tres
index 1ad5656..e766b5f 100644
--- a/fish/species/bluegill/bluegill.tres
+++ b/fish/species/bluegill/bluegill.tres
@@ -2,19 +2,13 @@
[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Resource" path="res://fish/day_availability.tres" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
+[ext_resource type="Resource" path="res://fish/default_availability.tres" id="3_availability"]
+[ext_resource type="Texture2D" path="res://fish/species/bluegill/fish_bluegill.png" id="4_texture"]
[resource]
script = ExtResource("1_fish_data")
id = &"bluegill"
display_name = "bluegill"
-active = true
-catalog_number = 3102
-collection_group = &"Freshwater panfish"
-logbook_fact = "bluegill fathers sweep shallow bowls into the bottom for nests, then stand guard. whole neighborhoods of nests can crowd together."
-available_seasons = 7
-allowed_water_types = 1
rarity = 0
base_catch_weight = 10.0
catch_profile = ExtResource("2_profile")
diff --git a/fish/species/bluegill/fish_bluegill.png b/fish/species/bluegill/fish_bluegill.png
index 9ad6e4f..8569e3c 100644
Binary files a/fish/species/bluegill/fish_bluegill.png and b/fish/species/bluegill/fish_bluegill.png differ
diff --git a/fish/species/bonefish/bonefish.tres b/fish/species/bonefish/bonefish.tres
deleted file mode 100644
index b2949d2..0000000
--- a/fish/species/bonefish/bonefish.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"bonefish"
-display_name = "bonefish"
-active = true
-catalog_number = 7401
-collection_group = &"Tropical flats fish"
-logbook_fact = "bonefish sweep tropical flats in silvery schools and root for buried prey. their speed and pale bodies make them difficult to follow in shallow water."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.25
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 15.0
-sell_value_min = 13
-sell_value_max = 31
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/bowfin/bowfin.png b/fish/species/bowfin/bowfin.png
deleted file mode 100644
index 9d40491..0000000
Binary files a/fish/species/bowfin/bowfin.png and /dev/null differ
diff --git a/fish/species/bowfin/bowfin.png.import b/fish/species/bowfin/bowfin.png.import
deleted file mode 100644
index b5f2a10..0000000
--- a/fish/species/bowfin/bowfin.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dgqegw70vkihk"
-path="res://.godot/imported/bowfin.png-062edda1eb3529c0e6365e0ca93cf3ef.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/bowfin/bowfin.png"
-dest_files=["res://.godot/imported/bowfin.png-062edda1eb3529c0e6365e0ca93cf3ef.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/bowfin/bowfin.tres b/fish/species/bowfin/bowfin.tres
deleted file mode 100644
index 7b2881f..0000000
--- a/fish/species/bowfin/bowfin.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/101.png" id="4_texture"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"bowfin"
-display_name = "bowfin"
-active = true
-catalog_number = 101
-collection_group = &"Ancient freshwater fish"
-logbook_fact = "bowfin can gulp air at the surface when warm backwaters run low on oxygen. a long dorsal fin ripples as they stalk prey."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.28
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 20.0
-sell_value_min = 13
-sell_value_max = 30
-sell_value_curve = 0.85
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/boxfish_yellow/boxfish_yellow.tres b/fish/species/boxfish_yellow/boxfish_yellow.tres
deleted file mode 100644
index aeda5ad..0000000
--- a/fish/species/boxfish_yellow/boxfish_yellow.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"boxfish_yellow"
-display_name = "yellow boxfish"
-active = true
-catalog_number = 5608
-collection_group = &"Reef oddities"
-logbook_fact = "yellow boxfish juveniles are bright cubes covered in black spots. rigid body armor leaves small fins to provide careful steering."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.32
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.03
-weight_max_lb = 0.8
-sell_value_min = 10
-sell_value_max = 22
-sell_value_curve = 0.96
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/bream_common/bream_common.tres b/fish/species/bream_common/bream_common.tres
deleted file mode 100644
index 1c7398c..0000000
--- a/fish/species/bream_common/bream_common.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"bream_common"
-display_name = "common bream"
-active = true
-catalog_number = 2502
-collection_group = &"Freshwater coarse fish"
-logbook_fact = "common bream gather in slow rivers and lakes with silvery, deep-sided bodies. protruding mouths sift insect larvae from soft bottoms."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 1.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 15.0
-sell_value_min = 4
-sell_value_max = 12
-sell_value_curve = 0.88
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/buffalo_bigmouth/buffalo_bigmouth.tres b/fish/species/buffalo_bigmouth/buffalo_bigmouth.tres
deleted file mode 100644
index bae0473..0000000
--- a/fish/species/buffalo_bigmouth/buffalo_bigmouth.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"buffalo_bigmouth"
-display_name = "bigmouth buffalo"
-active = true
-catalog_number = 2301
-collection_group = &"Freshwater bottom fish"
-logbook_fact = "bigmouth buffalo filter plankton with fine gill rakers instead of rooting for prey. some individuals survive for more than a century."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 3
-base_catch_weight = 0.12
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 8.0
-weight_max_lb = 80.0
-sell_value_min = 28
-sell_value_max = 63
-sell_value_curve = 0.82
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/buffalo_smallmouth/buffalo_smallmouth.tres b/fish/species/buffalo_smallmouth/buffalo_smallmouth.tres
deleted file mode 100644
index 2756035..0000000
--- a/fish/species/buffalo_smallmouth/buffalo_smallmouth.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"buffalo_smallmouth"
-display_name = "smallmouth buffalo"
-active = true
-catalog_number = 2304
-collection_group = &"Freshwater bottom fish"
-logbook_fact = "smallmouth buffalo use thick lips to graze bottom-dwelling food in warm rivers. their deep, heavy bodies form a pronounced arch behind the head."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 3.0
-weight_max_lb = 45.0
-sell_value_min = 14
-sell_value_max = 34
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/bullhead_black/bullhead_black.tres b/fish/species/bullhead_black/bullhead_black.tres
deleted file mode 100644
index 1d904bd..0000000
--- a/fish/species/bullhead_black/bullhead_black.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"bullhead_black"
-display_name = "black bullhead"
-active = true
-catalog_number = 2401
-collection_group = &"Freshwater catfish"
-logbook_fact = "black bullheads tolerate warm, cloudy water with little oxygen. stout spines protect the dorsal and pectoral fins from predators."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 1.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 6.0
-sell_value_min = 5
-sell_value_max = 11
-sell_value_curve = 0.92
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/bullhead_brown/bullhead_brown.tres b/fish/species/bullhead_brown/bullhead_brown.tres
deleted file mode 100644
index b3651b2..0000000
--- a/fish/species/bullhead_brown/bullhead_brown.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"bullhead_brown"
-display_name = "brown bullhead"
-active = true
-catalog_number = 2403
-collection_group = &"Freshwater catfish"
-logbook_fact = "brown bullheads search muddy bottoms with eight sensitive barbels. males guard nests and herd newly hatched young in a dark moving ball."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 2.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 4.0
-sell_value_min = 3
-sell_value_max = 7
-sell_value_curve = 0.95
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/bullhead_yellow/bullhead_yellow.tres b/fish/species/bullhead_yellow/bullhead_yellow.tres
deleted file mode 100644
index 4dce589..0000000
--- a/fish/species/bullhead_yellow/bullhead_yellow.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"bullhead_yellow"
-display_name = "yellow bullhead"
-active = true
-catalog_number = 2408
-collection_group = &"Freshwater catfish"
-logbook_fact = "yellow bullheads use pale chin barbels to distinguish food in murky shallows. both parents may guard the nest and schooling young."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 2.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 3.5
-sell_value_min = 3
-sell_value_max = 7
-sell_value_curve = 0.95
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/burbot/burbot.tres b/fish/species/burbot/burbot.tres
deleted file mode 100644
index e46006c..0000000
--- a/fish/species/burbot/burbot.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"burbot"
-display_name = "burbot"
-active = true
-catalog_number = 1001
-collection_group = &"Cold freshwater fish"
-logbook_fact = "burbot are the only freshwater members of the cod family. they become most active in cold darkness and may spawn beneath winter ice."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.24
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 18.0
-sell_value_min = 12
-sell_value_max = 28
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/butterflyfish_copperband/butterflyfish_copperband.tres b/fish/species/butterflyfish_copperband/butterflyfish_copperband.tres
deleted file mode 100644
index 7a7c112..0000000
--- a/fish/species/butterflyfish_copperband/butterflyfish_copperband.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"butterflyfish_copperband"
-display_name = "copperband butterflyfish"
-active = true
-catalog_number = 5701
-collection_group = &"Reef ornamentals"
-logbook_fact = "copperband butterflyfish probe reef cracks with long narrow snouts. bold orange bands and a false eyespot confuse predators."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.32
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.03
-weight_max_lb = 0.4
-sell_value_min = 9
-sell_value_max = 21
-sell_value_curve = 0.98
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/capelin/capelin.tres b/fish/species/capelin/capelin.tres
deleted file mode 100644
index 5759cb8..0000000
--- a/fish/species/capelin/capelin.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"capelin"
-display_name = "capelin"
-active = true
-catalog_number = 1302
-collection_group = &"Cold schooling fish"
-logbook_fact = "capelin are small polar fish that feed whales, seabirds, and larger fish. many schools roll onto beaches or shallows to release their eggs."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 1.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.03
-weight_max_lb = 0.2
-sell_value_min = 4
-sell_value_max = 8
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/carp/carp.tres b/fish/species/carp/carp.tres
index 24b50e7..bb4642d 100644
--- a/fish/species/carp/carp.tres
+++ b/fish/species/carp/carp.tres
@@ -1,12 +1,14 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://cdkdwpwu384ck"]
+[gd_resource type="Resource" load_steps=5 format=3]
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
+[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
[ext_resource type="Resource" path="res://fishing/carp_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
+[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability_script"]
+[ext_resource type="Texture2D" path="res://fish/species/carp/fish_carp_common.png" id="4_texture"]
[sub_resource type="Resource" id="CarpAvailability"]
script = ExtResource("3_availability_script")
+allow_day = true
+allow_night = true
preferred_bait_tags = Array[StringName]([&"corn", &"bread"])
preferred_bait_weight_multiplier = 1.5
@@ -14,12 +16,6 @@ preferred_bait_weight_multiplier = 1.5
script = ExtResource("1_fish_data")
id = &"carp"
display_name = "common carp"
-active = true
-catalog_number = 2503
-collection_group = &"Freshwater coarse fish"
-logbook_fact = "whisker-like barbels help a common carp investigate the bottom. nosing through sediment can cloud the water and loosen plants."
-available_seasons = 15
-allowed_water_types = 1
rarity = 2
base_catch_weight = 1.5
catch_profile = ExtResource("2_profile")
diff --git a/fish/species/carp/fish_carp_common.png b/fish/species/carp/fish_carp_common.png
index 3a98f8e..50d0b31 100644
Binary files a/fish/species/carp/fish_carp_common.png and b/fish/species/carp/fish_carp_common.png differ
diff --git a/fish/species/catfish_blue/catfish_blue.tres b/fish/species/catfish_blue/catfish_blue.tres
deleted file mode 100644
index 8df5866..0000000
--- a/fish/species/catfish_blue/catfish_blue.tres
+++ /dev/null
@@ -1,37 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://v54vjd6k2mdb"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="BlueCatfishAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"river"])
-allow_day = false
-preferred_bait_tags = Array[StringName]([&"worm", &"minnow"])
-preferred_bait_weight_multiplier = 1.35
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"catfish_blue"
-display_name = "blue catfish"
-active = true
-catalog_number = 2402
-collection_group = &"Freshwater catfish"
-logbook_fact = "a deep fork in the tail inspired the blue catfish's scientific name. it rests deep by daylight and becomes more active after dark."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 1.25
-catch_profile = ExtResource("2_profile")
-availability = SubResource("BlueCatfishAvailability")
-weight_min_lb = 3.0
-weight_max_lb = 12.0
-display_scale_min = 0.95
-display_scale_max = 1.55
-display_scale_curve = 0.9
-sell_value_min = 6
-sell_value_max = 9
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/catfish_blue/fish_catfish_blue.png b/fish/species/catfish_blue/fish_catfish_blue.png
deleted file mode 100644
index 73511b6..0000000
Binary files a/fish/species/catfish_blue/fish_catfish_blue.png and /dev/null differ
diff --git a/fish/species/catfish_blue/fish_catfish_blue.png.import b/fish/species/catfish_blue/fish_catfish_blue.png.import
deleted file mode 100644
index 60d3606..0000000
--- a/fish/species/catfish_blue/fish_catfish_blue.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://7f1dtcou8moe"
-path="res://.godot/imported/fish_catfish_blue.png-03480cfe99c4fdece0a97273445594bd.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/catfish_blue/fish_catfish_blue.png"
-dest_files=["res://.godot/imported/fish_catfish_blue.png-03480cfe99c4fdece0a97273445594bd.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/catfish_channel/catfish_channel.tres b/fish/species/catfish_channel/catfish_channel.tres
deleted file mode 100644
index e72fdb0..0000000
--- a/fish/species/catfish_channel/catfish_channel.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://b7kpxcciokl4"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="ChannelCatfishAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"starter_pond", &"pond"])
-allow_day = false
-preferred_bait_tags = Array[StringName]([&"worm"])
-preferred_bait_weight_multiplier = 1.35
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"catfish_channel"
-display_name = "channel catfish"
-active = true
-catalog_number = 2404
-collection_group = &"Freshwater catfish"
-logbook_fact = "a channel catfish can taste through skin all over its body, especially near its gills and whiskers. the male watches the eggs."
-available_seasons = 7
-allowed_water_types = 1
-base_catch_weight = 2.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("ChannelCatfishAvailability")
-weight_min_lb = 1.5
-weight_max_lb = 7.0
-display_scale_min = 0.85
-display_scale_max = 1.35
-sell_value_min = 4
-sell_value_max = 7
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/catfish_channel/fish_catfish_channel.png b/fish/species/catfish_channel/fish_catfish_channel.png
deleted file mode 100644
index 566955a..0000000
Binary files a/fish/species/catfish_channel/fish_catfish_channel.png and /dev/null differ
diff --git a/fish/species/catfish_channel/fish_catfish_channel.png.import b/fish/species/catfish_channel/fish_catfish_channel.png.import
deleted file mode 100644
index 4a37a2d..0000000
--- a/fish/species/catfish_channel/fish_catfish_channel.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dvxpk2ltt3blb"
-path="res://.godot/imported/fish_catfish_channel.png-5078da9a00f276d273bfcd2b50b98937.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/catfish_channel/fish_catfish_channel.png"
-dest_files=["res://.godot/imported/fish_catfish_channel.png-5078da9a00f276d273bfcd2b50b98937.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/catfish_flathead/catfish_flathead.tres b/fish/species/catfish_flathead/catfish_flathead.tres
deleted file mode 100644
index de0e5f6..0000000
--- a/fish/species/catfish_flathead/catfish_flathead.tres
+++ /dev/null
@@ -1,36 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://c4r5ccnpw5nrq"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="FlatheadCatfishAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"river"])
-allow_day = false
-preferred_bait_tags = Array[StringName]([&"worm", &"minnow"])
-preferred_bait_weight_multiplier = 1.35
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"catfish_flathead"
-display_name = "flathead catfish"
-active = true
-catalog_number = 2405
-collection_group = &"Freshwater catfish"
-logbook_fact = "flatheads favor hideouts made by logs, rocks, ledges, and other underwater structure. as they grow, fish dominate their menu."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("FlatheadCatfishAvailability")
-weight_min_lb = 3.0
-weight_max_lb = 14.0
-display_scale_min = 1.0
-display_scale_max = 1.6
-display_scale_curve = 0.9
-sell_value_min = 7
-sell_value_max = 10
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/catfish_flathead/fish_catfish_flathead.png b/fish/species/catfish_flathead/fish_catfish_flathead.png
deleted file mode 100644
index 0122fda..0000000
Binary files a/fish/species/catfish_flathead/fish_catfish_flathead.png and /dev/null differ
diff --git a/fish/species/catfish_flathead/fish_catfish_flathead.png.import b/fish/species/catfish_flathead/fish_catfish_flathead.png.import
deleted file mode 100644
index 0d7eb41..0000000
--- a/fish/species/catfish_flathead/fish_catfish_flathead.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bacx63lp0yh7t"
-path="res://.godot/imported/fish_catfish_flathead.png-31b800b34c27c75b84a2ee98a13ff954.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/catfish_flathead/fish_catfish_flathead.png"
-dest_files=["res://.godot/imported/fish_catfish_flathead.png-31b800b34c27c75b84a2ee98a13ff954.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/catfish_walking/catfish_walking.tres b/fish/species/catfish_walking/catfish_walking.tres
deleted file mode 100644
index fe36cee..0000000
--- a/fish/species/catfish_walking/catfish_walking.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"catfish_walking"
-display_name = "walking catfish"
-active = true
-catalog_number = 7510
-collection_group = &"Tropical freshwater fish"
-logbook_fact = "walking catfish wriggle across wet ground using stiff pectoral spines for support. an air-breathing organ helps them survive the journey."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 5.0
-sell_value_min = 11
-sell_value_max = 24
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/catfish_white/catfish_white.tres b/fish/species/catfish_white/catfish_white.tres
deleted file mode 100644
index 86b799f..0000000
--- a/fish/species/catfish_white/catfish_white.tres
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://cr3kar4oioloh"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="WhiteCatfishAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"starter_pond", &"pond"])
-allow_day = false
-preferred_bait_tags = Array[StringName]([&"worm"])
-preferred_bait_weight_multiplier = 1.35
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"catfish_white"
-display_name = "white catfish"
-active = true
-catalog_number = 2407
-collection_group = &"Freshwater catfish"
-logbook_fact = "white catfish began along the atlantic coast between new york and florida. people later carried them far beyond that home range."
-available_seasons = 7
-allowed_water_types = 1
-base_catch_weight = 2.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("WhiteCatfishAvailability")
-weight_min_lb = 1.0
-weight_max_lb = 5.0
-display_scale_max = 1.25
-sell_value_min = 4
-sell_value_max = 6
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/catfish_white/fish_catfish_white.png b/fish/species/catfish_white/fish_catfish_white.png
deleted file mode 100644
index bdcb730..0000000
Binary files a/fish/species/catfish_white/fish_catfish_white.png and /dev/null differ
diff --git a/fish/species/catfish_white/fish_catfish_white.png.import b/fish/species/catfish_white/fish_catfish_white.png.import
deleted file mode 100644
index 1bdd1d6..0000000
--- a/fish/species/catfish_white/fish_catfish_white.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bf6kiawwekv7g"
-path="res://.godot/imported/fish_catfish_white.png-13b8ec9cfaabf9930cf00da49c81a3bd.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/catfish_white/fish_catfish_white.png"
-dest_files=["res://.godot/imported/fish_catfish_white.png-13b8ec9cfaabf9930cf00da49c81a3bd.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/char_arctic/char_arctic.tres b/fish/species/char_arctic/char_arctic.tres
deleted file mode 100644
index 17d08d1..0000000
--- a/fish/species/char_arctic/char_arctic.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-forbid_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"char_arctic"
-display_name = "arctic char"
-active = true
-catalog_number = 1101
-collection_group = &"Cold freshwater salmonids"
-logbook_fact = "arctic char thrive farther north than any other freshwater fish. some remain in lakes while others migrate between rivers and the sea."
-available_seasons = 13
-allowed_water_types = 3
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 25.0
-sell_value_min = 14
-sell_value_max = 32
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/chromis_blue_green/chromis_blue_green.tres b/fish/species/chromis_blue_green/chromis_blue_green.tres
deleted file mode 100644
index 65f35b4..0000000
--- a/fish/species/chromis_blue_green/chromis_blue_green.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/5901.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"chromis_blue_green"
-display_name = "blue-green chromis"
-active = true
-catalog_number = 5901
-collection_group = &"Reef schooling fish"
-logbook_fact = "blue-green chromis form loose clouds above branching coral. the school dives into narrow shelter whenever a predator approaches."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 2.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.01
-weight_max_lb = 0.25
-sell_value_min = 2
-sell_value_max = 4
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/chub_creek/chub_creek.tres b/fish/species/chub_creek/chub_creek.tres
deleted file mode 100644
index 1739712..0000000
--- a/fish/species/chub_creek/chub_creek.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"chub_creek"
-display_name = "creek chub"
-active = true
-catalog_number = 2802
-collection_group = &"Freshwater minnows"
-logbook_fact = "creek chubs gather around pools and undercut banks in small streams. males build gravel ridges where several fish may spawn."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 3.2
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 0.8
-sell_value_min = 2
-sell_value_max = 4
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/chub_european/chub_european.tres b/fish/species/chub_european/chub_european.tres
deleted file mode 100644
index a218aec..0000000
--- a/fish/species/chub_european/chub_european.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://cmyhlwqrx36wg"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="ChubEuropeanAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"river"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"chub_european"
-display_name = "european chub"
-active = true
-catalog_number = 2505
-collection_group = &"Freshwater coarse fish"
-logbook_fact = "european chub are adaptable river fish. adults often patrol shallows and feed on insects, small fish, and fruit."
-available_seasons = 15
-allowed_water_types = 1
-base_catch_weight = 0.55
-catch_profile = ExtResource("2_profile")
-availability = SubResource("ChubEuropeanAvailability")
-weight_min_lb = 0.2
-weight_max_lb = 6.0
-display_scale_min = 0.85
-display_scale_curve = 0.9
-sell_value_min = 2
-sell_value_max = 4
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/chub_european/fish_chub_european.png b/fish/species/chub_european/fish_chub_european.png
deleted file mode 100644
index e850ada..0000000
Binary files a/fish/species/chub_european/fish_chub_european.png and /dev/null differ
diff --git a/fish/species/chub_european/fish_chub_european.png.import b/fish/species/chub_european/fish_chub_european.png.import
deleted file mode 100644
index 0c241b0..0000000
--- a/fish/species/chub_european/fish_chub_european.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c04wtbyslmpll"
-path="res://.godot/imported/fish_chub_european.png-da2784298a62b1d90038dacb4cf25e35.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/chub_european/fish_chub_european.png"
-dest_files=["res://.godot/imported/fish_chub_european.png-da2784298a62b1d90038dacb4cf25e35.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/chub_flame/chub_flame.tres b/fish/species/chub_flame/chub_flame.tres
deleted file mode 100644
index 2bf815c..0000000
--- a/fish/species/chub_flame/chub_flame.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://d25q23kfdhm3o"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="ChubFlameAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"river"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"chub_flame"
-display_name = "flame chub"
-active = true
-catalog_number = 2805
-collection_group = &"Freshwater minnows"
-logbook_fact = "flame chubs prefer cool, clear streams. their bright breeding colors make a small fish easy to spot in spring water."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.4
-catch_profile = ExtResource("2_profile")
-availability = SubResource("ChubFlameAvailability")
-weight_min_lb = 0.05
-weight_max_lb = 0.15
-display_scale_min = 0.85
-display_scale_max = 1.1
-display_scale_curve = 0.9
-sell_value_min = 3
-sell_value_max = 6
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/chub_flame/fish_chub_flame.png b/fish/species/chub_flame/fish_chub_flame.png
deleted file mode 100644
index a94fc71..0000000
Binary files a/fish/species/chub_flame/fish_chub_flame.png and /dev/null differ
diff --git a/fish/species/chub_flame/fish_chub_flame.png.import b/fish/species/chub_flame/fish_chub_flame.png.import
deleted file mode 100644
index 8cad0d9..0000000
--- a/fish/species/chub_flame/fish_chub_flame.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bla6pc21imxsi"
-path="res://.godot/imported/fish_chub_flame.png-9c7780910f16d333d5109ee4e80aa8fb.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/chub_flame/fish_chub_flame.png"
-dest_files=["res://.godot/imported/fish_chub_flame.png-9c7780910f16d333d5109ee4e80aa8fb.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/chub_lake/chub_lake.tres b/fish/species/chub_lake/chub_lake.tres
deleted file mode 100644
index 6da6b43..0000000
--- a/fish/species/chub_lake/chub_lake.tres
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://n7u2krjf4ril"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="ChubLakeAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"lake"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"chub_lake"
-display_name = "lake chub"
-active = true
-catalog_number = 2807
-collection_group = &"Freshwater minnows"
-logbook_fact = "lake chubs school in cold northern water and use their small mouths to pick insects and other tiny prey from the current."
-available_seasons = 13
-allowed_water_types = 1
-base_catch_weight = 0.55
-catch_profile = ExtResource("2_profile")
-availability = SubResource("ChubLakeAvailability")
-weight_min_lb = 0.05
-weight_max_lb = 0.5
-display_scale_min = 0.85
-display_scale_max = 1.0
-display_scale_curve = 0.9
-sell_value_min = 2
-sell_value_max = 4
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/chub_lake/fish_chub_lake.png b/fish/species/chub_lake/fish_chub_lake.png
deleted file mode 100644
index 6aac214..0000000
Binary files a/fish/species/chub_lake/fish_chub_lake.png and /dev/null differ
diff --git a/fish/species/chub_lake/fish_chub_lake.png.import b/fish/species/chub_lake/fish_chub_lake.png.import
deleted file mode 100644
index 1be73f0..0000000
--- a/fish/species/chub_lake/fish_chub_lake.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://w8cjrpnv350h"
-path="res://.godot/imported/fish_chub_lake.png-cbb7f27fe4a21820691bbb9c3b9bd3e9.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/chub_lake/fish_chub_lake.png"
-dest_files=["res://.godot/imported/fish_chub_lake.png-cbb7f27fe4a21820691bbb9c3b9bd3e9.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/cisco/cisco.tres b/fish/species/cisco/cisco.tres
deleted file mode 100644
index 1bc53b0..0000000
--- a/fish/species/cisco/cisco.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"cisco"
-display_name = "cisco"
-active = true
-catalog_number = 1002
-collection_group = &"Cold freshwater fish"
-logbook_fact = "cisco school in the cool open water of northern lakes. these slim relatives of salmon feed on plankton and become prey for larger fish."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 2.2
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 3.0
-sell_value_min = 3
-sell_value_max = 7
-sell_value_curve = 0.95
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/clam_geoduck/clam_geoduck.tres b/fish/species/clam_geoduck/clam_geoduck.tres
deleted file mode 100644
index 492113c..0000000
--- a/fish/species/clam_geoduck/clam_geoduck.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"clam_geoduck"
-display_name = "Pacific geoduck"
-active = true
-catalog_number = 6403
-collection_group = &"Shellfish"
-logbook_fact = "pacific geoducks bury their shells deep while a long siphon reaches the seafloor. these clams can survive for well over a century."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.25
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 15.0
-sell_value_min = 14
-sell_value_max = 34
-sell_value_curve = 0.88
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/clam_giant/clam_giant.tres b/fish/species/clam_giant/clam_giant.tres
deleted file mode 100644
index 2b4f59d..0000000
--- a/fish/species/clam_giant/clam_giant.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"clam_giant"
-display_name = "giant clam"
-active = true
-catalog_number = 6402
-collection_group = &"Shellfish"
-logbook_fact = "giant clams host photosynthetic algae inside brightly colored mantles. heavy shells remain anchored to one reef spot throughout adulthood."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 5.0
-weight_max_lb = 500.0
-sell_value_min = 28
-sell_value_max = 72
-sell_value_curve = 0.8
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/clam_manila/clam_manila.png b/fish/species/clam_manila/clam_manila.png
deleted file mode 100644
index 72f5bfb..0000000
Binary files a/fish/species/clam_manila/clam_manila.png and /dev/null differ
diff --git a/fish/species/clam_manila/clam_manila.tres b/fish/species/clam_manila/clam_manila.tres
deleted file mode 100644
index 20e3f8a..0000000
--- a/fish/species/clam_manila/clam_manila.tres
+++ /dev/null
@@ -1,28 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=4 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Texture2D" path="res://fish/species/clam_manila/clam_manila.png" id="3_texture"]
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"clam_manila"
-display_name = "Manila clam"
-active = true
-catalog_number = 6406
-collection_group = &"Shellfish"
-logbook_fact = "manila clams bury themselves in wet coastal sand and reveal their hiding places with small spurts of water."
-collection_method = 2
-logbook_section = 1
-habitat_label = "wet beach sand"
-available_seasons = 15
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 1.0
-catch_profile = ExtResource("2_profile")
-weight_min_lb = 0.05
-weight_max_lb = 0.3
-sell_value_min = 3
-sell_value_max = 8
-sell_value_curve = 1.0
-display_texture = ExtResource("3_texture")
diff --git a/fish/species/clownfish_ocellaris/clownfish_ocellaris.tres b/fish/species/clownfish_ocellaris/clownfish_ocellaris.tres
deleted file mode 100644
index 16566cb..0000000
--- a/fish/species/clownfish_ocellaris/clownfish_ocellaris.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/5702.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"clownfish_ocellaris"
-display_name = "ocellaris clownfish"
-active = true
-catalog_number = 5702
-collection_group = &"Reef ornamentals"
-logbook_fact = "ocellaris clownfish shelter safely among stinging anemone tentacles. each group is led by a female that develops from the largest male."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 1.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.01
-weight_max_lb = 0.15
-sell_value_min = 5
-sell_value_max = 10
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/cobia/cobia.tres b/fish/species/cobia/cobia.tres
deleted file mode 100644
index 51dec82..0000000
--- a/fish/species/cobia/cobia.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"cobia"
-display_name = "cobia"
-active = true
-catalog_number = 3602
-collection_group = &"Large coastal predators"
-logbook_fact = "cobia often follow sharks, rays, and floating structures in warm seas. a broad dark stripe runs along their long, powerful bodies."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.24
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 5.0
-weight_max_lb = 80.0
-sell_value_min = 15
-sell_value_max = 40
-sell_value_curve = 0.82
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/cod_atlantic/cod_atlantic.tres b/fish/species/cod_atlantic/cod_atlantic.tres
deleted file mode 100644
index 640ccaa..0000000
--- a/fish/species/cod_atlantic/cod_atlantic.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"cod_atlantic"
-display_name = "atlantic cod"
-active = true
-catalog_number = 1402
-collection_group = &"Cold shelf fish"
-logbook_fact = "atlantic cod carry a pale lateral line and one chin barbel. they search cold seafloors for crustaceans, mollusks, and smaller fish."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 1.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 55.0
-sell_value_min = 5
-sell_value_max = 18
-sell_value_curve = 0.85
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/cod_pacific/cod_pacific.tres b/fish/species/cod_pacific/cod_pacific.tres
deleted file mode 100644
index 729acc1..0000000
--- a/fish/species/cod_pacific/cod_pacific.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"cod_pacific"
-display_name = "pacific cod"
-active = true
-catalog_number = 1406
-collection_group = &"Cold shelf fish"
-logbook_fact = "pacific cod use a chin barbel to investigate cold shelf bottoms. seasonal schools follow prey between deeper water and coastal spawning grounds."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 1.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 45.0
-sell_value_min = 5
-sell_value_max = 17
-sell_value_curve = 0.85
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/coelacanth_west_indian/coelacanth_west_indian.tres b/fish/species/coelacanth_west_indian/coelacanth_west_indian.tres
deleted file mode 100644
index 8b7bf18..0000000
--- a/fish/species/coelacanth_west_indian/coelacanth_west_indian.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"coelacanth_west_indian"
-display_name = "West Indian Ocean coelacanth"
-active = true
-catalog_number = 1702
-collection_group = &"Deep-sea legends"
-logbook_fact = "west indian ocean coelacanths shelter in deep volcanic caves by day. fleshy lobed fins move in an alternating, limb-like rhythm."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.03
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 20.0
-weight_max_lb = 200.0
-sell_value_min = 65
-sell_value_max = 145
-sell_value_curve = 0.8
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/conch_queen/conch_queen.tres b/fish/species/conch_queen/conch_queen.tres
deleted file mode 100644
index 045012b..0000000
--- a/fish/species/conch_queen/conch_queen.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"conch_queen"
-display_name = "queen conch"
-active = true
-catalog_number = 6404
-collection_group = &"Shellfish"
-logbook_fact = "queen conchs graze algae with a long flexible snout. adults develop a broad flared shell lip and move by vaulting on one muscular foot."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.24
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 5.0
-sell_value_min = 15
-sell_value_max = 36
-sell_value_curve = 0.91
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/cornetfish_red/cornetfish_red.tres b/fish/species/cornetfish_red/cornetfish_red.tres
deleted file mode 100644
index ea750bd..0000000
--- a/fish/species/cornetfish_red/cornetfish_red.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"cornetfish_red"
-display_name = "red cornetfish"
-active = true
-catalog_number = 5606
-collection_group = &"Reef oddities"
-logbook_fact = "red cornetfish stalk reefs with extremely long bodies and tubular snouts. a trailing filament extends from the center of the tail."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.11
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 4.0
-sell_value_min = 24
-sell_value_max = 55
-sell_value_curve = 0.92
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/cowfish_longhorn/cowfish_longhorn.tres b/fish/species/cowfish_longhorn/cowfish_longhorn.tres
deleted file mode 100644
index 2a848a7..0000000
--- a/fish/species/cowfish_longhorn/cowfish_longhorn.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"cowfish_longhorn"
-display_name = "longhorn cowfish"
-active = true
-catalog_number = 5604
-collection_group = &"Reef oddities"
-logbook_fact = "longhorn cowfish wear a rigid box of fused scales with horns above the eyes. small fins steer the angular body with surprising precision."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.13
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 2.0
-sell_value_min = 23
-sell_value_max = 52
-sell_value_curve = 0.94
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/crab_blue/crab_blue.tres b/fish/species/crab_blue/crab_blue.tres
deleted file mode 100644
index 2ee3279..0000000
--- a/fish/species/crab_blue/crab_blue.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=4 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"crab_blue"
-display_name = "blue crab"
-active = false
-catalog_number = 3902
-collection_group = &"Marine crabs"
-logbook_fact = "blue crabs swim with flattened rear legs shaped like paddles. bright blue claws and an olive shell conceal a quick, defensive temperament."
-collection_method = 1
-logbook_section = 1
-habitat_label = "shallow coastal water"
-available_seasons = 6
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 1.15
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 1.1
-sell_value_min = 3
-sell_value_max = 8
-sell_value_curve = 0.95
diff --git a/fish/species/crab_brown/crab_brown.png b/fish/species/crab_brown/crab_brown.png
deleted file mode 100644
index a06f039..0000000
Binary files a/fish/species/crab_brown/crab_brown.png and /dev/null differ
diff --git a/fish/species/crab_brown/crab_brown.png.import b/fish/species/crab_brown/crab_brown.png.import
deleted file mode 100644
index 9faf442..0000000
--- a/fish/species/crab_brown/crab_brown.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dmn3rafvxtnb1"
-path="res://.godot/imported/crab_brown.png-4241f8408bf426d406e45f4cce16bbc8.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/crab_brown/crab_brown.png"
-dest_files=["res://.godot/imported/crab_brown.png-4241f8408bf426d406e45f4cce16bbc8.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/crab_brown/crab_brown.tres b/fish/species/crab_brown/crab_brown.tres
deleted file mode 100644
index 7dec427..0000000
--- a/fish/species/crab_brown/crab_brown.tres
+++ /dev/null
@@ -1,28 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=4 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/3906.png" id="3_texture"]
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"crab_brown"
-display_name = "brown crab"
-active = true
-catalog_number = 3906
-collection_group = &"Marine crabs"
-logbook_fact = "brown crabs hide among rocks and sand along sheltered coasts. broad claws help them feed on shellfish and other small shore animals."
-collection_method = 1
-logbook_section = 1
-habitat_label = "dry beach sand"
-available_seasons = 15
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 1.0
-catch_profile = ExtResource("2_profile")
-weight_min_lb = 0.5
-weight_max_lb = 3.0
-sell_value_min = 5
-sell_value_max = 15
-sell_value_curve = 1.0
-display_texture = ExtResource("3_texture")
diff --git a/fish/species/crab_dungeness/crab_dungeness.tres b/fish/species/crab_dungeness/crab_dungeness.tres
deleted file mode 100644
index bb56819..0000000
--- a/fish/species/crab_dungeness/crab_dungeness.tres
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=4 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"crab_dungeness"
-display_name = "Dungeness crab"
-active = false
-catalog_number = 3903
-collection_group = &"Marine crabs"
-logbook_fact = "dungeness crabs bury beneath sand with only their eyes and antennae exposed. broad toothed shells protect them along cool pacific coasts."
-collection_method = 1
-logbook_section = 1
-habitat_label = "shallow coastal sand"
-available_seasons = 13
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.65
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 3.0
-sell_value_min = 6
-sell_value_max = 14
-sell_value_curve = 0.92
diff --git a/fish/species/crab_ghost/crab_ghost.tres b/fish/species/crab_ghost/crab_ghost.tres
deleted file mode 100644
index db54293..0000000
--- a/fish/species/crab_ghost/crab_ghost.tres
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=4 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"crab_ghost"
-display_name = "Atlantic ghost crab"
-active = false
-catalog_number = 3901
-collection_group = &"Marine crabs"
-logbook_fact = "atlantic ghost crabs dash across beaches on long pale legs and retreat into deep burrows. raised eyes scan nearly every direction."
-collection_method = 1
-logbook_section = 1
-habitat_label = "dry beach sand"
-available_seasons = 6
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.28
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.02
-weight_max_lb = 0.5
-sell_value_min = 11
-sell_value_max = 24
-sell_value_curve = 0.97
diff --git a/fish/species/crab_japanese_spider/crab_japanese_spider.tres b/fish/species/crab_japanese_spider/crab_japanese_spider.tres
deleted file mode 100644
index 33ffbff..0000000
--- a/fish/species/crab_japanese_spider/crab_japanese_spider.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"crab_japanese_spider"
-display_name = "Japanese spider crab"
-active = true
-catalog_number = 3904
-collection_group = &"Marine crabs"
-logbook_fact = "japanese spider crabs carry the longest leg span of any living arthropod. adults roam deep seafloors around japan."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.045
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 5.0
-weight_max_lb = 44.0
-sell_value_min = 38
-sell_value_max = 88
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/crab_red_king/crab_red_king.tres b/fish/species/crab_red_king/crab_red_king.tres
deleted file mode 100644
index c82579a..0000000
--- a/fish/species/crab_red_king/crab_red_king.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"crab_red_king"
-display_name = "red king crab"
-active = true
-catalog_number = 3905
-collection_group = &"Marine crabs"
-logbook_fact = "red king crabs stride over cold northern bottoms on long armored legs. despite the name, they are closer to hermit crabs than true crabs."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.11
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 2.0
-weight_max_lb = 24.0
-sell_value_min = 24
-sell_value_max = 58
-sell_value_curve = 0.85
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/crappie_black/crappie_black.tres b/fish/species/crappie_black/crappie_black.tres
deleted file mode 100644
index 0359a2c..0000000
--- a/fish/species/crappie_black/crappie_black.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"crappie_black"
-display_name = "black crappie"
-active = true
-catalog_number = 3101
-collection_group = &"Freshwater panfish"
-logbook_fact = "black crappie gather around submerged wood and vegetation in clear water. irregular dark speckles scatter across their deep silver bodies."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 1.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 4.0
-sell_value_min = 5
-sell_value_max = 11
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/crappie_white/crappie_white.tres b/fish/species/crappie_white/crappie_white.tres
deleted file mode 100644
index 7806c37..0000000
--- a/fish/species/crappie_white/crappie_white.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"crappie_white"
-display_name = "white crappie"
-active = true
-catalog_number = 3107
-collection_group = &"Freshwater panfish"
-logbook_fact = "white crappie favor warm, cloudy water and gather around structure. vertical bars cross their silver sides instead of scattered speckles."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 1.7
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 4.5
-sell_value_min = 4
-sell_value_max = 9
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/crayfish_red_swamp/crayfish_red_swamp.tres b/fish/species/crayfish_red_swamp/crayfish_red_swamp.tres
deleted file mode 100644
index b53448d..0000000
--- a/fish/species/crayfish_red_swamp/crayfish_red_swamp.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"crayfish_red_swamp"
-display_name = "red swamp crayfish"
-active = true
-catalog_number = 2601
-collection_group = &"Freshwater crustaceans"
-logbook_fact = "red swamp crayfish burrow into wet ground when water recedes. adaptable populations feed on plants, carrion, and small aquatic animals."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 1.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.03
-weight_max_lb = 0.25
-sell_value_min = 3
-sell_value_max = 7
-sell_value_curve = 0.98
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/croaker_atlantic/croaker_atlantic.tres b/fish/species/croaker_atlantic/croaker_atlantic.tres
deleted file mode 100644
index c35e7de..0000000
--- a/fish/species/croaker_atlantic/croaker_atlantic.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"croaker_atlantic"
-display_name = "atlantic croaker"
-active = true
-catalog_number = 6801
-collection_group = &"Temperate estuary"
-logbook_fact = "atlantic croakers vibrate muscles against the swim bladder to make a croaking sound. chin barbels locate worms in sandy bays."
-available_seasons = 7
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 2.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 5.0
-sell_value_min = 3
-sell_value_max = 8
-sell_value_curve = 0.92
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/croaker_yellowfin/croaker_yellowfin.tres b/fish/species/croaker_yellowfin/croaker_yellowfin.tres
deleted file mode 100644
index 84885f9..0000000
--- a/fish/species/croaker_yellowfin/croaker_yellowfin.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"croaker_yellowfin"
-display_name = "yellowfin croaker"
-active = true
-catalog_number = 6807
-collection_group = &"Temperate estuary"
-logbook_fact = "yellowfin croakers search sandy surf zones with a small chin barbel. internal muscles produce croaks and drumming calls."
-available_seasons = 7
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 2.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 4.0
-sell_value_min = 3
-sell_value_max = 8
-sell_value_curve = 0.92
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/cusk/cusk.tres b/fish/species/cusk/cusk.tres
deleted file mode 100644
index b223ba1..0000000
--- a/fish/species/cusk/cusk.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"cusk"
-display_name = "cusk"
-active = true
-catalog_number = 1202
-collection_group = &"Cold reef fish"
-logbook_fact = "cusk move slowly across cold rocky bottoms, sheltering in cracks and caves. a single long fin runs around much of the rear body."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.75
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 30.0
-sell_value_min = 7
-sell_value_max = 19
-sell_value_curve = 0.85
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/cuttlefish_common/cuttlefish_common.tres b/fish/species/cuttlefish_common/cuttlefish_common.tres
deleted file mode 100644
index c1ef5ec..0000000
--- a/fish/species/cuttlefish_common/cuttlefish_common.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"cuttlefish_common"
-display_name = "common cuttlefish"
-active = true
-catalog_number = 1502
-collection_group = &"Cuttlefish and nautiluses"
-logbook_fact = "common cuttlefish flash changing colors and patterns through specialized skin cells. an internal cuttlebone controls buoyancy."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.55
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 12.0
-sell_value_min = 7
-sell_value_max = 17
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/cuttlefish_flamboyant/cuttlefish_flamboyant.tres b/fish/species/cuttlefish_flamboyant/cuttlefish_flamboyant.tres
deleted file mode 100644
index 496ade9..0000000
--- a/fish/species/cuttlefish_flamboyant/cuttlefish_flamboyant.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"cuttlefish_flamboyant"
-display_name = "flamboyant cuttlefish"
-active = true
-catalog_number = 1503
-collection_group = &"Cuttlefish and nautiluses"
-logbook_fact = "flamboyant cuttlefish walk along the seafloor on their arms and fins. vivid displays warn predators of potent toxins."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 0.4
-sell_value_min = 26
-sell_value_max = 58
-sell_value_curve = 0.96
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/damselfish_sergeant_major/damselfish_sergeant_major.tres b/fish/species/damselfish_sergeant_major/damselfish_sergeant_major.tres
deleted file mode 100644
index 483565c..0000000
--- a/fish/species/damselfish_sergeant_major/damselfish_sergeant_major.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"damselfish_sergeant_major"
-display_name = "sergeant major"
-active = true
-catalog_number = 5904
-collection_group = &"Reef schooling fish"
-logbook_fact = "sergeant majors guard purple egg patches on hard reef surfaces. five dark bars cross their yellow and silver bodies."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 2.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.02
-weight_max_lb = 0.5
-sell_value_min = 2
-sell_value_max = 5
-sell_value_curve = 0.98
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/danio_zebra/danio_zebra.tres b/fish/species/danio_zebra/danio_zebra.tres
deleted file mode 100644
index 0f73935..0000000
--- a/fish/species/danio_zebra/danio_zebra.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"danio_zebra"
-display_name = "zebrafish"
-active = true
-catalog_number = 2103
-collection_group = &"Freshwater aquarium fish"
-logbook_fact = "zebrafish carry horizontal blue stripes from gill to tail. hardy schools thrive in shallow streams, flooded fields, and quiet pools."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 1.35
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.01
-weight_max_lb = 0.12
-sell_value_min = 2
-sell_value_max = 5
-sell_value_curve = 0.98
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/discus/discus.tres b/fish/species/discus/discus.tres
deleted file mode 100644
index 9803f77..0000000
--- a/fish/species/discus/discus.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"discus"
-display_name = "discus"
-active = true
-catalog_number = 7503
-collection_group = &"Tropical freshwater fish"
-logbook_fact = "discus move through quiet amazon backwaters with round, flattened bodies. parents feed newly hatched young with nutrient-rich skin mucus."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 3
-base_catch_weight = 0.12
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 1.0
-sell_value_min = 24
-sell_value_max = 52
-sell_value_curve = 0.92
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/dolphin_bottlenose/dolphin_bottlenose.tres b/fish/species/dolphin_bottlenose/dolphin_bottlenose.tres
deleted file mode 100644
index 2b410ea..0000000
--- a/fish/species/dolphin_bottlenose/dolphin_bottlenose.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"dolphin_bottlenose"
-display_name = "common bottlenose dolphin"
-active = true
-catalog_number = 7702
-collection_group = &"Whales and dolphins"
-logbook_fact = "common bottlenose dolphins coordinate with whistles, clicks, and body signals. echolocation reveals prey hidden in dark or cloudy water."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.012
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 330.0
-weight_max_lb = 1400.0
-sell_value_min = 75
-sell_value_max = 180
-sell_value_curve = 0.76
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/dorado_golden/dorado_golden.tres b/fish/species/dorado_golden/dorado_golden.tres
deleted file mode 100644
index e22f7c9..0000000
--- a/fish/species/dorado_golden/dorado_golden.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"dorado_golden"
-display_name = "golden dorado"
-active = true
-catalog_number = 7602
-collection_group = &"Tropical freshwater predators"
-logbook_fact = "golden dorado chase fish through powerful south american rivers. muscular tails, heavy jaws, and golden scales suit life in fast current."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 5.0
-weight_max_lb = 55.0
-sell_value_min = 29
-sell_value_max = 66
-sell_value_curve = 0.82
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/dugong/dugong.tres b/fish/species/dugong/dugong.tres
deleted file mode 100644
index 0c81d2e..0000000
--- a/fish/species/dugong/dugong.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"dugong"
-display_name = "dugong"
-active = true
-catalog_number = 501
-collection_group = &"Coastal marine mammals"
-logbook_fact = "dugongs graze seagrass meadows with a downturned, bristled snout. they are the only fully marine members of the sirenian family."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.011
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 500.0
-weight_max_lb = 1100.0
-sell_value_min = 72
-sell_value_max = 165
-sell_value_curve = 0.77
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/eel_american/eel_american.tres b/fish/species/eel_american/eel_american.tres
deleted file mode 100644
index a433afa..0000000
--- a/fish/species/eel_american/eel_american.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"eel_american"
-display_name = "american eel"
-active = true
-catalog_number = 4001
-collection_group = &"Migratory eels"
-logbook_fact = "american eels hatch far out in the sargasso sea before entering freshwater rivers. adults later reverse the journey to spawn once."
-available_seasons = 12
-allowed_water_types = 3
-rarity = 2
-base_catch_weight = 0.25
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 8.0
-sell_value_min = 12
-sell_value_max = 27
-sell_value_curve = 0.88
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/eel_european/eel_european.tres b/fish/species/eel_european/eel_european.tres
deleted file mode 100644
index 7683d92..0000000
--- a/fish/species/eel_european/eel_european.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"eel_european"
-display_name = "european eel"
-active = true
-catalog_number = 4002
-collection_group = &"Migratory eels"
-logbook_fact = "european eels cross the atlantic as transparent larvae before entering rivers and lakes. mature silver eels return to sea to reproduce."
-available_seasons = 12
-allowed_water_types = 3
-rarity = 2
-base_catch_weight = 0.2
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 10.0
-sell_value_min = 13
-sell_value_max = 30
-sell_value_curve = 0.88
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/electric_eel/electric_eel.tres b/fish/species/electric_eel/electric_eel.tres
deleted file mode 100644
index ef5b6df..0000000
--- a/fish/species/electric_eel/electric_eel.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"electric_eel"
-display_name = "electric eel"
-active = true
-catalog_number = 2901
-collection_group = &"Freshwater oddities"
-logbook_fact = "electric eels navigate and hunt with electrical pulses produced along most of the body. they must surface regularly to breathe air."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 44.0
-sell_value_min = 25
-sell_value_max = 60
-sell_value_curve = 0.82
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/fallfish/fallfish.tres b/fish/species/fallfish/fallfish.tres
deleted file mode 100644
index 53ef7b6..0000000
--- a/fish/species/fallfish/fallfish.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"fallfish"
-display_name = "fallfish"
-active = true
-catalog_number = 2506
-collection_group = &"Freshwater coarse fish"
-logbook_fact = "fallfish are large minnows that build broad stone nests in flowing water. other stream fish often reuse the gravel mounds for spawning."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.9
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 3.5
-sell_value_min = 5
-sell_value_max = 10
-sell_value_curve = 0.92
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/fangtooth_common/fangtooth_common.tres b/fish/species/fangtooth_common/fangtooth_common.tres
deleted file mode 100644
index 1004279..0000000
--- a/fish/species/fangtooth_common/fangtooth_common.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"fangtooth_common"
-display_name = "common fangtooth"
-active = true
-catalog_number = 1802
-collection_group = &"Deep-sea oddities"
-logbook_fact = "common fangtooths carry outsized teeth that fit into pockets in the roof of the mouth. they migrate upward from deep water after dark."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 0.5
-sell_value_min = 25
-sell_value_max = 56
-sell_value_curve = 0.96
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/flounder_peacock/flounder_peacock.tres b/fish/species/flounder_peacock/flounder_peacock.tres
deleted file mode 100644
index 3d096df..0000000
--- a/fish/species/flounder_peacock/flounder_peacock.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"flounder_peacock"
-display_name = "peacock flounder"
-active = true
-catalog_number = 5401
-collection_group = &"Reef flatfish"
-logbook_fact = "peacock flounders change their spots and colors to match tropical sand. both eyes scan above while the flat body remains concealed."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 5.0
-sell_value_min = 11
-sell_value_max = 24
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/flounder_summer/flounder_summer.tres b/fish/species/flounder_summer/flounder_summer.tres
deleted file mode 100644
index 680738d..0000000
--- a/fish/species/flounder_summer/flounder_summer.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"flounder_summer"
-display_name = "summer flounder"
-active = true
-catalog_number = 402
-collection_group = &"Coastal flatfish"
-logbook_fact = "summer flounder bury themselves in sand with only their eyes exposed. a sudden burst carries them upward to seize passing prey."
-available_seasons = 7
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.9
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 15.0
-sell_value_min = 6
-sell_value_max = 16
-sell_value_curve = 0.88
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/flounder_winter/flounder_winter.tres b/fish/species/flounder_winter/flounder_winter.tres
deleted file mode 100644
index c3ae09b..0000000
--- a/fish/species/flounder_winter/flounder_winter.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/403.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"flounder_winter"
-display_name = "winter flounder"
-active = true
-catalog_number = 403
-collection_group = &"Coastal flatfish"
-logbook_fact = "winter flounder favor cool bays and estuaries where they rest on muddy bottoms. their eyes sit on the right side of the body."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 1.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 8.0
-sell_value_min = 4
-sell_value_max = 9
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/flyingfish_tropical/flyingfish_tropical.tres b/fish/species/flyingfish_tropical/flyingfish_tropical.tres
deleted file mode 100644
index 12fd4bb..0000000
--- a/fish/species/flyingfish_tropical/flyingfish_tropical.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"flyingfish_tropical"
-display_name = "tropical flyingfish"
-active = true
-catalog_number = 6603
-collection_group = &"Surface ocean fish"
-logbook_fact = "tropical flyingfish accelerate underwater before spreading enlarged fins above the surface. glides help them escape pursuing predators."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.35
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 1.5
-sell_value_min = 10
-sell_value_max = 22
-sell_value_curve = 0.94
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/foureyes_largescale/foureyes_largescale.tres b/fish/species/foureyes_largescale/foureyes_largescale.tres
deleted file mode 100644
index 7ebba2f..0000000
--- a/fish/species/foureyes_largescale/foureyes_largescale.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"foureyes_largescale"
-display_name = "largescale foureyes"
-active = true
-catalog_number = 202
-collection_group = &"Brackish oddities"
-logbook_fact = "largescale foureyes cruise with each eye divided across the waterline. one half watches above while the other scans below."
-available_seasons = 15
-allowed_water_types = 3
-rarity = 2
-base_catch_weight = 0.32
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 1.0
-sell_value_min = 10
-sell_value_max = 23
-sell_value_curve = 0.95
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/freshwater_drum/freshwater_drum.tres b/fish/species/freshwater_drum/freshwater_drum.tres
deleted file mode 100644
index f52f9fe..0000000
--- a/fish/species/freshwater_drum/freshwater_drum.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"freshwater_drum"
-display_name = "freshwater drum"
-active = true
-catalog_number = 2902
-collection_group = &"Freshwater oddities"
-logbook_fact = "freshwater drum crush snails and mussels with powerful throat teeth. muscles vibrating against the swim bladder produce a low drumming sound."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.7
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 35.0
-sell_value_min = 7
-sell_value_max = 20
-sell_value_curve = 0.85
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/gar_alligator/gar_alligator.tres b/fish/species/gar_alligator/gar_alligator.tres
deleted file mode 100644
index f5439a4..0000000
--- a/fish/species/gar_alligator/gar_alligator.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"gar_alligator"
-display_name = "alligator gar"
-active = true
-catalog_number = 2701
-collection_group = &"Freshwater giants"
-logbook_fact = "alligator gar combine a broad tooth-filled snout with heavy armor-like scales. a vascular swim bladder lets them breathe air at the surface."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 4
-base_catch_weight = 0.055
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 20.0
-weight_max_lb = 300.0
-sell_value_min = 55
-sell_value_max = 125
-sell_value_curve = 0.8
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/gar_longnose/gar_longnose.png b/fish/species/gar_longnose/gar_longnose.png
deleted file mode 100644
index 2cfa552..0000000
Binary files a/fish/species/gar_longnose/gar_longnose.png and /dev/null differ
diff --git a/fish/species/gar_longnose/gar_longnose.png.import b/fish/species/gar_longnose/gar_longnose.png.import
deleted file mode 100644
index 385d22c..0000000
--- a/fish/species/gar_longnose/gar_longnose.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://brb46wv5v6htb"
-path="res://.godot/imported/gar_longnose.png-91bf63255a3f7c479162d07d1e71edf8.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/gar_longnose/gar_longnose.png"
-dest_files=["res://.godot/imported/gar_longnose.png-91bf63255a3f7c479162d07d1e71edf8.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/gar_longnose/gar_longnose.tres b/fish/species/gar_longnose/gar_longnose.tres
deleted file mode 100644
index aaabbfe..0000000
--- a/fish/species/gar_longnose/gar_longnose.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/103.png" id="4_texture"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"gar_longnose"
-display_name = "longnose gar"
-active = true
-catalog_number = 103
-collection_group = &"Ancient freshwater fish"
-logbook_fact = "longnose gar wait near the surface before snapping sideways at passing fish. their narrow jaws carry rows of fine needle-like teeth."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.24
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 2.0
-weight_max_lb = 35.0
-sell_value_min = 14
-sell_value_max = 34
-sell_value_curve = 0.85
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/gar_spotted/gar_spotted.png b/fish/species/gar_spotted/gar_spotted.png
deleted file mode 100644
index 80cdfdf..0000000
Binary files a/fish/species/gar_spotted/gar_spotted.png and /dev/null differ
diff --git a/fish/species/gar_spotted/gar_spotted.png.import b/fish/species/gar_spotted/gar_spotted.png.import
deleted file mode 100644
index 95d737f..0000000
--- a/fish/species/gar_spotted/gar_spotted.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b81tl0mmq7gxx"
-path="res://.godot/imported/gar_spotted.png-0361bdcc323bb7449e240ef3835b7e18.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/gar_spotted/gar_spotted.png"
-dest_files=["res://.godot/imported/gar_spotted.png-0361bdcc323bb7449e240ef3835b7e18.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/gar_spotted/gar_spotted.tres b/fish/species/gar_spotted/gar_spotted.tres
deleted file mode 100644
index 3405ee9..0000000
--- a/fish/species/gar_spotted/gar_spotted.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/106.png" id="4_texture"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"gar_spotted"
-display_name = "spotted gar"
-active = true
-catalog_number = 106
-collection_group = &"Ancient freshwater fish"
-logbook_fact = "spotted gar linger among vegetation where their mottled bodies break up their outline. they rise to gulp air as well as breathing through gills."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.2
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 15.0
-sell_value_min = 13
-sell_value_max = 29
-sell_value_curve = 0.88
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/goby_round/fish_goby_round.png b/fish/species/goby_round/fish_goby_round.png
deleted file mode 100644
index 02a5855..0000000
Binary files a/fish/species/goby_round/fish_goby_round.png and /dev/null differ
diff --git a/fish/species/goby_round/fish_goby_round.png.import b/fish/species/goby_round/fish_goby_round.png.import
deleted file mode 100644
index 6fb96e8..0000000
--- a/fish/species/goby_round/fish_goby_round.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bdhywe0r1xcio"
-path="res://.godot/imported/fish_goby_round.png-3c3fd3dd06c7b5dd4539ab186bf0d865.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/goby_round/fish_goby_round.png"
-dest_files=["res://.godot/imported/fish_goby_round.png-3c3fd3dd06c7b5dd4539ab186bf0d865.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/goby_round/goby_round.tres b/fish/species/goby_round/goby_round.tres
deleted file mode 100644
index d8edf5e..0000000
--- a/fish/species/goby_round/goby_round.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://o1pojc8xthiw"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="RoundGobyAvailability"]
-script = ExtResource("3_availability_script")
-preferred_bait_tags = Array[StringName]([&"worm", &"minnow"])
-preferred_bait_weight_multiplier = 1.35
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"goby_round"
-display_name = "round goby"
-active = true
-catalog_number = 3401
-collection_group = &"Freshwater small fish"
-logbook_fact = "round gobies use fused pelvic fins like a suction cup to hold onto rocks. they are small fish with a famously large appetite."
-available_seasons = 15
-allowed_water_types = 1
-base_catch_weight = 8.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("RoundGobyAvailability")
-weight_min_lb = 0.1
-weight_max_lb = 0.6
-display_scale_min = 0.55
-display_scale_max = 0.9
-sell_value_min = 2
-sell_value_max = 3
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/goldeye/goldeye.tres b/fish/species/goldeye/goldeye.tres
deleted file mode 100644
index 1d09b7c..0000000
--- a/fish/species/goldeye/goldeye.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"goldeye"
-display_name = "goldeye"
-active = true
-catalog_number = 2903
-collection_group = &"Freshwater oddities"
-logbook_fact = "goldeye patrol turbid rivers with large light-sensitive eyes and a toothed tongue. they rise toward the surface to hunt insects and small fish."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 6.0
-sell_value_min = 12
-sell_value_max = 25
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/goldfish/fish_goldfish.png b/fish/species/goldfish/fish_goldfish.png
deleted file mode 100644
index 7214195..0000000
Binary files a/fish/species/goldfish/fish_goldfish.png and /dev/null differ
diff --git a/fish/species/goldfish/fish_goldfish.png.import b/fish/species/goldfish/fish_goldfish.png.import
deleted file mode 100644
index 4d9a960..0000000
--- a/fish/species/goldfish/fish_goldfish.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c6uth7aob03kg"
-path="res://.godot/imported/fish_goldfish.png-567cddb5ae1fc7839cc6dbc80a9a23f3.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/goldfish/fish_goldfish.png"
-dest_files=["res://.godot/imported/fish_goldfish.png-567cddb5ae1fc7839cc6dbc80a9a23f3.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/goldfish/goldfish.tres b/fish/species/goldfish/goldfish.tres
deleted file mode 100644
index 444741b..0000000
--- a/fish/species/goldfish/goldfish.tres
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://cw7vdjhhfgtax"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="GoldfishAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"starter_pond", &"pond"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"goldfish"
-display_name = "goldfish"
-active = true
-catalog_number = 3002
-collection_group = &"Freshwater ornamental"
-logbook_fact = "goldfish can tolerate a surprisingly wide range of conditions. their wild relatives often live in slow, weedy water."
-available_seasons = 15
-allowed_water_types = 1
-base_catch_weight = 0.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("GoldfishAvailability")
-weight_min_lb = 0.1
-weight_max_lb = 4.0
-display_scale_min = 0.85
-display_scale_max = 1.1
-display_scale_curve = 0.9
-sell_value_min = 1
-sell_value_max = 3
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/goldfish_bubbleeye/fish_goldfish_bubbleeye.png b/fish/species/goldfish_bubbleeye/fish_goldfish_bubbleeye.png
deleted file mode 100644
index c25b2f7..0000000
Binary files a/fish/species/goldfish_bubbleeye/fish_goldfish_bubbleeye.png and /dev/null differ
diff --git a/fish/species/goldfish_bubbleeye/fish_goldfish_bubbleeye.png.import b/fish/species/goldfish_bubbleeye/fish_goldfish_bubbleeye.png.import
deleted file mode 100644
index 1344959..0000000
--- a/fish/species/goldfish_bubbleeye/fish_goldfish_bubbleeye.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://brjjnxr6n7olr"
-path="res://.godot/imported/fish_goldfish_bubbleeye.png-a8dcb7e4fb9c7a1beebe2852d2d38d80.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/goldfish_bubbleeye/fish_goldfish_bubbleeye.png"
-dest_files=["res://.godot/imported/fish_goldfish_bubbleeye.png-a8dcb7e4fb9c7a1beebe2852d2d38d80.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/goldfish_bubbleeye/goldfish_bubbleeye.tres b/fish/species/goldfish_bubbleeye/goldfish_bubbleeye.tres
deleted file mode 100644
index d88719c..0000000
--- a/fish/species/goldfish_bubbleeye/goldfish_bubbleeye.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://beiutt7wq87rp"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="GoldfishBubbleeyeAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"starter_pond", &"pond"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"goldfish_bubbleeye"
-display_name = "bubble-eye goldfish"
-active = true
-catalog_number = 3001
-collection_group = &"Freshwater ornamental"
-logbook_fact = "bubble-eye goldfish have delicate fluid-filled sacs beneath their eyes, making careful handling especially important."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.35
-catch_profile = ExtResource("2_profile")
-availability = SubResource("GoldfishBubbleeyeAvailability")
-weight_min_lb = 0.1
-weight_max_lb = 0.35
-display_scale_min = 0.85
-display_scale_max = 1.0
-display_scale_curve = 0.9
-sell_value_min = 3
-sell_value_max = 7
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/gourami_giant/gourami_giant.tres b/fish/species/gourami_giant/gourami_giant.tres
deleted file mode 100644
index b4f8f52..0000000
--- a/fish/species/gourami_giant/gourami_giant.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"gourami_giant"
-display_name = "giant gourami"
-active = true
-catalog_number = 7505
-collection_group = &"Tropical freshwater fish"
-logbook_fact = "giant gouramis breathe surface air through a specialized labyrinth organ. adults graze plants but also investigate insects and small animals."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.25
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 2.0
-weight_max_lb = 20.0
-sell_value_min = 13
-sell_value_max = 31
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/grayling_arctic/grayling_arctic.tres b/fish/species/grayling_arctic/grayling_arctic.tres
deleted file mode 100644
index ae6321a..0000000
--- a/fish/species/grayling_arctic/grayling_arctic.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"grayling_arctic"
-display_name = "arctic grayling"
-active = true
-catalog_number = 1102
-collection_group = &"Cold freshwater salmonids"
-logbook_fact = "arctic grayling carry a tall, colorful dorsal fin like a sail. they feed on drifting insects in cold, clear rivers and lakes."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.75
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 6.0
-sell_value_min = 6
-sell_value_max = 14
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/green_sunfish/green_sunfish.tres b/fish/species/green_sunfish/green_sunfish.tres
deleted file mode 100644
index fdd1ddd..0000000
--- a/fish/species/green_sunfish/green_sunfish.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"green_sunfish"
-display_name = "green sunfish"
-active = true
-catalog_number = 3103
-collection_group = &"Freshwater panfish"
-logbook_fact = "green sunfish tolerate small, warm, cloudy pools better than many relatives. a large mouth lets them tackle insects, crayfish, and fish."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 3.2
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 1.5
-sell_value_min = 2
-sell_value_max = 5
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/grouper_atlantic_goliath/grouper_atlantic_goliath.tres b/fish/species/grouper_atlantic_goliath/grouper_atlantic_goliath.tres
deleted file mode 100644
index eb2d2bf..0000000
--- a/fish/species/grouper_atlantic_goliath/grouper_atlantic_goliath.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"grouper_atlantic_goliath"
-display_name = "Atlantic goliath grouper"
-active = true
-catalog_number = 5801
-collection_group = &"Reef predators"
-logbook_fact = "atlantic goliath groupers occupy reefs, wrecks, and mangrove roots. enormous mouths pull prey inward with a sudden pressure drop."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.05
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 20.0
-weight_max_lb = 800.0
-sell_value_min = 50
-sell_value_max = 125
-sell_value_curve = 0.78
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/grouper_giant/grouper_giant.tres b/fish/species/grouper_giant/grouper_giant.tres
deleted file mode 100644
index 9051115..0000000
--- a/fish/species/grouper_giant/grouper_giant.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"grouper_giant"
-display_name = "giant grouper"
-active = true
-catalog_number = 5803
-collection_group = &"Reef predators"
-logbook_fact = "giant groupers become massive ambush predators around indo-pacific reefs. juveniles carry bold black-and-yellow markings."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.045
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 20.0
-weight_max_lb = 880.0
-sell_value_min = 52
-sell_value_max = 128
-sell_value_curve = 0.78
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/grouper_gulf/fish_grouper_gulf.png b/fish/species/grouper_gulf/fish_grouper_gulf.png
deleted file mode 100644
index b1dd35a..0000000
Binary files a/fish/species/grouper_gulf/fish_grouper_gulf.png and /dev/null differ
diff --git a/fish/species/grouper_gulf/fish_grouper_gulf.png.import b/fish/species/grouper_gulf/fish_grouper_gulf.png.import
deleted file mode 100644
index c14f066..0000000
--- a/fish/species/grouper_gulf/fish_grouper_gulf.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://ctu8xd5or7v3h"
-path="res://.godot/imported/fish_grouper_gulf.png-8313134ee6830c8e9208b9e3cf5c5333.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/grouper_gulf/fish_grouper_gulf.png"
-dest_files=["res://.godot/imported/fish_grouper_gulf.png-8313134ee6830c8e9208b9e3cf5c5333.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/grouper_gulf/grouper_gulf.tres b/fish/species/grouper_gulf/grouper_gulf.tres
deleted file mode 100644
index ec4b583..0000000
--- a/fish/species/grouper_gulf/grouper_gulf.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://c5w5e2rkqiaep"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="GrouperGulfAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"grouper_gulf"
-display_name = "gulf grouper"
-active = true
-catalog_number = 5805
-collection_group = &"Reef predators"
-logbook_fact = "gulf grouper are ambush predators that use a sudden gulp to pull prey into their cavernous mouths."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("GrouperGulfAvailability")
-weight_min_lb = 2
-weight_max_lb = 40
-display_scale_min = 0.85
-display_scale_max = 1.8
-display_scale_curve = 0.9
-sell_value_min = 10
-sell_value_max = 24
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/grouper_nassau/grouper_nassau.tres b/fish/species/grouper_nassau/grouper_nassau.tres
deleted file mode 100644
index de4b0fd..0000000
--- a/fish/species/grouper_nassau/grouper_nassau.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"grouper_nassau"
-display_name = "Nassau grouper"
-active = true
-catalog_number = 5806
-collection_group = &"Reef predators"
-logbook_fact = "nassau groupers shift their bars and colors with mood and surroundings. adults gather at traditional reef sites to spawn."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.12
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 2.0
-weight_max_lb = 55.0
-sell_value_min = 25
-sell_value_max = 62
-sell_value_curve = 0.82
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/grouper_red/fish_grouper_red.png b/fish/species/grouper_red/fish_grouper_red.png
deleted file mode 100644
index f0564b7..0000000
Binary files a/fish/species/grouper_red/fish_grouper_red.png and /dev/null differ
diff --git a/fish/species/grouper_red/fish_grouper_red.png.import b/fish/species/grouper_red/fish_grouper_red.png.import
deleted file mode 100644
index bd8eac9..0000000
--- a/fish/species/grouper_red/fish_grouper_red.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://jjcc78tffxhb"
-path="res://.godot/imported/fish_grouper_red.png-6056013819bdaaeb55a98fa0f9d8ca75.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/grouper_red/fish_grouper_red.png"
-dest_files=["res://.godot/imported/fish_grouper_red.png-6056013819bdaaeb55a98fa0f9d8ca75.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/grouper_red/grouper_red.tres b/fish/species/grouper_red/grouper_red.tres
deleted file mode 100644
index d746cc2..0000000
--- a/fish/species/grouper_red/grouper_red.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://ckr1c747mvjl0"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="GrouperRedAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"grouper_red"
-display_name = "red grouper"
-active = true
-catalog_number = 5807
-collection_group = &"Reef predators"
-logbook_fact = "red grouper excavate shelters in reef rubble. the hollows they make can become hiding places for many smaller animals."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.2
-catch_profile = ExtResource("2_profile")
-availability = SubResource("GrouperRedAvailability")
-weight_min_lb = 2
-weight_max_lb = 30
-display_scale_min = 0.85
-display_scale_max = 2.0
-display_scale_curve = 0.9
-sell_value_min = 16
-sell_value_max = 36
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/grunt_bluestriped/grunt_bluestriped.tres b/fish/species/grunt_bluestriped/grunt_bluestriped.tres
deleted file mode 100644
index 87e9215..0000000
--- a/fish/species/grunt_bluestriped/grunt_bluestriped.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"grunt_bluestriped"
-display_name = "bluestriped grunt"
-active = true
-catalog_number = 5902
-collection_group = &"Reef schooling fish"
-logbook_fact = "bluestriped grunts shelter on reefs by day and spread over seagrass to feed at night. grinding throat teeth produce audible grunts."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 2.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 3.0
-sell_value_min = 3
-sell_value_max = 7
-sell_value_curve = 0.94
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/grunt_french/grunt_french.tres b/fish/species/grunt_french/grunt_french.tres
deleted file mode 100644
index da0bd90..0000000
--- a/fish/species/grunt_french/grunt_french.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"grunt_french"
-display_name = "French grunt"
-active = true
-catalog_number = 5903
-collection_group = &"Reef schooling fish"
-logbook_fact = "french grunts gather in yellow-striped schools around reefs and wrecks. they travel into nearby sand and grass beds after dark."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 2.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 3.5
-sell_value_min = 3
-sell_value_max = 7
-sell_value_curve = 0.94
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/gulper_eel/gulper_eel.tres b/fish/species/gulper_eel/gulper_eel.tres
deleted file mode 100644
index 11245c9..0000000
--- a/fish/species/gulper_eel/gulper_eel.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"gulper_eel"
-display_name = "gulper eel"
-active = true
-catalog_number = 1803
-collection_group = &"Deep-sea oddities"
-logbook_fact = "gulper eels unfold enormous mouths to engulf prey in the deep sea. a whip-like tail ends in a small light-producing organ."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.04
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 18.0
-sell_value_min = 44
-sell_value_max = 92
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/guppy/guppy.tres b/fish/species/guppy/guppy.tres
deleted file mode 100644
index 9771dfb..0000000
--- a/fish/species/guppy/guppy.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"guppy"
-display_name = "guppy"
-active = true
-catalog_number = 2101
-collection_group = &"Freshwater aquarium fish"
-logbook_fact = "guppies are livebearers whose young arrive ready to swim. males display bright colors while females often favor larger, less flashy bodies."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 1.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.01
-weight_max_lb = 0.15
-sell_value_min = 2
-sell_value_max = 5
-sell_value_curve = 0.98
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/haddock/haddock.tres b/fish/species/haddock/haddock.tres
deleted file mode 100644
index ec5db7d..0000000
--- a/fish/species/haddock/haddock.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"haddock"
-display_name = "haddock"
-active = true
-catalog_number = 1405
-collection_group = &"Cold shelf fish"
-logbook_fact = "haddock are recognized by a dark shoulder mark and black lateral line. schools forage over cold seafloors for small bottom animals."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 2.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 20.0
-sell_value_min = 4
-sell_value_max = 11
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/hake_pacific/hake_pacific.tres b/fish/species/hake_pacific/hake_pacific.tres
deleted file mode 100644
index 2a7293b..0000000
--- a/fish/species/hake_pacific/hake_pacific.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"hake_pacific"
-display_name = "pacific hake"
-active = true
-catalog_number = 1407
-collection_group = &"Cold shelf fish"
-logbook_fact = "pacific hake rise from deep water at night to feed on krill and fish. large schools migrate along the west coast with changing seasons."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 1.7
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 8.0
-sell_value_min = 3
-sell_value_max = 9
-sell_value_curve = 0.92
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/hake_silver/hake_silver.tres b/fish/species/hake_silver/hake_silver.tres
deleted file mode 100644
index ffb42ff..0000000
--- a/fish/species/hake_silver/hake_silver.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"hake_silver"
-display_name = "silver hake"
-active = true
-catalog_number = 1408
-collection_group = &"Cold shelf fish"
-logbook_fact = "silver hake hunt fish and crustaceans over sandy atlantic shelves. they move upward after dark and return toward the bottom by day."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 1.7
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 5.0
-sell_value_min = 3
-sell_value_max = 8
-sell_value_curve = 0.92
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/halfbeak_ballyhoo/halfbeak_ballyhoo.tres b/fish/species/halfbeak_ballyhoo/halfbeak_ballyhoo.tres
deleted file mode 100644
index 9512381..0000000
--- a/fish/species/halfbeak_ballyhoo/halfbeak_ballyhoo.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"halfbeak_ballyhoo"
-display_name = "ballyhoo"
-active = true
-catalog_number = 6601
-collection_group = &"Surface ocean fish"
-logbook_fact = "ballyhoo skim warm ocean surfaces with an extended lower jaw. tight schools scatter into quick leaps when predators strike."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 2.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 1.0
-sell_value_min = 2
-sell_value_max = 5
-sell_value_curve = 0.98
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/halibut_atlantic/halibut_atlantic.tres b/fish/species/halibut_atlantic/halibut_atlantic.tres
deleted file mode 100644
index ead724a..0000000
--- a/fish/species/halibut_atlantic/halibut_atlantic.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"halibut_atlantic"
-display_name = "atlantic halibut"
-active = true
-catalog_number = 3701
-collection_group = &"Large flatfish"
-logbook_fact = "atlantic halibut are immense right-eyed flatfish of cold northern seas. adults ambush fish and squid from deep rocky or sandy bottoms."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 10.0
-weight_max_lb = 300.0
-sell_value_min = 32
-sell_value_max = 82
-sell_value_curve = 0.8
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/halibut_pacific/halibut_pacific.tres b/fish/species/halibut_pacific/halibut_pacific.tres
deleted file mode 100644
index 3856b5a..0000000
--- a/fish/species/halibut_pacific/halibut_pacific.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"halibut_pacific"
-display_name = "pacific halibut"
-active = true
-catalog_number = 3702
-collection_group = &"Large flatfish"
-logbook_fact = "pacific halibut lie camouflaged on cold continental shelves. powerful flat bodies can travel long distances between feeding and spawning grounds."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 10.0
-weight_max_lb = 350.0
-sell_value_min = 34
-sell_value_max = 86
-sell_value_curve = 0.8
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/hellbender/hellbender.tres b/fish/species/hellbender/hellbender.tres
deleted file mode 100644
index 2c55863..0000000
--- a/fish/species/hellbender/hellbender.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"hellbender"
-display_name = "hellbender"
-active = true
-catalog_number = 2002
-collection_group = &"Freshwater amphibians"
-logbook_fact = "hellbenders hide beneath large rocks in cool, clean rivers. folds of loose skin absorb oxygen while the flattened body grips the current."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 5.5
-sell_value_min = 24
-sell_value_max = 54
-sell_value_curve = 0.91
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/herring_atlantic/herring_atlantic.tres b/fish/species/herring_atlantic/herring_atlantic.tres
deleted file mode 100644
index e193dcc..0000000
--- a/fish/species/herring_atlantic/herring_atlantic.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"herring_atlantic"
-display_name = "atlantic herring"
-active = true
-catalog_number = 1301
-collection_group = &"Cold schooling fish"
-logbook_fact = "atlantic herring gather in immense schools that move between feeding and spawning grounds. they filter plankton with fine gill rakers."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 3.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 1.5
-sell_value_min = 2
-sell_value_max = 5
-sell_value_curve = 0.98
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/herring_pacific/herring_pacific.tres b/fish/species/herring_pacific/herring_pacific.tres
deleted file mode 100644
index 7d8e754..0000000
--- a/fish/species/herring_pacific/herring_pacific.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"herring_pacific"
-display_name = "pacific herring"
-active = true
-catalog_number = 1304
-collection_group = &"Cold schooling fish"
-logbook_fact = "pacific herring return to coastal shallows to coat plants and rocks with eggs. their oily schools support entire marine food webs."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 2.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 1.5
-sell_value_min = 2
-sell_value_max = 5
-sell_value_curve = 0.98
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/hogfish/hogfish.tres b/fish/species/hogfish/hogfish.tres
deleted file mode 100644
index 0e94a20..0000000
--- a/fish/species/hogfish/hogfish.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/6103.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"hogfish"
-display_name = "hogfish"
-active = true
-catalog_number = 6103
-collection_group = &"Reef wrasses"
-logbook_fact = "hogfish root through sand with long pig-like snouts in search of shellfish. adults can change from female to male as social groups shift."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 20.0
-sell_value_min = 12
-sell_value_max = 30
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/ide/ide.tres b/fish/species/ide/ide.tres
deleted file mode 100644
index aea8907..0000000
--- a/fish/species/ide/ide.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"ide"
-display_name = "ide"
-active = true
-catalog_number = 2507
-collection_group = &"Freshwater coarse fish"
-logbook_fact = "ide move between rivers, lakes, and brackish coastal water in loose schools. adults eat insects, crustaceans, plants, and small fish."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 9.0
-sell_value_min = 6
-sell_value_max = 14
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/jack_crevalle/jack_crevalle.tres b/fish/species/jack_crevalle/jack_crevalle.tres
deleted file mode 100644
index 4110dcc..0000000
--- a/fish/species/jack_crevalle/jack_crevalle.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"jack_crevalle"
-display_name = "crevalle jack"
-active = true
-catalog_number = 705
-collection_group = &"Coastal pelagic fish"
-logbook_fact = "crevalle jacks patrol warm coasts in fast-moving schools. their deep bodies and forked tails deliver long, powerful runs."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 35.0
-sell_value_min = 7
-sell_value_max = 20
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/jelly_crystal/jelly_crystal.tres b/fish/species/jelly_crystal/jelly_crystal.tres
deleted file mode 100644
index f8746fa..0000000
--- a/fish/species/jelly_crystal/jelly_crystal.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"jelly_crystal"
-display_name = "crystal jelly"
-active = true
-catalog_number = 3501
-collection_group = &"Jellies"
-logbook_fact = "crystal jellies are nearly transparent except for glowing points around the bell. their green fluorescent protein transformed biological research."
-available_seasons = 9
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.24
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 4.0
-sell_value_min = 15
-sell_value_max = 34
-sell_value_curve = 0.93
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/jelly_lions_mane/jelly_lions_mane.tres b/fish/species/jelly_lions_mane/jelly_lions_mane.tres
deleted file mode 100644
index c7c6fbb..0000000
--- a/fish/species/jelly_lions_mane/jelly_lions_mane.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"jelly_lions_mane"
-display_name = "lion's mane jelly"
-active = true
-catalog_number = 3502
-collection_group = &"Jellies"
-logbook_fact = "lion's mane jellies trail dense curtains of tentacles through cold seas. the bell darkens from yellow to deep red as the animal grows."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.11
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 200.0
-sell_value_min = 24
-sell_value_max = 58
-sell_value_curve = 0.82
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/jelly_moon/jelly_moon.tres b/fish/species/jelly_moon/jelly_moon.tres
deleted file mode 100644
index 378c11d..0000000
--- a/fish/species/jelly_moon/jelly_moon.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"jelly_moon"
-display_name = "moon jelly"
-active = true
-catalog_number = 3503
-collection_group = &"Jellies"
-logbook_fact = "moon jellies pulse gently with four horseshoe-shaped organs visible through the bell. short tentacles sweep plankton toward the mouth."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.7
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 5.0
-sell_value_min = 6
-sell_value_max = 14
-sell_value_curve = 0.94
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/jelly_pacific_sea_nettle/jelly_pacific_sea_nettle.tres b/fish/species/jelly_pacific_sea_nettle/jelly_pacific_sea_nettle.tres
deleted file mode 100644
index b31f4b3..0000000
--- a/fish/species/jelly_pacific_sea_nettle/jelly_pacific_sea_nettle.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"jelly_pacific_sea_nettle"
-display_name = "Pacific sea nettle"
-active = true
-catalog_number = 3504
-collection_group = &"Jellies"
-logbook_fact = "pacific sea nettles carry rust-colored stripes across pale bells. long tentacles and frilled mouth arms capture plankton and small fish."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.28
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 12.0
-sell_value_min = 14
-sell_value_max = 32
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/jelly_sea_wasp/jelly_sea_wasp.tres b/fish/species/jelly_sea_wasp/jelly_sea_wasp.tres
deleted file mode 100644
index 29bed0a..0000000
--- a/fish/species/jelly_sea_wasp/jelly_sea_wasp.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"jelly_sea_wasp"
-display_name = "sea wasp"
-active = true
-catalog_number = 3505
-collection_group = &"Jellies"
-logbook_fact = "sea wasps have box-shaped bells and clusters of eyes along the lower edge. their long tentacles deliver exceptionally potent venom."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.035
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 6.0
-sell_value_min = 42
-sell_value_max = 90
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/knifefish_clown/knifefish_clown.tres b/fish/species/knifefish_clown/knifefish_clown.tres
deleted file mode 100644
index c83b877..0000000
--- a/fish/species/knifefish_clown/knifefish_clown.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"knifefish_clown"
-display_name = "clown knifefish"
-active = true
-catalog_number = 7501
-collection_group = &"Tropical freshwater fish"
-logbook_fact = "clown knifefish undulate one long belly fin to glide forward or backward. dark eye-like spots line their silver nocturnal bodies."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.25
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 25.0
-sell_value_min = 16
-sell_value_max = 38
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/koi/koi.tres b/fish/species/koi/koi.tres
deleted file mode 100644
index 58c7c29..0000000
--- a/fish/species/koi/koi.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"koi"
-display_name = "koi"
-active = true
-catalog_number = 3003
-collection_group = &"Freshwater ornamental"
-logbook_fact = "koi are colorful domesticated forms of common carp selected over many generations. each fish carries a distinctive arrangement of patches and scales."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.28
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 35.0
-sell_value_min = 14
-sell_value_max = 34
-sell_value_curve = 0.87
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/krill_antarctic/krill_antarctic.tres b/fish/species/krill_antarctic/krill_antarctic.tres
deleted file mode 100644
index 8cdf8e6..0000000
--- a/fish/species/krill_antarctic/krill_antarctic.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"krill_antarctic"
-display_name = "Antarctic krill"
-active = true
-catalog_number = 3802
-collection_group = &"Lobsters and shrimp"
-logbook_fact = "antarctic krill form enormous swarms beneath southern seas and ice. these small crustaceans feed whales, seals, penguins, and fish."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.001
-weight_max_lb = 0.01
-sell_value_min = 10
-sell_value_max = 22
-sell_value_curve = 0.99
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/ladyfish/ladyfish.tres b/fish/species/ladyfish/ladyfish.tres
deleted file mode 100644
index 57c277c..0000000
--- a/fish/species/ladyfish/ladyfish.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/7403.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"ladyfish"
-display_name = "ladyfish"
-active = true
-catalog_number = 7403
-collection_group = &"Tropical flats fish"
-logbook_fact = "ladyfish race through warm shallows after schooling prey and often leap when pursued. a deeply forked tail powers their slender bodies."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 1.7
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 8.0
-sell_value_min = 4
-sell_value_max = 10
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/lanternfish_spotted/lanternfish_spotted.tres b/fish/species/lanternfish_spotted/lanternfish_spotted.tres
deleted file mode 100644
index 28446b7..0000000
--- a/fish/species/lanternfish_spotted/lanternfish_spotted.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"lanternfish_spotted"
-display_name = "spotted lanternfish"
-active = true
-catalog_number = 1806
-collection_group = &"Deep-sea oddities"
-logbook_fact = "spotted lanternfish carry rows of light-producing organs along their bodies. immense schools rise toward the surface each night."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.24
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 0.5
-sell_value_min = 14
-sell_value_max = 30
-sell_value_curve = 0.95
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/largemouth_bass/largemouth_bass.tres b/fish/species/largemouth_bass/largemouth_bass.tres
deleted file mode 100644
index aa89d04..0000000
--- a/fish/species/largemouth_bass/largemouth_bass.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"largemouth_bass"
-display_name = "largemouth bass"
-active = true
-catalog_number = 2201
-collection_group = &"Freshwater bass"
-logbook_fact = "largemouth bass wait among weeds and timber before lunging at prey. the upper jaw extends beyond the eye when the mouth is closed."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 2.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 18.0
-sell_value_min = 4
-sell_value_max = 12
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/lingcod/lingcod.tres b/fish/species/lingcod/lingcod.tres
deleted file mode 100644
index 807dc8b..0000000
--- a/fish/species/lingcod/lingcod.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"lingcod"
-display_name = "lingcod"
-active = true
-catalog_number = 7001
-collection_group = &"Temperate reef predators"
-logbook_fact = "lingcod guard rocky dens and ambush fish with a huge tooth-lined mouth. despite the name, they are greenling relatives rather than cod."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.25
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 2.0
-weight_max_lb = 60.0
-sell_value_min = 14
-sell_value_max = 38
-sell_value_curve = 0.83
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/lionfish_red/lionfish_red.tres b/fish/species/lionfish_red/lionfish_red.tres
deleted file mode 100644
index f3c3b3b..0000000
--- a/fish/species/lionfish_red/lionfish_red.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"lionfish_red"
-display_name = "red lionfish"
-active = true
-catalog_number = 5201
-collection_group = &"Reef ambush fish"
-logbook_fact = "red lionfish spread ornate venomous spines when threatened. broad fins herd smaller fish into corners before a rapid gulp."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 3.0
-sell_value_min = 11
-sell_value_max = 25
-sell_value_curve = 0.92
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/loach_clown/loach_clown.tres b/fish/species/loach_clown/loach_clown.tres
deleted file mode 100644
index f99a1be..0000000
--- a/fish/species/loach_clown/loach_clown.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"loach_clown"
-display_name = "clown loach"
-active = true
-catalog_number = 7502
-collection_group = &"Tropical freshwater fish"
-logbook_fact = "clown loaches rest in groups and make clicking sounds while feeding. orange bodies carry three broad black bands and small eye spines."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.32
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 2.0
-sell_value_min = 10
-sell_value_max = 22
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/loach_weather/loach_weather.tres b/fish/species/loach_weather/loach_weather.tres
deleted file mode 100644
index cc7afef..0000000
--- a/fish/species/loach_weather/loach_weather.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"loach_weather"
-display_name = "weather loach"
-active = true
-catalog_number = 2905
-collection_group = &"Freshwater oddities"
-logbook_fact = "weather loaches gulp air and pass it through the intestine when oxygen runs low. restless activity often increases as air pressure falls."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.85
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 0.7
-sell_value_min = 5
-sell_value_max = 11
-sell_value_curve = 0.95
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/lobster_american/lobster_american.tres b/fish/species/lobster_american/lobster_american.tres
deleted file mode 100644
index b70b809..0000000
--- a/fish/species/lobster_american/lobster_american.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"lobster_american"
-display_name = "American lobster"
-active = true
-catalog_number = 3801
-collection_group = &"Lobsters and shrimp"
-logbook_fact = "american lobsters shelter in rocky crevices and emerge to forage along the bottom. one claw crushes while the other cuts and grips."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.28
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 44.0
-sell_value_min = 16
-sell_value_max = 42
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/lobster_caribbean_spiny/lobster_caribbean_spiny.tres b/fish/species/lobster_caribbean_spiny/lobster_caribbean_spiny.tres
deleted file mode 100644
index cdd828c..0000000
--- a/fish/species/lobster_caribbean_spiny/lobster_caribbean_spiny.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"lobster_caribbean_spiny"
-display_name = "Caribbean spiny lobster"
-active = true
-catalog_number = 3803
-collection_group = &"Lobsters and shrimp"
-logbook_fact = "caribbean spiny lobsters rely on long antennae and a thorny shell instead of large claws. groups march together across the seafloor."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.25
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 26.0
-sell_value_min = 15
-sell_value_max = 38
-sell_value_curve = 0.87
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/lungfish_west_african/lungfish_west_african.png b/fish/species/lungfish_west_african/lungfish_west_african.png
deleted file mode 100644
index 9960ab0..0000000
Binary files a/fish/species/lungfish_west_african/lungfish_west_african.png and /dev/null differ
diff --git a/fish/species/lungfish_west_african/lungfish_west_african.png.import b/fish/species/lungfish_west_african/lungfish_west_african.png.import
deleted file mode 100644
index dfb1875..0000000
--- a/fish/species/lungfish_west_african/lungfish_west_african.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://i7uefoxij0xr"
-path="res://.godot/imported/lungfish_west_african.png-39f7845a079da2da99841f01c797b183.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/lungfish_west_african/lungfish_west_african.png"
-dest_files=["res://.godot/imported/lungfish_west_african.png-39f7845a079da2da99841f01c797b183.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/lungfish_west_african/lungfish_west_african.tres b/fish/species/lungfish_west_african/lungfish_west_african.tres
deleted file mode 100644
index f049cff..0000000
--- a/fish/species/lungfish_west_african/lungfish_west_african.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/107.png" id="4_texture"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"lungfish_west_african"
-display_name = "West African lungfish"
-active = true
-catalog_number = 107
-collection_group = &"Ancient freshwater fish"
-logbook_fact = "west african lungfish breathe air with paired lungs. when floodplains dry, they seal themselves in a mud cocoon until water returns."
-available_seasons = 6
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 10.0
-sell_value_min = 12
-sell_value_max = 28
-sell_value_curve = 0.86
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/mackerel_atlantic/fish_mackerel_atlantic.png b/fish/species/mackerel_atlantic/fish_mackerel_atlantic.png
deleted file mode 100644
index e5fbd98..0000000
Binary files a/fish/species/mackerel_atlantic/fish_mackerel_atlantic.png and /dev/null differ
diff --git a/fish/species/mackerel_atlantic/fish_mackerel_atlantic.png.import b/fish/species/mackerel_atlantic/fish_mackerel_atlantic.png.import
deleted file mode 100644
index 424fcda..0000000
--- a/fish/species/mackerel_atlantic/fish_mackerel_atlantic.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://breyc8fqq8lq3"
-path="res://.godot/imported/fish_mackerel_atlantic.png-51809cdfc48d2f0c69100567067a64e8.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/mackerel_atlantic/fish_mackerel_atlantic.png"
-dest_files=["res://.godot/imported/fish_mackerel_atlantic.png-51809cdfc48d2f0c69100567067a64e8.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/mackerel_atlantic/mackerel_atlantic.tres b/fish/species/mackerel_atlantic/mackerel_atlantic.tres
deleted file mode 100644
index ba6e0d8..0000000
--- a/fish/species/mackerel_atlantic/mackerel_atlantic.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://cm8hgt2qubpi8"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/701.png" id="4_texture"]
-
-[sub_resource type="Resource" id="MackerelAtlanticAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"mackerel_atlantic"
-display_name = "atlantic mackerel"
-active = true
-catalog_number = 701
-collection_group = &"Coastal pelagic fish"
-logbook_fact = "atlantic mackerel travel in fast schools across the north atlantic and feed on plankton and small schooling fish."
-available_seasons = 7
-allowed_water_types = 2
-base_catch_weight = 0.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("MackerelAtlanticAvailability")
-weight_max_lb = 5
-display_scale_min = 0.85
-display_scale_max = 1.4
-display_scale_curve = 0.9
-sell_value_min = 4
-sell_value_max = 9
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/mackerel_cero/fish_mackerel_cero.png b/fish/species/mackerel_cero/fish_mackerel_cero.png
deleted file mode 100644
index 610b99c..0000000
Binary files a/fish/species/mackerel_cero/fish_mackerel_cero.png and /dev/null differ
diff --git a/fish/species/mackerel_cero/fish_mackerel_cero.png.import b/fish/species/mackerel_cero/fish_mackerel_cero.png.import
deleted file mode 100644
index 131c8fc..0000000
--- a/fish/species/mackerel_cero/fish_mackerel_cero.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://g8qanq47g1b8"
-path="res://.godot/imported/fish_mackerel_cero.png-8da33920501446c8e4d87a07f02f0710.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/mackerel_cero/fish_mackerel_cero.png"
-dest_files=["res://.godot/imported/fish_mackerel_cero.png-8da33920501446c8e4d87a07f02f0710.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/mackerel_cero/mackerel_cero.tres b/fish/species/mackerel_cero/mackerel_cero.tres
deleted file mode 100644
index 25cd4c5..0000000
--- a/fish/species/mackerel_cero/mackerel_cero.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://du4ixk4sssvac"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="MackerelCeroAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"mackerel_cero"
-display_name = "cero mackerel"
-active = true
-catalog_number = 703
-collection_group = &"Coastal pelagic fish"
-logbook_fact = "cero mackerel are streamlined coastal hunters. sharp teeth help them slash through schools of smaller fish."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.35
-catch_profile = ExtResource("2_profile")
-availability = SubResource("MackerelCeroAvailability")
-weight_min_lb = 1
-weight_max_lb = 15
-display_scale_min = 0.85
-display_scale_max = 1.5
-display_scale_curve = 0.9
-sell_value_min = 7
-sell_value_max = 15
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/mackerel_chub/fish_mackerel_chub.png b/fish/species/mackerel_chub/fish_mackerel_chub.png
deleted file mode 100644
index 2d668fd..0000000
Binary files a/fish/species/mackerel_chub/fish_mackerel_chub.png and /dev/null differ
diff --git a/fish/species/mackerel_chub/fish_mackerel_chub.png.import b/fish/species/mackerel_chub/fish_mackerel_chub.png.import
deleted file mode 100644
index d6be52b..0000000
--- a/fish/species/mackerel_chub/fish_mackerel_chub.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b7q2t02oo2bcp"
-path="res://.godot/imported/fish_mackerel_chub.png-16a10a6e686dbb920ba5ae98c9dd10a8.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/mackerel_chub/fish_mackerel_chub.png"
-dest_files=["res://.godot/imported/fish_mackerel_chub.png-16a10a6e686dbb920ba5ae98c9dd10a8.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/mackerel_chub/mackerel_chub.tres b/fish/species/mackerel_chub/mackerel_chub.tres
deleted file mode 100644
index 891abe4..0000000
--- a/fish/species/mackerel_chub/mackerel_chub.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://b6lga2c60qkc4"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/704.png" id="4_texture"]
-
-[sub_resource type="Resource" id="MackerelChubAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"mackerel_chub"
-display_name = "chub mackerel"
-active = true
-catalog_number = 704
-collection_group = &"Coastal pelagic fish"
-logbook_fact = "chub mackerel school near the surface and follow plankton blooms. their dark wavy bars help identify them at a glance."
-available_seasons = 7
-allowed_water_types = 2
-base_catch_weight = 0.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("MackerelChubAvailability")
-weight_max_lb = 4
-display_scale_min = 0.85
-display_scale_max = 1.4
-display_scale_curve = 0.9
-sell_value_min = 4
-sell_value_max = 9
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/mackerel_king/fish_mackerel_king.png b/fish/species/mackerel_king/fish_mackerel_king.png
deleted file mode 100644
index 7ec6614..0000000
Binary files a/fish/species/mackerel_king/fish_mackerel_king.png and /dev/null differ
diff --git a/fish/species/mackerel_king/fish_mackerel_king.png.import b/fish/species/mackerel_king/fish_mackerel_king.png.import
deleted file mode 100644
index 213307a..0000000
--- a/fish/species/mackerel_king/fish_mackerel_king.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b2fag3xemyx5s"
-path="res://.godot/imported/fish_mackerel_king.png-6cd4b6aef031a72436383f7e75f54cc9.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/mackerel_king/fish_mackerel_king.png"
-dest_files=["res://.godot/imported/fish_mackerel_king.png-6cd4b6aef031a72436383f7e75f54cc9.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/mackerel_king/mackerel_king.tres b/fish/species/mackerel_king/mackerel_king.tres
deleted file mode 100644
index 9c00470..0000000
--- a/fish/species/mackerel_king/mackerel_king.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://bacndgfbn8oj7"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="MackerelKingAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"mackerel_king"
-display_name = "king mackerel"
-active = true
-catalog_number = 706
-collection_group = &"Coastal pelagic fish"
-logbook_fact = "king mackerel are swift coastal predators. their pointed teeth and long body are built for sudden bursts of speed."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("MackerelKingAvailability")
-weight_min_lb = 3
-weight_max_lb = 40
-display_scale_min = 0.85
-display_scale_max = 1.8
-display_scale_curve = 0.9
-sell_value_min = 10
-sell_value_max = 24
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/mackerel_spanish/fish_mackerel_spanish.png b/fish/species/mackerel_spanish/fish_mackerel_spanish.png
deleted file mode 100644
index 5cbe847..0000000
Binary files a/fish/species/mackerel_spanish/fish_mackerel_spanish.png and /dev/null differ
diff --git a/fish/species/mackerel_spanish/fish_mackerel_spanish.png.import b/fish/species/mackerel_spanish/fish_mackerel_spanish.png.import
deleted file mode 100644
index 00d8314..0000000
--- a/fish/species/mackerel_spanish/fish_mackerel_spanish.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cy2hmpenpkee2"
-path="res://.godot/imported/fish_mackerel_spanish.png-14336b47fbd7fc1f1bcf0ce301b2b130.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/mackerel_spanish/fish_mackerel_spanish.png"
-dest_files=["res://.godot/imported/fish_mackerel_spanish.png-14336b47fbd7fc1f1bcf0ce301b2b130.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/mackerel_spanish/mackerel_spanish.tres b/fish/species/mackerel_spanish/mackerel_spanish.tres
deleted file mode 100644
index 1f0bcf4..0000000
--- a/fish/species/mackerel_spanish/mackerel_spanish.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://cg7y0numxuy3e"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="MackerelSpanishAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"mackerel_spanish"
-display_name = "spanish mackerel"
-active = true
-catalog_number = 707
-collection_group = &"Coastal pelagic fish"
-logbook_fact = "spanish mackerel hunt in warm coastal waters and often travel in loose schools while chasing baitfish."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.35
-catch_profile = ExtResource("2_profile")
-availability = SubResource("MackerelSpanishAvailability")
-weight_min_lb = 1
-weight_max_lb = 15
-display_scale_min = 0.85
-display_scale_max = 1.6
-display_scale_curve = 0.9
-sell_value_min = 8
-sell_value_max = 18
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/madtom_tadpole/madtom_tadpole.tres b/fish/species/madtom_tadpole/madtom_tadpole.tres
deleted file mode 100644
index d0a5545..0000000
--- a/fish/species/madtom_tadpole/madtom_tadpole.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"madtom_tadpole"
-display_name = "tadpole madtom"
-active = true
-catalog_number = 2406
-collection_group = &"Freshwater catfish"
-logbook_fact = "tadpole madtoms are tiny nocturnal catfish that shelter beneath vegetation and debris. venomous fin spines discourage larger predators."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.35
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.02
-weight_max_lb = 0.15
-sell_value_min = 8
-sell_value_max = 16
-sell_value_curve = 0.95
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/mahi_mahi/mahi_mahi.tres b/fish/species/mahi_mahi/mahi_mahi.tres
deleted file mode 100644
index 691a52a..0000000
--- a/fish/species/mahi_mahi/mahi_mahi.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/4701.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"mahi_mahi"
-display_name = "mahi-mahi"
-active = true
-catalog_number = 4701
-collection_group = &"Oceanic pelagic fish"
-logbook_fact = "mahi-mahi flash brilliant blue, green, and gold while chasing prey near the surface. they grow quickly and gather around floating objects."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.28
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 2.0
-weight_max_lb = 50.0
-sell_value_min = 15
-sell_value_max = 38
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/mahseer_golden/mahseer_golden.tres b/fish/species/mahseer_golden/mahseer_golden.tres
deleted file mode 100644
index 1459de1..0000000
--- a/fish/species/mahseer_golden/mahseer_golden.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"mahseer_golden"
-display_name = "golden mahseer"
-active = true
-catalog_number = 7603
-collection_group = &"Tropical freshwater predators"
-logbook_fact = "golden mahseer migrate through swift mountain rivers to reach spawning grounds. large scales shine bronze beneath reddish fins."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 3
-base_catch_weight = 0.09
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 8.0
-weight_max_lb = 90.0
-sell_value_min = 32
-sell_value_max = 72
-sell_value_curve = 0.82
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/manatee_west_indian/manatee_west_indian.tres b/fish/species/manatee_west_indian/manatee_west_indian.tres
deleted file mode 100644
index 7154157..0000000
--- a/fish/species/manatee_west_indian/manatee_west_indian.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"manatee_west_indian"
-display_name = "West Indian manatee"
-active = true
-catalog_number = 505
-collection_group = &"Coastal marine mammals"
-logbook_fact = "west indian manatees graze aquatic plants in warm coastal water and rivers. they surface regularly to breathe through nostrils atop the snout."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.012
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 440.0
-weight_max_lb = 1300.0
-sell_value_min = 70
-sell_value_max = 160
-sell_value_curve = 0.77
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/manta_ray_giant/manta_ray_giant.tres b/fish/species/manta_ray_giant/manta_ray_giant.tres
deleted file mode 100644
index 13eef47..0000000
--- a/fish/species/manta_ray_giant/manta_ray_giant.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"manta_ray_giant"
-display_name = "giant manta ray"
-active = true
-catalog_number = 5102
-collection_group = &"Rays and skates"
-logbook_fact = "giant manta rays funnel plankton into wide mouths with two flexible head fins. each belly carries a unique pattern of dark spots."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.025
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 100.0
-weight_max_lb = 1000.0
-sell_value_min = 65
-sell_value_max = 150
-sell_value_curve = 0.76
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/marlin_black/fish_marlin_black.png b/fish/species/marlin_black/fish_marlin_black.png
deleted file mode 100644
index ae4d9ab..0000000
Binary files a/fish/species/marlin_black/fish_marlin_black.png and /dev/null differ
diff --git a/fish/species/marlin_black/fish_marlin_black.png.import b/fish/species/marlin_black/fish_marlin_black.png.import
deleted file mode 100644
index 429803e..0000000
--- a/fish/species/marlin_black/fish_marlin_black.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dkdctr2o52vrb"
-path="res://.godot/imported/fish_marlin_black.png-8e8b83d60a3f56d5d14f9e7c3ce2a9f9.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/marlin_black/fish_marlin_black.png"
-dest_files=["res://.godot/imported/fish_marlin_black.png-8e8b83d60a3f56d5d14f9e7c3ce2a9f9.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/marlin_black/marlin_black.tres b/fish/species/marlin_black/marlin_black.tres
deleted file mode 100644
index 26f506a..0000000
--- a/fish/species/marlin_black/marlin_black.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://dlm608g365aa3"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="MarlinBlackAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"marlin_black"
-display_name = "black marlin"
-active = true
-catalog_number = 4601
-collection_group = &"Oceanic billfish"
-logbook_fact = "black marlin are powerful billfish that spend much of their lives in warm open ocean and can make blistering runs."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.08
-catch_profile = ExtResource("2_profile")
-availability = SubResource("MarlinBlackAvailability")
-weight_min_lb = 80
-weight_max_lb = 700
-display_scale_min = 0.85
-display_scale_max = 2.8
-display_scale_curve = 0.9
-sell_value_min = 45
-sell_value_max = 90
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/marlin_blue/fish_marlin_blue.png b/fish/species/marlin_blue/fish_marlin_blue.png
deleted file mode 100644
index 0f4c357..0000000
Binary files a/fish/species/marlin_blue/fish_marlin_blue.png and /dev/null differ
diff --git a/fish/species/marlin_blue/fish_marlin_blue.png.import b/fish/species/marlin_blue/fish_marlin_blue.png.import
deleted file mode 100644
index 73053d8..0000000
--- a/fish/species/marlin_blue/fish_marlin_blue.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://de35fjoytbomu"
-path="res://.godot/imported/fish_marlin_blue.png-268aff4aa2760732f42f94b881681364.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/marlin_blue/fish_marlin_blue.png"
-dest_files=["res://.godot/imported/fish_marlin_blue.png-268aff4aa2760732f42f94b881681364.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/marlin_blue/marlin_blue.tres b/fish/species/marlin_blue/marlin_blue.tres
deleted file mode 100644
index c8ee35d..0000000
--- a/fish/species/marlin_blue/marlin_blue.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://b15syao6ighl4"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="MarlinBlueAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"marlin_blue"
-display_name = "blue marlin"
-active = true
-catalog_number = 4602
-collection_group = &"Oceanic billfish"
-logbook_fact = "blue marlin are highly migratory open-ocean hunters. their long bill helps them stun prey before turning back to feed."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.05
-catch_profile = ExtResource("2_profile")
-availability = SubResource("MarlinBlueAvailability")
-weight_min_lb = 100
-weight_max_lb = 1000
-display_scale_min = 0.85
-display_scale_max = 3.0
-display_scale_curve = 0.9
-sell_value_min = 60
-sell_value_max = 120
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/marlin_white/fish_marlin_white.png b/fish/species/marlin_white/fish_marlin_white.png
deleted file mode 100644
index 327bd91..0000000
Binary files a/fish/species/marlin_white/fish_marlin_white.png and /dev/null differ
diff --git a/fish/species/marlin_white/fish_marlin_white.png.import b/fish/species/marlin_white/fish_marlin_white.png.import
deleted file mode 100644
index 1ec83d0..0000000
--- a/fish/species/marlin_white/fish_marlin_white.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bdvy4ujx2htcy"
-path="res://.godot/imported/fish_marlin_white.png-a5c5fec8891d945d6565d5097e312c31.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/marlin_white/fish_marlin_white.png"
-dest_files=["res://.godot/imported/fish_marlin_white.png-a5c5fec8891d945d6565d5097e312c31.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/marlin_white/marlin_white.tres b/fish/species/marlin_white/marlin_white.tres
deleted file mode 100644
index a69d01d..0000000
--- a/fish/species/marlin_white/marlin_white.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://jjxr31djus2h"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="MarlinWhiteAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"marlin_white"
-display_name = "white marlin"
-active = true
-catalog_number = 4605
-collection_group = &"Oceanic billfish"
-logbook_fact = "white marlin favor warm offshore water and use their rounded dorsal fin and bill to weave through schools of prey."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("MarlinWhiteAvailability")
-weight_min_lb = 30
-weight_max_lb = 180
-display_scale_min = 0.85
-display_scale_max = 2.5
-display_scale_curve = 0.9
-sell_value_min = 35
-sell_value_max = 70
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/menhaden_atlantic/menhaden_atlantic.tres b/fish/species/menhaden_atlantic/menhaden_atlantic.tres
deleted file mode 100644
index 53b4d6e..0000000
--- a/fish/species/menhaden_atlantic/menhaden_atlantic.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/802.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"menhaden_atlantic"
-display_name = "atlantic menhaden"
-active = true
-catalog_number = 802
-collection_group = &"Coastal schooling fish"
-logbook_fact = "atlantic menhaden strain plankton from coastal water with fine gill rakers. dense schools feed whales, seabirds, and predatory fish."
-available_seasons = 7
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 3.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 1.5
-sell_value_min = 2
-sell_value_max = 5
-sell_value_curve = 0.98
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/milkfish/milkfish.tres b/fish/species/milkfish/milkfish.tres
deleted file mode 100644
index 9c8362a..0000000
--- a/fish/species/milkfish/milkfish.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/7101.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"milkfish"
-display_name = "milkfish"
-active = true
-catalog_number = 7101
-collection_group = &"Tropical coastal fish"
-logbook_fact = "milkfish graze algae and tiny organisms in warm coastal shallows. deeply forked tails drive large schools through lagoons and estuaries."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 1.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 25.0
-sell_value_min = 4
-sell_value_max = 12
-sell_value_curve = 0.88
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/minnow_fathead/minnow_fathead.tres b/fish/species/minnow_fathead/minnow_fathead.tres
deleted file mode 100644
index cf00159..0000000
--- a/fish/species/minnow_fathead/minnow_fathead.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"minnow_fathead"
-display_name = "fathead minnow"
-active = true
-catalog_number = 2804
-collection_group = &"Freshwater minnows"
-logbook_fact = "fathead minnows tolerate warm, muddy ponds where many fish struggle. males guard eggs laid beneath rocks, logs, and other cover."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 4.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.02
-weight_max_lb = 0.2
-sell_value_min = 1
-sell_value_max = 2
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/monkfish/monkfish.tres b/fish/species/monkfish/monkfish.tres
deleted file mode 100644
index d485570..0000000
--- a/fish/species/monkfish/monkfish.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"monkfish"
-display_name = "monkfish"
-active = true
-catalog_number = 603
-collection_group = &"Coastal oddities"
-logbook_fact = "monkfish hide against the bottom and wave a fleshy lure above a cavernous mouth. a sudden gulp draws fish and crustaceans inward."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.25
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 2.0
-weight_max_lb = 55.0
-sell_value_min = 14
-sell_value_max = 35
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/mooneye/mooneye.tres b/fish/species/mooneye/mooneye.tres
deleted file mode 100644
index 3dff8a1..0000000
--- a/fish/species/mooneye/mooneye.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"mooneye"
-display_name = "mooneye"
-active = true
-catalog_number = 2904
-collection_group = &"Freshwater oddities"
-logbook_fact = "mooneyes flash silver in clear rivers and lakes while feeding near the surface. their large eyes suit evening hunts for insects and small fish."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.85
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 4.0
-sell_value_min = 5
-sell_value_max = 11
-sell_value_curve = 0.92
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/moray_green/moray_green.tres b/fish/species/moray_green/moray_green.tres
deleted file mode 100644
index e0d49ae..0000000
--- a/fish/species/moray_green/moray_green.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"moray_green"
-display_name = "green moray"
-active = true
-catalog_number = 5301
-collection_group = &"Reef eels"
-logbook_fact = "green morays appear green because yellow mucus coats blue-gray skin. a second set of jaws pulls captured prey deeper into the throat."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.28
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 30.0
-sell_value_min = 13
-sell_value_max = 32
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/mudskipper_atlantic/mudskipper_atlantic.tres b/fish/species/mudskipper_atlantic/mudskipper_atlantic.tres
deleted file mode 100644
index 7df958a..0000000
--- a/fish/species/mudskipper_atlantic/mudskipper_atlantic.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/201.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"mudskipper_atlantic"
-display_name = "atlantic mudskipper"
-active = true
-catalog_number = 201
-collection_group = &"Brackish oddities"
-logbook_fact = "atlantic mudskippers use sturdy fins to hop and crawl across exposed mud. moist skin and mouth linings let them breathe out of water."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 1.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.03
-weight_max_lb = 0.5
-sell_value_min = 5
-sell_value_max = 11
-sell_value_curve = 0.95
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/mullet_red/mullet_red.tres b/fish/species/mullet_red/mullet_red.tres
deleted file mode 100644
index 67a35fa..0000000
--- a/fish/species/mullet_red/mullet_red.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"mullet_red"
-display_name = "red mullet"
-active = true
-catalog_number = 301
-collection_group = &"Coastal bottom fish"
-logbook_fact = "red mullet comb sandy bottoms with two chin barbels that taste hidden prey. their foraging leaves winding trails in the sediment."
-available_seasons = 7
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.9
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 3.0
-sell_value_min = 5
-sell_value_max = 11
-sell_value_curve = 0.92
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/mullet_striped/mullet_striped.tres b/fish/species/mullet_striped/mullet_striped.tres
deleted file mode 100644
index a285ee7..0000000
--- a/fish/species/mullet_striped/mullet_striped.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/6804.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"mullet_striped"
-display_name = "striped mullet"
-active = true
-catalog_number = 6804
-collection_group = &"Temperate estuary"
-logbook_fact = "striped mullet leap clear of the water and travel in coastal schools. they graze algae and detritus with specialized digestive systems."
-available_seasons = 7
-allowed_water_types = 3
-rarity = 0
-base_catch_weight = 2.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 8.0
-sell_value_min = 3
-sell_value_max = 8
-sell_value_curve = 0.92
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/muskellunge/muskellunge.tres b/fish/species/muskellunge/muskellunge.tres
deleted file mode 100644
index e747d07..0000000
--- a/fish/species/muskellunge/muskellunge.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"muskellunge"
-display_name = "muskellunge"
-active = true
-catalog_number = 2704
-collection_group = &"Freshwater giants"
-logbook_fact = "muskellunge wait motionless among weeds before exploding toward prey. pale bodies carry bars or spots that fade into broken underwater light."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 3
-base_catch_weight = 0.11
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 8.0
-weight_max_lb = 60.0
-sell_value_min = 32
-sell_value_max = 72
-sell_value_curve = 0.82
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/nautilus_chambered/nautilus_chambered.tres b/fish/species/nautilus_chambered/nautilus_chambered.tres
deleted file mode 100644
index 562f16b..0000000
--- a/fish/species/nautilus_chambered/nautilus_chambered.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"nautilus_chambered"
-display_name = "chambered nautilus"
-active = true
-catalog_number = 1501
-collection_group = &"Cuttlefish and nautiluses"
-logbook_fact = "chambered nautiluses regulate buoyancy with gas-filled shell chambers. dozens of small tentacles feel for food in dark reef slopes."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.05
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 3.0
-sell_value_min = 34
-sell_value_max = 76
-sell_value_curve = 0.92
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/needlefish_hound/needlefish_hound.tres b/fish/species/needlefish_hound/needlefish_hound.tres
deleted file mode 100644
index 5d97670..0000000
--- a/fish/species/needlefish_hound/needlefish_hound.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"needlefish_hound"
-display_name = "houndfish"
-active = true
-catalog_number = 6602
-collection_group = &"Surface ocean fish"
-logbook_fact = "houndfish hunt near the surface with long jaws filled with fine teeth. powerful leaps can carry their narrow bodies far above the water."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 10.0
-sell_value_min = 11
-sell_value_max = 25
-sell_value_curve = 0.88
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/oarfish_giant/oarfish_giant.tres b/fish/species/oarfish_giant/oarfish_giant.tres
deleted file mode 100644
index 2bcd4ab..0000000
--- a/fish/species/oarfish_giant/oarfish_giant.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"oarfish_giant"
-display_name = "giant oarfish"
-active = true
-catalog_number = 1701
-collection_group = &"Deep-sea legends"
-logbook_fact = "giant oarfish drift vertically through deep open water, rowing with a crimson dorsal fin. their ribbon-like bodies can reach remarkable lengths."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.035
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 20.0
-weight_max_lb = 600.0
-sell_value_min = 60
-sell_value_max = 140
-sell_value_curve = 0.78
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/ocean_perch_pacific/ocean_perch_pacific.tres b/fish/species/ocean_perch_pacific/ocean_perch_pacific.tres
deleted file mode 100644
index 6b0cdcc..0000000
--- a/fish/species/ocean_perch_pacific/ocean_perch_pacific.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"ocean_perch_pacific"
-display_name = "Pacific ocean perch"
-active = true
-catalog_number = 1603
-collection_group = &"Deep shelf fish"
-logbook_fact = "pacific ocean perch hover around deep rocky reefs in the north pacific. these slow-growing fish give birth to live young."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 12.0
-sell_value_min = 7
-sell_value_max = 17
-sell_value_curve = 0.88
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/octopus_common/octopus_common.tres b/fish/species/octopus_common/octopus_common.tres
deleted file mode 100644
index e379010..0000000
--- a/fish/species/octopus_common/octopus_common.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"octopus_common"
-display_name = "common octopus"
-active = true
-catalog_number = 4901
-collection_group = &"Octopuses"
-logbook_fact = "common octopuses squeeze through narrow cracks and solve problems while hunting reefs. skin cells rapidly change color and texture."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.65
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 22.0
-sell_value_min = 7
-sell_value_max = 18
-sell_value_curve = 0.87
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/octopus_day/octopus_day.tres b/fish/species/octopus_day/octopus_day.tres
deleted file mode 100644
index 4384f38..0000000
--- a/fish/species/octopus_day/octopus_day.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"octopus_day"
-display_name = "day octopus"
-active = true
-catalog_number = 4902
-collection_group = &"Octopuses"
-logbook_fact = "day octopuses forage across tropical reefs while the sun is high. shifting bars and spots communicate alarm, camouflage, and threat."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.24
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 15.0
-sell_value_min = 15
-sell_value_max = 36
-sell_value_curve = 0.88
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/octopus_giant_pacific/octopus_giant_pacific.tres b/fish/species/octopus_giant_pacific/octopus_giant_pacific.tres
deleted file mode 100644
index 1091088..0000000
--- a/fish/species/octopus_giant_pacific/octopus_giant_pacific.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"octopus_giant_pacific"
-display_name = "giant Pacific octopus"
-active = true
-catalog_number = 4903
-collection_group = &"Octopuses"
-logbook_fact = "giant pacific octopuses explore cold rocky dens with thousands of arm suckers. females guard their eggs without feeding until they hatch."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 5.0
-weight_max_lb = 200.0
-sell_value_min = 28
-sell_value_max = 70
-sell_value_curve = 0.81
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/octopus_greater_blue_ringed/octopus_greater_blue_ringed.tres b/fish/species/octopus_greater_blue_ringed/octopus_greater_blue_ringed.tres
deleted file mode 100644
index f2a21d9..0000000
--- a/fish/species/octopus_greater_blue_ringed/octopus_greater_blue_ringed.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"octopus_greater_blue_ringed"
-display_name = "greater blue-ringed octopus"
-active = true
-catalog_number = 4904
-collection_group = &"Octopuses"
-logbook_fact = "greater blue-ringed octopuses reveal vivid rings when threatened. a tiny beak can deliver venom strong enough to stop breathing."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.04
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 0.3
-sell_value_min = 38
-sell_value_max = 82
-sell_value_curve = 0.96
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/opah/opah.tres b/fish/species/opah/opah.tres
deleted file mode 100644
index f9b202a..0000000
--- a/fish/species/opah/opah.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"opah"
-display_name = "opah"
-active = true
-catalog_number = 4702
-collection_group = &"Oceanic pelagic fish"
-logbook_fact = "opah circulate warm blood through much of the body, an unusual trait among fish. round silver flanks glow with red fins and pale spots."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.09
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 20.0
-weight_max_lb = 600.0
-sell_value_min = 36
-sell_value_max = 88
-sell_value_curve = 0.8
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/oscar/oscar.tres b/fish/species/oscar/oscar.tres
deleted file mode 100644
index 8e780cb..0000000
--- a/fish/species/oscar/oscar.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"oscar"
-display_name = "oscar"
-active = true
-catalog_number = 7507
-collection_group = &"Tropical freshwater fish"
-logbook_fact = "oscars are large south american cichlids that recognize routines and defend territories. a tail eyespot may redirect attacks away from the head."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 4.0
-sell_value_min = 6
-sell_value_max = 14
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/oyster_black_lip_pearl/oyster_black_lip_pearl.tres b/fish/species/oyster_black_lip_pearl/oyster_black_lip_pearl.tres
deleted file mode 100644
index 776218d..0000000
--- a/fish/species/oyster_black_lip_pearl/oyster_black_lip_pearl.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"oyster_black_lip_pearl"
-display_name = "black-lip pearl oyster"
-active = true
-catalog_number = 6401
-collection_group = &"Shellfish"
-logbook_fact = "black-lip pearl oysters line their shells with dark-edged nacre. a trapped irritant can become coated in the same pearly material."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.22
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 5.0
-sell_value_min = 14
-sell_value_max = 40
-sell_value_curve = 0.91
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/paddlefish/paddlefish.png b/fish/species/paddlefish/paddlefish.png
deleted file mode 100644
index 4a564d2..0000000
Binary files a/fish/species/paddlefish/paddlefish.png and /dev/null differ
diff --git a/fish/species/paddlefish/paddlefish.png.import b/fish/species/paddlefish/paddlefish.png.import
deleted file mode 100644
index 543fc80..0000000
--- a/fish/species/paddlefish/paddlefish.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://djnsidpdhgay5"
-path="res://.godot/imported/paddlefish.png-396db174ea9cb0a42741f573e0ac5be3.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/paddlefish/paddlefish.png"
-dest_files=["res://.godot/imported/paddlefish.png-396db174ea9cb0a42741f573e0ac5be3.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/paddlefish/paddlefish.tres b/fish/species/paddlefish/paddlefish.tres
deleted file mode 100644
index 498730b..0000000
--- a/fish/species/paddlefish/paddlefish.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/104.png" id="4_texture"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"paddlefish"
-display_name = "paddlefish"
-active = true
-catalog_number = 104
-collection_group = &"Ancient freshwater fish"
-logbook_fact = "paddlefish cruise open rivers with their mouths wide, filtering tiny plankton from the water. the broad snout senses faint electrical signals."
-available_seasons = 3
-allowed_water_types = 1
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 10.0
-weight_max_lb = 120.0
-sell_value_min = 34
-sell_value_max = 76
-sell_value_curve = 0.82
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/parrotfish_rainbow/parrotfish_rainbow.tres b/fish/species/parrotfish_rainbow/parrotfish_rainbow.tres
deleted file mode 100644
index c0b0dc2..0000000
--- a/fish/species/parrotfish_rainbow/parrotfish_rainbow.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"parrotfish_rainbow"
-display_name = "rainbow parrotfish"
-active = true
-catalog_number = 5502
-collection_group = &"Reef grazers"
-logbook_fact = "rainbow parrotfish scrape algae from reefs with fused beak-like teeth. crushed coral passes through them and returns to the reef as sand."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.12
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 2.0
-weight_max_lb = 40.0
-sell_value_min = 25
-sell_value_max = 58
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/parrotfish_stoplight/parrotfish_stoplight.tres b/fish/species/parrotfish_stoplight/parrotfish_stoplight.tres
deleted file mode 100644
index 50962f4..0000000
--- a/fish/species/parrotfish_stoplight/parrotfish_stoplight.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"parrotfish_stoplight"
-display_name = "stoplight parrotfish"
-active = true
-catalog_number = 5503
-collection_group = &"Reef grazers"
-logbook_fact = "stoplight parrotfish change color dramatically as they mature. powerful dental plates bite algae from hard reef surfaces."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.28
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 15.0
-sell_value_min = 12
-sell_value_max = 29
-sell_value_curve = 0.88
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/peacock_bass/peacock_bass.tres b/fish/species/peacock_bass/peacock_bass.tres
deleted file mode 100644
index 7cfbb5d..0000000
--- a/fish/species/peacock_bass/peacock_bass.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"peacock_bass"
-display_name = "peacock bass"
-active = true
-catalog_number = 7604
-collection_group = &"Tropical freshwater predators"
-logbook_fact = "peacock bass are aggressive south american cichlids rather than true bass. adults guard nests and hunt fish in warm clear water."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.25
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 25.0
-sell_value_min = 15
-sell_value_max = 36
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/permit/permit.tres b/fish/species/permit/permit.tres
deleted file mode 100644
index f58283c..0000000
--- a/fish/species/permit/permit.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"permit"
-display_name = "permit"
-active = true
-catalog_number = 7404
-collection_group = &"Tropical flats fish"
-logbook_fact = "permit browse tropical flats for crabs and shellfish with rubbery mouths. tall silver bodies carry long sickle-shaped dorsal and anal fins."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.11
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 3.0
-weight_max_lb = 50.0
-sell_value_min = 27
-sell_value_max = 62
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/pickerel_chain/pickerel_chain.tres b/fish/species/pickerel_chain/pickerel_chain.tres
deleted file mode 100644
index db02b7f..0000000
--- a/fish/species/pickerel_chain/pickerel_chain.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"pickerel_chain"
-display_name = "chain pickerel"
-active = true
-catalog_number = 3301
-collection_group = &"Freshwater predators"
-logbook_fact = "chain pickerel hover among vegetation before darting at fish and frogs. a chain-like pattern breaks up their green flanks."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 9.0
-sell_value_min = 6
-sell_value_max = 15
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/pickerel_grass/pickerel_grass.tres b/fish/species/pickerel_grass/pickerel_grass.tres
deleted file mode 100644
index 9ef8952..0000000
--- a/fish/species/pickerel_grass/pickerel_grass.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"pickerel_grass"
-display_name = "grass pickerel"
-active = true
-catalog_number = 3302
-collection_group = &"Freshwater predators"
-logbook_fact = "grass pickerel remain small enough to hunt in narrow, weedy creeks and marshes. dark bars camouflage their slim bodies among stems."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 2.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 1.5
-sell_value_min = 2
-sell_value_max = 6
-sell_value_curve = 0.95
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/pike_northern/pike_northern.tres b/fish/species/pike_northern/pike_northern.tres
deleted file mode 100644
index 05f1ed7..0000000
--- a/fish/species/pike_northern/pike_northern.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"pike_northern"
-display_name = "northern pike"
-active = true
-catalog_number = 3303
-collection_group = &"Freshwater predators"
-logbook_fact = "northern pike are solitary ambush hunters built for sudden acceleration. pale bean-shaped spots cover their long green bodies."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.75
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 30.0
-sell_value_min = 8
-sell_value_max = 24
-sell_value_curve = 0.85
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/pipefish_bay/pipefish_bay.tres b/fish/species/pipefish_bay/pipefish_bay.tres
deleted file mode 100644
index 0436a8a..0000000
--- a/fish/species/pipefish_bay/pipefish_bay.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"pipefish_bay"
-display_name = "bay pipefish"
-active = true
-catalog_number = 601
-collection_group = &"Coastal oddities"
-logbook_fact = "bay pipefish hide upright among seagrass, matching the blades with their slender bodies. males carry developing eggs in a brood pouch."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.9
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.005
-weight_max_lb = 0.04
-sell_value_min = 4
-sell_value_max = 9
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/piranha_red_bellied/piranha_red_bellied.tres b/fish/species/piranha_red_bellied/piranha_red_bellied.tres
deleted file mode 100644
index ebc9fab..0000000
--- a/fish/species/piranha_red_bellied/piranha_red_bellied.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"piranha_red_bellied"
-display_name = "red-bellied piranha"
-active = true
-catalog_number = 7605
-collection_group = &"Tropical freshwater predators"
-logbook_fact = "red-bellied piranhas communicate with barks and drum-like sounds. schools provide protection as they search for fish, insects, and carrion."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 4.0
-sell_value_min = 12
-sell_value_max = 27
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/plaice_european/plaice_european.tres b/fish/species/plaice_european/plaice_european.tres
deleted file mode 100644
index e66f162..0000000
--- a/fish/species/plaice_european/plaice_european.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"plaice_european"
-display_name = "European plaice"
-active = true
-catalog_number = 401
-collection_group = &"Coastal flatfish"
-logbook_fact = "european plaice lie flat on the seafloor with both eyes on the same side. orange spots help distinguish them from neighboring flatfish."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 1.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 10.0
-sell_value_min = 4
-sell_value_max = 10
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/pollock_alaska/pollock_alaska.tres b/fish/species/pollock_alaska/pollock_alaska.tres
deleted file mode 100644
index e9add13..0000000
--- a/fish/species/pollock_alaska/pollock_alaska.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"pollock_alaska"
-display_name = "Alaska pollock"
-active = true
-catalog_number = 1401
-collection_group = &"Cold shelf fish"
-logbook_fact = "alaska pollock form vast schools over cold northern shelves. they feed on plankton and fish while supporting many larger predators."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 2.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 8.0
-sell_value_min = 3
-sell_value_max = 8
-sell_value_curve = 0.92
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/pollock_atlantic/pollock_atlantic.tres b/fish/species/pollock_atlantic/pollock_atlantic.tres
deleted file mode 100644
index 494dd23..0000000
--- a/fish/species/pollock_atlantic/pollock_atlantic.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"pollock_atlantic"
-display_name = "Atlantic pollock"
-active = true
-catalog_number = 1403
-collection_group = &"Cold shelf fish"
-logbook_fact = "atlantic pollock are swift relatives of cod with dark backs and pale lateral lines. adults gather around rocky banks and wrecks."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.9
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 35.0
-sell_value_min = 7
-sell_value_max = 18
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/pomfret_black/fish_pomfret_black.png b/fish/species/pomfret_black/fish_pomfret_black.png
deleted file mode 100644
index 3fd52d8..0000000
Binary files a/fish/species/pomfret_black/fish_pomfret_black.png and /dev/null differ
diff --git a/fish/species/pomfret_black/fish_pomfret_black.png.import b/fish/species/pomfret_black/fish_pomfret_black.png.import
deleted file mode 100644
index c8748a8..0000000
--- a/fish/species/pomfret_black/fish_pomfret_black.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://ch3y6xdo22j1o"
-path="res://.godot/imported/fish_pomfret_black.png-f757c46fb76cb4f9eee68c3143813d3f.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/pomfret_black/fish_pomfret_black.png"
-dest_files=["res://.godot/imported/fish_pomfret_black.png-f757c46fb76cb4f9eee68c3143813d3f.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/pomfret_black/pomfret_black.tres b/fish/species/pomfret_black/pomfret_black.tres
deleted file mode 100644
index a3c9e54..0000000
--- a/fish/species/pomfret_black/pomfret_black.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://bsm1itwx50p1q"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="PomfretBlackAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"pomfret_black"
-display_name = "black pomfret"
-active = true
-catalog_number = 803
-collection_group = &"Coastal schooling fish"
-logbook_fact = "black pomfret have a deep, laterally compressed body that lets them maneuver neatly through open water."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("PomfretBlackAvailability")
-weight_min_lb = 1
-weight_max_lb = 15
-display_scale_min = 0.85
-display_scale_max = 1.8
-display_scale_curve = 0.9
-sell_value_min = 8
-sell_value_max = 18
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/pomfret_chinese/fish_pomfret_chinese.png b/fish/species/pomfret_chinese/fish_pomfret_chinese.png
deleted file mode 100644
index 2280027..0000000
Binary files a/fish/species/pomfret_chinese/fish_pomfret_chinese.png and /dev/null differ
diff --git a/fish/species/pomfret_chinese/fish_pomfret_chinese.png.import b/fish/species/pomfret_chinese/fish_pomfret_chinese.png.import
deleted file mode 100644
index cdf2c5b..0000000
--- a/fish/species/pomfret_chinese/fish_pomfret_chinese.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cijr0gseldncc"
-path="res://.godot/imported/fish_pomfret_chinese.png-a68ccb2e407266ab98980bb3087760ad.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/pomfret_chinese/fish_pomfret_chinese.png"
-dest_files=["res://.godot/imported/fish_pomfret_chinese.png-a68ccb2e407266ab98980bb3087760ad.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/pomfret_chinese/pomfret_chinese.tres b/fish/species/pomfret_chinese/pomfret_chinese.tres
deleted file mode 100644
index e3f236c..0000000
--- a/fish/species/pomfret_chinese/pomfret_chinese.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://di8qq1hek8cpo"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="PomfretChineseAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"pomfret_chinese"
-display_name = "chinese pomfret"
-active = true
-catalog_number = 804
-collection_group = &"Coastal schooling fish"
-logbook_fact = "chinese pomfret are silvery schooling fish with a tall, flattened body and long sickle-shaped fins."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("PomfretChineseAvailability")
-weight_min_lb = 1
-weight_max_lb = 20
-display_scale_min = 0.85
-display_scale_max = 1.9
-display_scale_curve = 0.9
-sell_value_min = 9
-sell_value_max = 20
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/pomfret_golden/fish_pomfret_golden.png b/fish/species/pomfret_golden/fish_pomfret_golden.png
deleted file mode 100644
index 3929da6..0000000
Binary files a/fish/species/pomfret_golden/fish_pomfret_golden.png and /dev/null differ
diff --git a/fish/species/pomfret_golden/fish_pomfret_golden.png.import b/fish/species/pomfret_golden/fish_pomfret_golden.png.import
deleted file mode 100644
index bd77922..0000000
--- a/fish/species/pomfret_golden/fish_pomfret_golden.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://7rjv7k60bv7q"
-path="res://.godot/imported/fish_pomfret_golden.png-932aebb2b7e0fef891368bcd8e2ca1c7.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/pomfret_golden/fish_pomfret_golden.png"
-dest_files=["res://.godot/imported/fish_pomfret_golden.png-932aebb2b7e0fef891368bcd8e2ca1c7.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/pomfret_golden/pomfret_golden.tres b/fish/species/pomfret_golden/pomfret_golden.tres
deleted file mode 100644
index 162a2e7..0000000
--- a/fish/species/pomfret_golden/pomfret_golden.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://b5ivfnfidlu81"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="PomfretGoldenAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"pomfret_golden"
-display_name = "golden pomfret"
-active = true
-catalog_number = 807
-collection_group = &"Coastal schooling fish"
-logbook_fact = "golden pomfret are warm-water swimmers whose golden sheen is most noticeable along the fins and flanks."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.18
-catch_profile = ExtResource("2_profile")
-availability = SubResource("PomfretGoldenAvailability")
-weight_min_lb = 2
-weight_max_lb = 25
-display_scale_min = 0.85
-display_scale_max = 2.1
-display_scale_curve = 0.9
-sell_value_min = 14
-sell_value_max = 30
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/pomfret_white/fish_pomfret_white.png b/fish/species/pomfret_white/fish_pomfret_white.png
deleted file mode 100644
index cf020d5..0000000
Binary files a/fish/species/pomfret_white/fish_pomfret_white.png and /dev/null differ
diff --git a/fish/species/pomfret_white/fish_pomfret_white.png.import b/fish/species/pomfret_white/fish_pomfret_white.png.import
deleted file mode 100644
index b3da3f2..0000000
--- a/fish/species/pomfret_white/fish_pomfret_white.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c3fr0x4e1nfe0"
-path="res://.godot/imported/fish_pomfret_white.png-10c529e20d581d581fc12328c7d152fe.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/pomfret_white/fish_pomfret_white.png"
-dest_files=["res://.godot/imported/fish_pomfret_white.png-10c529e20d581d581fc12328c7d152fe.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/pomfret_white/pomfret_white.tres b/fish/species/pomfret_white/pomfret_white.tres
deleted file mode 100644
index 8652e63..0000000
--- a/fish/species/pomfret_white/pomfret_white.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://6rfka15wc2iy"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="PomfretWhiteAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"pomfret_white"
-display_name = "white pomfret"
-active = true
-catalog_number = 810
-collection_group = &"Coastal schooling fish"
-logbook_fact = "white pomfret gather over coastal grounds and use their compact, deep bodies to turn quickly while feeding."
-available_seasons = 15
-allowed_water_types = 2
-base_catch_weight = 0.45
-catch_profile = ExtResource("2_profile")
-availability = SubResource("PomfretWhiteAvailability")
-weight_max_lb = 10
-display_scale_min = 0.85
-display_scale_max = 1.6
-display_scale_curve = 0.9
-sell_value_min = 5
-sell_value_max = 12
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/pompano_african/pompano_african.tres b/fish/species/pompano_african/pompano_african.tres
deleted file mode 100644
index 598da61..0000000
--- a/fish/species/pompano_african/pompano_african.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"pompano_african"
-display_name = "African pompano"
-active = true
-catalog_number = 3601
-collection_group = &"Large coastal predators"
-logbook_fact = "african pompano develop long streaming fin rays while young. adults become deep-bodied silver hunters around reefs and offshore structure."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.26
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 2.0
-weight_max_lb = 50.0
-sell_value_min = 14
-sell_value_max = 35
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/pompano_florida/pompano_florida.tres b/fish/species/pompano_florida/pompano_florida.tres
deleted file mode 100644
index b21035c..0000000
--- a/fish/species/pompano_florida/pompano_florida.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"pompano_florida"
-display_name = "Florida pompano"
-active = true
-catalog_number = 7402
-collection_group = &"Tropical flats fish"
-logbook_fact = "florida pompano search warm surf for crabs, clams, and sand fleas. short blunt snouts lead into deep, silvery bodies."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.85
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 9.0
-sell_value_min = 7
-sell_value_max = 16
-sell_value_curve = 0.88
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/porcupinefish_spotted/porcupinefish_spotted.tres b/fish/species/porcupinefish_spotted/porcupinefish_spotted.tres
deleted file mode 100644
index 93d5dca..0000000
--- a/fish/species/porcupinefish_spotted/porcupinefish_spotted.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"porcupinefish_spotted"
-display_name = "spotted porcupinefish"
-active = true
-catalog_number = 5607
-collection_group = &"Reef oddities"
-logbook_fact = "spotted porcupinefish inflate until long spines stand outward. fused teeth form a strong beak for crushing shelled prey."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.12
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 7.0
-sell_value_min = 24
-sell_value_max = 54
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/porgy_scup/porgy_scup.tres b/fish/species/porgy_scup/porgy_scup.tres
deleted file mode 100644
index 59dd833..0000000
--- a/fish/species/porgy_scup/porgy_scup.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"porgy_scup"
-display_name = "scup"
-active = true
-catalog_number = 6904
-collection_group = &"Temperate reef fish"
-logbook_fact = "scup use small strong teeth to crush shellfish and other bottom prey. deep silver bodies gather around piers, reefs, and eelgrass."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 2.2
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 5.0
-sell_value_min = 3
-sell_value_max = 8
-sell_value_curve = 0.92
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/pufferfish_guineafowl/pufferfish_guineafowl.tres b/fish/species/pufferfish_guineafowl/pufferfish_guineafowl.tres
deleted file mode 100644
index 754cf4d..0000000
--- a/fish/species/pufferfish_guineafowl/pufferfish_guineafowl.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"pufferfish_guineafowl"
-display_name = "guineafowl puffer"
-active = true
-catalog_number = 5603
-collection_group = &"Reef oddities"
-logbook_fact = "guineafowl puffers swallow water to inflate when threatened. adults may shift from a spotted dark pattern to a vivid yellow form."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 5.0
-sell_value_min = 11
-sell_value_max = 26
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/pumpkinseed/pumpkinseed.tres b/fish/species/pumpkinseed/pumpkinseed.tres
deleted file mode 100644
index 7928e4f..0000000
--- a/fish/species/pumpkinseed/pumpkinseed.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"pumpkinseed"
-display_name = "pumpkinseed"
-active = true
-catalog_number = 3104
-collection_group = &"Freshwater panfish"
-logbook_fact = "pumpkinseeds use compact teeth to crush snails and other hard prey. blue facial lines and an orange gill spot brighten their round bodies."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 3.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 1.3
-sell_value_min = 2
-sell_value_max = 5
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/queenfish_talang/queenfish_talang.tres b/fish/species/queenfish_talang/queenfish_talang.tres
deleted file mode 100644
index d6ee3be..0000000
--- a/fish/species/queenfish_talang/queenfish_talang.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"queenfish_talang"
-display_name = "talang queenfish"
-active = true
-catalog_number = 708
-collection_group = &"Coastal pelagic fish"
-logbook_fact = "talang queenfish chase schooling prey near warm coasts and estuaries. hooked teeth and a deeply forked tail suit their rapid attacks."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 20.0
-sell_value_min = 7
-sell_value_max = 18
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/quillback/quillback.tres b/fish/species/quillback/quillback.tres
deleted file mode 100644
index 2ca6399..0000000
--- a/fish/species/quillback/quillback.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"quillback"
-display_name = "quillback"
-active = true
-catalog_number = 2303
-collection_group = &"Freshwater bottom fish"
-logbook_fact = "quillbacks carry a long first dorsal ray that rises like a quill. small mouths gather insect larvae and other food from river and lake bottoms."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 15.0
-sell_value_min = 6
-sell_value_max = 16
-sell_value_curve = 0.88
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/red_drum/red_drum.tres b/fish/species/red_drum/red_drum.tres
deleted file mode 100644
index 393a002..0000000
--- a/fish/species/red_drum/red_drum.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"red_drum"
-display_name = "red drum"
-active = true
-catalog_number = 6803
-collection_group = &"Temperate estuary"
-logbook_fact = "red drum use powerful tails to patrol surf and estuaries. one or more black spots near the tail may confuse predators about which end is the head."
-available_seasons = 7
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.75
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 45.0
-sell_value_min = 8
-sell_value_max = 23
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/redear_sunfish/redear_sunfish.tres b/fish/species/redear_sunfish/redear_sunfish.tres
deleted file mode 100644
index 6078d20..0000000
--- a/fish/species/redear_sunfish/redear_sunfish.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"redear_sunfish"
-display_name = "redear sunfish"
-active = true
-catalog_number = 3105
-collection_group = &"Freshwater panfish"
-logbook_fact = "redear sunfish specialize in snails, crushing shells with strong throat teeth. a red or orange margin marks the dark flap behind the gill."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 1.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 3.0
-sell_value_min = 4
-sell_value_max = 9
-sell_value_curve = 0.95
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/redhorse_golden/redhorse_golden.tres b/fish/species/redhorse_golden/redhorse_golden.tres
deleted file mode 100644
index 5ba103b..0000000
--- a/fish/species/redhorse_golden/redhorse_golden.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"redhorse_golden"
-display_name = "golden redhorse"
-active = true
-catalog_number = 2302
-collection_group = &"Freshwater bottom fish"
-logbook_fact = "golden redhorses probe flowing river bottoms with fleshy, downward-facing mouths. coppery scales and reddish fins give them a warm glow."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.9
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 8.0
-sell_value_min = 6
-sell_value_max = 14
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/roach_common/roach_common.tres b/fish/species/roach_common/roach_common.tres
deleted file mode 100644
index 8814f1c..0000000
--- a/fish/species/roach_common/roach_common.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"roach_common"
-display_name = "common roach"
-active = true
-catalog_number = 2504
-collection_group = &"Freshwater coarse fish"
-logbook_fact = "common roach thrive in many lakes and rivers, often schooling near vegetation. red fins and a red-orange eye brighten their silver bodies."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 2.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 4.0
-sell_value_min = 2
-sell_value_max = 6
-sell_value_curve = 0.95
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/rockfish_black/rockfish_black.tres b/fish/species/rockfish_black/rockfish_black.tres
deleted file mode 100644
index f26a41c..0000000
--- a/fish/species/rockfish_black/rockfish_black.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"rockfish_black"
-display_name = "black rockfish"
-active = true
-catalog_number = 6901
-collection_group = &"Temperate reef fish"
-logbook_fact = "black rockfish gather above reefs and kelp along the north pacific coast. like other rockfish, they give birth to live young."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 1.7
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 10.0
-sell_value_min = 4
-sell_value_max = 11
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/rockfish_canary/rockfish_canary.tres b/fish/species/rockfish_canary/rockfish_canary.tres
deleted file mode 100644
index 1e65d72..0000000
--- a/fish/species/rockfish_canary/rockfish_canary.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"rockfish_canary"
-display_name = "canary rockfish"
-active = true
-catalog_number = 6903
-collection_group = &"Temperate reef fish"
-logbook_fact = "canary rockfish glow orange-yellow over deep pacific reefs. these long-lived fish mature slowly and give birth to live young."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.7
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 12.0
-sell_value_min = 7
-sell_value_max = 17
-sell_value_curve = 0.88
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/rockfish_yelloweye/rockfish_yelloweye.tres b/fish/species/rockfish_yelloweye/rockfish_yelloweye.tres
deleted file mode 100644
index 89aab8e..0000000
--- a/fish/species/rockfish_yelloweye/rockfish_yelloweye.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"rockfish_yelloweye"
-display_name = "yelloweye rockfish"
-active = true
-catalog_number = 6907
-collection_group = &"Temperate reef fish"
-logbook_fact = "yelloweye rockfish can live for more than a century on deep pacific reefs. bright golden eyes stand out against orange-red bodies."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.09
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 2.0
-weight_max_lb = 35.0
-sell_value_min = 28
-sell_value_max = 64
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/roosterfish/roosterfish.tres b/fish/species/roosterfish/roosterfish.tres
deleted file mode 100644
index 388dd70..0000000
--- a/fish/species/roosterfish/roosterfish.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"roosterfish"
-display_name = "roosterfish"
-active = true
-catalog_number = 7201
-collection_group = &"Tropical coastal predators"
-logbook_fact = "roosterfish raise seven long dorsal spines while chasing prey through tropical surf. dark bands cross their sleek silver bodies."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.11
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 5.0
-weight_max_lb = 100.0
-sell_value_min = 30
-sell_value_max = 70
-sell_value_curve = 0.82
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/rudd/rudd.tres b/fish/species/rudd/rudd.tres
deleted file mode 100644
index ae031ac..0000000
--- a/fish/species/rudd/rudd.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"rudd"
-display_name = "rudd"
-active = true
-catalog_number = 2508
-collection_group = &"Freshwater coarse fish"
-logbook_fact = "rudd feed near the surface with an upturned mouth and red fins. golden-green bodies disappear easily among warm, weedy shallows."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 2.4
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 4.5
-sell_value_min = 3
-sell_value_max = 7
-sell_value_curve = 0.95
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sablefish/sablefish.tres b/fish/species/sablefish/sablefish.tres
deleted file mode 100644
index e481e52..0000000
--- a/fish/species/sablefish/sablefish.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sablefish"
-display_name = "sablefish"
-active = true
-catalog_number = 1605
-collection_group = &"Deep shelf fish"
-logbook_fact = "sablefish roam deep north pacific slopes and can live for many decades. soft dark bodies conceal rich, oil-filled flesh."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.22
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 2.0
-weight_max_lb = 40.0
-sell_value_min = 15
-sell_value_max = 36
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sailfish/fish_sailfish.png b/fish/species/sailfish/fish_sailfish.png
deleted file mode 100644
index 32b447e..0000000
Binary files a/fish/species/sailfish/fish_sailfish.png and /dev/null differ
diff --git a/fish/species/sailfish/fish_sailfish.png.import b/fish/species/sailfish/fish_sailfish.png.import
deleted file mode 100644
index ee25011..0000000
--- a/fish/species/sailfish/fish_sailfish.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://8rlcnfqx8or0"
-path="res://.godot/imported/fish_sailfish.png-cc20ed43042eb2d6867954aa1a1772da.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/sailfish/fish_sailfish.png"
-dest_files=["res://.godot/imported/fish_sailfish.png-cc20ed43042eb2d6867954aa1a1772da.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/sailfish/sailfish.tres b/fish/species/sailfish/sailfish.tres
deleted file mode 100644
index 98e958d..0000000
--- a/fish/species/sailfish/sailfish.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://b3auce81mglkx"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="SailfishAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sailfish"
-display_name = "sailfish"
-active = true
-catalog_number = 4603
-collection_group = &"Oceanic billfish"
-logbook_fact = "sailfish raise their enormous dorsal fin while herding baitfish. they are among the fastest fish in the sea."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("SailfishAvailability")
-weight_min_lb = 30
-weight_max_lb = 200
-display_scale_min = 0.85
-display_scale_max = 2.6
-display_scale_curve = 0.9
-sell_value_min = 38
-sell_value_max = 78
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/salmon_atlantic/fish_salmon_atlantic.png b/fish/species/salmon_atlantic/fish_salmon_atlantic.png
deleted file mode 100644
index a28192e..0000000
Binary files a/fish/species/salmon_atlantic/fish_salmon_atlantic.png and /dev/null differ
diff --git a/fish/species/salmon_atlantic/fish_salmon_atlantic.png.import b/fish/species/salmon_atlantic/fish_salmon_atlantic.png.import
deleted file mode 100644
index 36fb242..0000000
--- a/fish/species/salmon_atlantic/fish_salmon_atlantic.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://ddbclfu5wtd5r"
-path="res://.godot/imported/fish_salmon_atlantic.png-e11e70a37a78573bfb86a21b48ae319b.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/salmon_atlantic/fish_salmon_atlantic.png"
-dest_files=["res://.godot/imported/fish_salmon_atlantic.png-e11e70a37a78573bfb86a21b48ae319b.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/salmon_atlantic/salmon_atlantic.tres b/fish/species/salmon_atlantic/salmon_atlantic.tres
deleted file mode 100644
index 1c77325..0000000
--- a/fish/species/salmon_atlantic/salmon_atlantic.tres
+++ /dev/null
@@ -1,35 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://b5ewuclck75yr"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/4401.png" id="4_texture"]
-
-[sub_resource type="Resource" id="AtlanticSalmonAvailability"]
-script = ExtResource("3_availability_script")
-preferred_bait_tags = Array[StringName]([&"minnow"])
-preferred_bait_weight_multiplier = 1.4
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"salmon_atlantic"
-display_name = "atlantic salmon"
-active = true
-catalog_number = 4401
-collection_group = &"Ocean-run salmon"
-logbook_fact = "atlantic salmon can return from the ocean to the river where they hatched. unlike many salmon, some survive to make the trip again."
-available_seasons = 5
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.65
-catch_profile = ExtResource("2_profile")
-availability = SubResource("AtlanticSalmonAvailability")
-weight_min_lb = 8.0
-weight_max_lb = 40.0
-display_scale_min = 1.05
-display_scale_max = 1.9
-display_scale_curve = 0.85
-sell_value_min = 20
-sell_value_max = 42
-sell_value_curve = 0.85
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/salmon_chum/fish_salmon_chum.png b/fish/species/salmon_chum/fish_salmon_chum.png
deleted file mode 100644
index 25a94eb..0000000
Binary files a/fish/species/salmon_chum/fish_salmon_chum.png and /dev/null differ
diff --git a/fish/species/salmon_chum/fish_salmon_chum.png.import b/fish/species/salmon_chum/fish_salmon_chum.png.import
deleted file mode 100644
index 587b83b..0000000
--- a/fish/species/salmon_chum/fish_salmon_chum.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dqjkmd6l1a2i4"
-path="res://.godot/imported/fish_salmon_chum.png-78c86aca1218255db2c488a5c22e4409.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/salmon_chum/fish_salmon_chum.png"
-dest_files=["res://.godot/imported/fish_salmon_chum.png-78c86aca1218255db2c488a5c22e4409.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/salmon_chum/salmon_chum.tres b/fish/species/salmon_chum/salmon_chum.tres
deleted file mode 100644
index 8fcabd0..0000000
--- a/fish/species/salmon_chum/salmon_chum.tres
+++ /dev/null
@@ -1,35 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://dhddj6grq203c"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="ChumSalmonAvailability"]
-script = ExtResource("3_availability_script")
-preferred_bait_tags = Array[StringName]([&"minnow"])
-preferred_bait_weight_multiplier = 1.4
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"salmon_chum"
-display_name = "chum salmon"
-active = true
-catalog_number = 4402
-collection_group = &"Ocean-run salmon"
-logbook_fact = "chum salmon travel immense distances and develop striking bars when they return to fresh water to spawn."
-available_seasons = 4
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 2.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("ChumSalmonAvailability")
-weight_min_lb = 6.0
-weight_max_lb = 25.0
-display_scale_min = 1.0
-display_scale_max = 1.75
-display_scale_curve = 0.9
-sell_value_min = 10
-sell_value_max = 22
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/salmon_coho/fish_salmon_coho.png b/fish/species/salmon_coho/fish_salmon_coho.png
deleted file mode 100644
index a7fe5f0..0000000
Binary files a/fish/species/salmon_coho/fish_salmon_coho.png and /dev/null differ
diff --git a/fish/species/salmon_coho/fish_salmon_coho.png.import b/fish/species/salmon_coho/fish_salmon_coho.png.import
deleted file mode 100644
index 1cd3254..0000000
--- a/fish/species/salmon_coho/fish_salmon_coho.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c4c4e43lbr88c"
-path="res://.godot/imported/fish_salmon_coho.png-fc57b7ff3c0ac63ccfec6db9cec1dedb.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/salmon_coho/fish_salmon_coho.png"
-dest_files=["res://.godot/imported/fish_salmon_coho.png-fc57b7ff3c0ac63ccfec6db9cec1dedb.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/salmon_coho/salmon_coho.tres b/fish/species/salmon_coho/salmon_coho.tres
deleted file mode 100644
index b1160b1..0000000
--- a/fish/species/salmon_coho/salmon_coho.tres
+++ /dev/null
@@ -1,35 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://nw1k2c7vtqtt"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="CohoSalmonAvailability"]
-script = ExtResource("3_availability_script")
-preferred_bait_tags = Array[StringName]([&"minnow"])
-preferred_bait_weight_multiplier = 1.4
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"salmon_coho"
-display_name = "coho salmon"
-active = true
-catalog_number = 4403
-collection_group = &"Ocean-run salmon"
-logbook_fact = "coho salmon are agile hunters known for powerful leaps. young coho may spend more than a year growing in streams."
-available_seasons = 12
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 1.4
-catch_profile = ExtResource("2_profile")
-availability = SubResource("CohoSalmonAvailability")
-weight_min_lb = 5.0
-weight_max_lb = 30.0
-display_scale_min = 1.0
-display_scale_max = 1.8
-display_scale_curve = 0.88
-sell_value_min = 14
-sell_value_max = 28
-sell_value_curve = 0.88
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/salmon_pink/fish_salmon_pink.png b/fish/species/salmon_pink/fish_salmon_pink.png
deleted file mode 100644
index 1fe2554..0000000
Binary files a/fish/species/salmon_pink/fish_salmon_pink.png and /dev/null differ
diff --git a/fish/species/salmon_pink/fish_salmon_pink.png.import b/fish/species/salmon_pink/fish_salmon_pink.png.import
deleted file mode 100644
index caf4c74..0000000
--- a/fish/species/salmon_pink/fish_salmon_pink.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bk27bsdrtlqap"
-path="res://.godot/imported/fish_salmon_pink.png-0c9ca3bc540b868c12fe32b1cdc59249.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/salmon_pink/fish_salmon_pink.png"
-dest_files=["res://.godot/imported/fish_salmon_pink.png-0c9ca3bc540b868c12fe32b1cdc59249.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/salmon_pink/salmon_pink.tres b/fish/species/salmon_pink/salmon_pink.tres
deleted file mode 100644
index bdfa32e..0000000
--- a/fish/species/salmon_pink/salmon_pink.tres
+++ /dev/null
@@ -1,35 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://cq0ikmuba6j73"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="PinkSalmonAvailability"]
-script = ExtResource("3_availability_script")
-preferred_bait_tags = Array[StringName]([&"minnow"])
-preferred_bait_weight_multiplier = 1.4
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"salmon_pink"
-display_name = "pink salmon"
-active = true
-catalog_number = 4404
-collection_group = &"Ocean-run salmon"
-logbook_fact = "pink salmon usually follow a strict two-year life cycle. spawning males develop the hump that inspired their nickname."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 3.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("PinkSalmonAvailability")
-weight_min_lb = 3.0
-weight_max_lb = 12.0
-display_scale_min = 0.9
-display_scale_max = 1.55
-display_scale_curve = 0.92
-sell_value_min = 8
-sell_value_max = 15
-sell_value_curve = 0.92
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/salmon_sockeye/fish_salmon_sockeye.png b/fish/species/salmon_sockeye/fish_salmon_sockeye.png
deleted file mode 100644
index 0569084..0000000
Binary files a/fish/species/salmon_sockeye/fish_salmon_sockeye.png and /dev/null differ
diff --git a/fish/species/salmon_sockeye/fish_salmon_sockeye.png.import b/fish/species/salmon_sockeye/fish_salmon_sockeye.png.import
deleted file mode 100644
index 71a8856..0000000
--- a/fish/species/salmon_sockeye/fish_salmon_sockeye.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cmokm3sra4ai8"
-path="res://.godot/imported/fish_salmon_sockeye.png-99d566111b5cad1e6ae4702b6f9128fd.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/salmon_sockeye/fish_salmon_sockeye.png"
-dest_files=["res://.godot/imported/fish_salmon_sockeye.png-99d566111b5cad1e6ae4702b6f9128fd.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/salmon_sockeye/salmon_sockeye.tres b/fish/species/salmon_sockeye/salmon_sockeye.tres
deleted file mode 100644
index 7bd4b1a..0000000
--- a/fish/species/salmon_sockeye/salmon_sockeye.tres
+++ /dev/null
@@ -1,35 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://bf2ej6rgypsqx"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="SockeyeSalmonAvailability"]
-script = ExtResource("3_availability_script")
-preferred_bait_tags = Array[StringName]([&"minnow"])
-preferred_bait_weight_multiplier = 1.4
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"salmon_sockeye"
-display_name = "sockeye salmon"
-active = true
-catalog_number = 4405
-collection_group = &"Ocean-run salmon"
-logbook_fact = "sockeye salmon feed heavily on plankton at sea. adults turn vivid red as they return inland to spawn."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 1.6
-catch_profile = ExtResource("2_profile")
-availability = SubResource("SockeyeSalmonAvailability")
-weight_min_lb = 4.0
-weight_max_lb = 15.0
-display_scale_min = 0.95
-display_scale_max = 1.6
-display_scale_curve = 0.9
-sell_value_min = 12
-sell_value_max = 24
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/sand_dollar/sand_dollar.tres b/fish/species/sand_dollar/sand_dollar.tres
deleted file mode 100644
index f780203..0000000
--- a/fish/species/sand_dollar/sand_dollar.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sand_dollar"
-display_name = "sand dollar"
-active = true
-catalog_number = 1904
-collection_group = &"Echinoderms"
-logbook_fact = "sand dollars burrow just beneath sandy seafloors and filter tiny food particles. the familiar star pattern marks their five-part body plan."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.01
-weight_max_lb = 0.2
-sell_value_min = 4
-sell_value_max = 10
-sell_value_curve = 0.98
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sand_lance_american/sand_lance_american.tres b/fish/species/sand_lance_american/sand_lance_american.tres
deleted file mode 100644
index 18f16be..0000000
--- a/fish/species/sand_lance_american/sand_lance_american.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/801.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sand_lance_american"
-display_name = "American sand lance"
-active = true
-catalog_number = 801
-collection_group = &"Coastal schooling fish"
-logbook_fact = "american sand lance dive headfirst into loose sand when danger approaches. immense schools carry energy from plankton to larger predators."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 3.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.02
-weight_max_lb = 0.4
-sell_value_min = 1
-sell_value_max = 3
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sardine_european/sardine_european.tres b/fish/species/sardine_european/sardine_european.tres
deleted file mode 100644
index 54008f8..0000000
--- a/fish/species/sardine_european/sardine_european.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/806.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sardine_european"
-display_name = "European pilchard"
-active = true
-catalog_number = 806
-collection_group = &"Coastal schooling fish"
-logbook_fact = "european pilchards form shimmering schools along atlantic and mediterranean coasts. they feed mainly on plankton near the surface."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 3.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 0.6
-sell_value_min = 2
-sell_value_max = 4
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sardine_pacific/sardine_pacific.tres b/fish/species/sardine_pacific/sardine_pacific.tres
deleted file mode 100644
index bea772d..0000000
--- a/fish/species/sardine_pacific/sardine_pacific.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sardine_pacific"
-display_name = "Pacific sardine"
-active = true
-catalog_number = 809
-collection_group = &"Coastal schooling fish"
-logbook_fact = "pacific sardines follow cool, productive water in enormous schools. their movements shift as currents and plankton supplies change."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 3.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 0.5
-sell_value_min = 2
-sell_value_max = 4
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sauger/fish_sauger.png b/fish/species/sauger/fish_sauger.png
deleted file mode 100644
index 188be07..0000000
Binary files a/fish/species/sauger/fish_sauger.png and /dev/null differ
diff --git a/fish/species/sauger/fish_sauger.png.import b/fish/species/sauger/fish_sauger.png.import
deleted file mode 100644
index 02110fe..0000000
--- a/fish/species/sauger/fish_sauger.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://8k7qkk4juciy"
-path="res://.godot/imported/fish_sauger.png-9b3abc6f8648ebbd51a56abeedbb4826.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/sauger/fish_sauger.png"
-dest_files=["res://.godot/imported/fish_sauger.png-9b3abc6f8648ebbd51a56abeedbb4826.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/sauger/sauger.tres b/fish/species/sauger/sauger.tres
deleted file mode 100644
index 3248bcb..0000000
--- a/fish/species/sauger/sauger.tres
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://suhm4yemc0vf"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="SaugerAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"river"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sauger"
-display_name = "sauger"
-active = true
-catalog_number = 3304
-collection_group = &"Freshwater predators"
-logbook_fact = "sauger favor turbid rivers and reservoirs. their mottled backs blend into the dim, shifting light near the bottom."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.35
-catch_profile = ExtResource("2_profile")
-availability = SubResource("SaugerAvailability")
-weight_max_lb = 8
-display_scale_min = 0.85
-display_scale_max = 1.4
-display_scale_curve = 0.9
-sell_value_min = 6
-sell_value_max = 12
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/saugeye/fish_saugeye.png b/fish/species/saugeye/fish_saugeye.png
deleted file mode 100644
index d106625..0000000
Binary files a/fish/species/saugeye/fish_saugeye.png and /dev/null differ
diff --git a/fish/species/saugeye/fish_saugeye.png.import b/fish/species/saugeye/fish_saugeye.png.import
deleted file mode 100644
index 6011e7b..0000000
--- a/fish/species/saugeye/fish_saugeye.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://lom25y0eqcw3"
-path="res://.godot/imported/fish_saugeye.png-638130dc7e3f534cf48d3da80fc2c3df.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/saugeye/fish_saugeye.png"
-dest_files=["res://.godot/imported/fish_saugeye.png-638130dc7e3f534cf48d3da80fc2c3df.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/saugeye/saugeye.tres b/fish/species/saugeye/saugeye.tres
deleted file mode 100644
index d6f09cf..0000000
--- a/fish/species/saugeye/saugeye.tres
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://oecfrxf7vgh0"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="SaugeyeAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"lake"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"saugeye"
-display_name = "saugeye"
-active = true
-catalog_number = 3305
-collection_group = &"Freshwater predators"
-logbook_fact = "saugeye are a fertile hybrid of sauger and walleye. they combine traits from both parent species and often grow quickly."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.2
-catch_profile = ExtResource("2_profile")
-availability = SubResource("SaugeyeAvailability")
-weight_max_lb = 12
-display_scale_min = 0.85
-display_scale_max = 1.6
-display_scale_curve = 0.9
-sell_value_min = 9
-sell_value_max = 18
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/sawfish_largetooth/sawfish_largetooth.tres b/fish/species/sawfish_largetooth/sawfish_largetooth.tres
deleted file mode 100644
index ac4680b..0000000
--- a/fish/species/sawfish_largetooth/sawfish_largetooth.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sawfish_largetooth"
-display_name = "largetooth sawfish"
-active = true
-catalog_number = 5103
-collection_group = &"Rays and skates"
-logbook_fact = "largetooth sawfish detect hidden prey with a long sensory rostrum. a swift sideways sweep stuns fish and exposes animals buried in mud."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.035
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 25.0
-weight_max_lb = 1300.0
-sell_value_min = 58
-sell_value_max = 135
-sell_value_curve = 0.78
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/scorpionfish_red/scorpionfish_red.tres b/fish/species/scorpionfish_red/scorpionfish_red.tres
deleted file mode 100644
index baf9046..0000000
--- a/fish/species/scorpionfish_red/scorpionfish_red.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"scorpionfish_red"
-display_name = "red scorpionfish"
-active = true
-catalog_number = 5202
-collection_group = &"Reef ambush fish"
-logbook_fact = "red scorpionfish vanish against rocky reefs through mottled color and skin flaps. venomous dorsal spines punish careless predators."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 7.0
-sell_value_min = 11
-sell_value_max = 25
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sea_bass_black/sea_bass_black.tres b/fish/species/sea_bass_black/sea_bass_black.tres
deleted file mode 100644
index 1c0eabd..0000000
--- a/fish/species/sea_bass_black/sea_bass_black.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sea_bass_black"
-display_name = "black sea bass"
-active = true
-catalog_number = 6902
-collection_group = &"Temperate reef fish"
-logbook_fact = "black sea bass shelter around reefs, piers, and wrecks. many begin life as female and later become male."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 1.7
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 8.0
-sell_value_min = 4
-sell_value_max = 10
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sea_bass_chilean/sea_bass_chilean.tres b/fish/species/sea_bass_chilean/sea_bass_chilean.tres
deleted file mode 100644
index dd9afac..0000000
--- a/fish/species/sea_bass_chilean/sea_bass_chilean.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sea_bass_chilean"
-display_name = "Patagonian toothfish"
-active = true
-catalog_number = 1604
-collection_group = &"Deep shelf fish"
-logbook_fact = "patagonian toothfish patrol cold, deep southern water with large canine teeth. natural antifreeze proteins protect their blood from freezing."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.09
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 10.0
-weight_max_lb = 220.0
-sell_value_min = 32
-sell_value_max = 78
-sell_value_curve = 0.8
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sea_cucumber_giant_california/sea_cucumber_giant_california.tres b/fish/species/sea_cucumber_giant_california/sea_cucumber_giant_california.tres
deleted file mode 100644
index bc1ca41..0000000
--- a/fish/species/sea_cucumber_giant_california/sea_cucumber_giant_california.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sea_cucumber_giant_california"
-display_name = "giant California sea cucumber"
-active = true
-catalog_number = 1902
-collection_group = &"Echinoderms"
-logbook_fact = "giant california sea cucumbers sweep organic particles from the seafloor with branching tentacles. soft tube feet anchor their long bodies."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.24
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 5.0
-sell_value_min = 13
-sell_value_max = 29
-sell_value_curve = 0.93
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sea_otter/sea_otter.tres b/fish/species/sea_otter/sea_otter.tres
deleted file mode 100644
index c5f4bfc..0000000
--- a/fish/species/sea_otter/sea_otter.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sea_otter"
-display_name = "sea otter"
-active = true
-catalog_number = 503
-collection_group = &"Coastal marine mammals"
-logbook_fact = "sea otters float on their backs while opening shellfish with stones. dense fur traps insulating air because they lack a thick blubber layer."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.02
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 30.0
-weight_max_lb = 100.0
-sell_value_min = 55
-sell_value_max = 125
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sea_star_crown_of_thorns/sea_star_crown_of_thorns.tres b/fish/species/sea_star_crown_of_thorns/sea_star_crown_of_thorns.tres
deleted file mode 100644
index 5e73beb..0000000
--- a/fish/species/sea_star_crown_of_thorns/sea_star_crown_of_thorns.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sea_star_crown_of_thorns"
-display_name = "crown-of-thorns sea star"
-active = true
-catalog_number = 1901
-collection_group = &"Echinoderms"
-logbook_fact = "crown-of-thorns sea stars crawl over reefs and digest coral tissue outside the body. venomous spines discourage many predators."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 7.0
-sell_value_min = 23
-sell_value_max = 52
-sell_value_curve = 0.91
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sea_star_sunflower/sea_star_sunflower.tres b/fish/species/sea_star_sunflower/sea_star_sunflower.tres
deleted file mode 100644
index 6bf1dc2..0000000
--- a/fish/species/sea_star_sunflower/sea_star_sunflower.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sea_star_sunflower"
-display_name = "sunflower sea star"
-active = true
-catalog_number = 1905
-collection_group = &"Echinoderms"
-logbook_fact = "sunflower sea stars move quickly on thousands of tube feet and may carry many arms. they hunt urchins, clams, and other bottom animals."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.25
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 13.0
-sell_value_min = 14
-sell_value_max = 32
-sell_value_curve = 0.89
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/seadragon_leafy/seadragon_leafy.tres b/fish/species/seadragon_leafy/seadragon_leafy.tres
deleted file mode 100644
index 1481d73..0000000
--- a/fish/species/seadragon_leafy/seadragon_leafy.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"seadragon_leafy"
-display_name = "leafy seadragon"
-active = true
-catalog_number = 6301
-collection_group = &"Seahorses and seadragons"
-logbook_fact = "leafy seadragons drift among seaweed with elaborate skin lobes that resemble leaves. males carry eggs beneath the tail."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 0.5
-sell_value_min = 24
-sell_value_max = 55
-sell_value_curve = 0.94
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/seadragon_weedy/seadragon_weedy.tres b/fish/species/seadragon_weedy/seadragon_weedy.tres
deleted file mode 100644
index bc1768b..0000000
--- a/fish/species/seadragon_weedy/seadragon_weedy.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"seadragon_weedy"
-display_name = "weedy seadragon"
-active = true
-catalog_number = 6302
-collection_group = &"Seahorses and seadragons"
-logbook_fact = "weedy seadragons rely on slender bodies and small leaf-like lobes for camouflage. males brood rows of eggs on the underside of the tail."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.22
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 0.6
-sell_value_min = 16
-sell_value_max = 38
-sell_value_curve = 0.93
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/seahorse_lined/seahorse_lined.tres b/fish/species/seahorse_lined/seahorse_lined.tres
deleted file mode 100644
index 0121fab..0000000
--- a/fish/species/seahorse_lined/seahorse_lined.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"seahorse_lined"
-display_name = "lined seahorse"
-active = true
-catalog_number = 602
-collection_group = &"Coastal oddities"
-logbook_fact = "lined seahorses anchor to grass and coral with curling tails. the male broods eggs in a pouch and releases fully formed young."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.28
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.005
-weight_max_lb = 0.05
-sell_value_min = 9
-sell_value_max = 20
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/seal_harbor/seal_harbor.tres b/fish/species/seal_harbor/seal_harbor.tres
deleted file mode 100644
index 5689a14..0000000
--- a/fish/species/seal_harbor/seal_harbor.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"seal_harbor"
-display_name = "harbor seal"
-active = true
-catalog_number = 502
-collection_group = &"Coastal marine mammals"
-logbook_fact = "harbor seals rest on rocky shores and sandbars between hunting trips. sensitive whiskers can trace the water trails left by moving fish."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.018
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 120.0
-weight_max_lb = 370.0
-sell_value_min = 60
-sell_value_max = 140
-sell_value_curve = 0.79
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/shark_blacktip/shark_blacktip.tres b/fish/species/shark_blacktip/shark_blacktip.tres
deleted file mode 100644
index e7fc4e6..0000000
--- a/fish/species/shark_blacktip/shark_blacktip.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"shark_blacktip"
-display_name = "blacktip shark"
-active = true
-catalog_number = 901
-collection_group = &"Coastal sharks"
-logbook_fact = "blacktip sharks are named for the dark markings on their fins. they hunt schooling fish in warm shallows and sometimes leap clear of the water."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 15.0
-weight_max_lb = 150.0
-sell_value_min = 32
-sell_value_max = 75
-sell_value_curve = 0.8
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/shark_great_hammerhead/shark_great_hammerhead.tres b/fish/species/shark_great_hammerhead/shark_great_hammerhead.tres
deleted file mode 100644
index a9b612e..0000000
--- a/fish/species/shark_great_hammerhead/shark_great_hammerhead.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"shark_great_hammerhead"
-display_name = "great hammerhead shark"
-active = true
-catalog_number = 902
-collection_group = &"Coastal sharks"
-logbook_fact = "great hammerheads sweep their wide heads over the bottom while searching for rays. widely spaced senses help pinpoint hidden prey."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.035
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 50.0
-weight_max_lb = 1000.0
-sell_value_min = 62
-sell_value_max = 145
-sell_value_curve = 0.78
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/shark_great_white/shark_great_white.tres b/fish/species/shark_great_white/shark_great_white.tres
deleted file mode 100644
index afd1cd0..0000000
--- a/fish/species/shark_great_white/shark_great_white.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"shark_great_white"
-display_name = "great white shark"
-active = true
-catalog_number = 903
-collection_group = &"Coastal sharks"
-logbook_fact = "great white sharks conserve heat in their swimming muscles while crossing cool seas. rows of serrated teeth are replaced throughout life."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.025
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 100.0
-weight_max_lb = 1000.0
-sell_value_min = 70
-sell_value_max = 150
-sell_value_curve = 0.76
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/shark_tiger/shark_tiger.tres b/fish/species/shark_tiger/shark_tiger.tres
deleted file mode 100644
index 72e2577..0000000
--- a/fish/species/shark_tiger/shark_tiger.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"shark_tiger"
-display_name = "tiger shark"
-active = true
-catalog_number = 904
-collection_group = &"Coastal sharks"
-logbook_fact = "tiger sharks carry dark vertical bars that fade as they mature. broad jaws let them investigate an unusually varied range of prey."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.06
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 50.0
-weight_max_lb = 1000.0
-sell_value_min = 50
-sell_value_max = 120
-sell_value_curve = 0.8
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/shark_whale/shark_whale.tres b/fish/species/shark_whale/shark_whale.tres
deleted file mode 100644
index 81739db..0000000
--- a/fish/species/shark_whale/shark_whale.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"shark_whale"
-display_name = "whale shark"
-active = true
-catalog_number = 905
-collection_group = &"Coastal sharks"
-logbook_fact = "whale sharks are enormous filter feeders covered in unique spot patterns. they cruise with open mouths to collect plankton and tiny fish."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.025
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 500.0
-weight_max_lb = 45000.0
-sell_value_min = 70
-sell_value_max = 160
-sell_value_curve = 0.76
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sheepshead/sheepshead.tres b/fish/species/sheepshead/sheepshead.tres
deleted file mode 100644
index d4b8cfa..0000000
--- a/fish/species/sheepshead/sheepshead.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/6905.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sheepshead"
-display_name = "sheepshead"
-active = true
-catalog_number = 6905
-collection_group = &"Temperate reef fish"
-logbook_fact = "sheepshead carry flat human-like front teeth for biting barnacles and crabs. dark bars cross their high silver bodies."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.75
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 20.0
-sell_value_min = 7
-sell_value_max = 18
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/shiner_common/shiner_common.tres b/fish/species/shiner_common/shiner_common.tres
deleted file mode 100644
index 562daf6..0000000
--- a/fish/species/shiner_common/shiner_common.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"shiner_common"
-display_name = "common shiner"
-active = true
-catalog_number = 2801
-collection_group = &"Freshwater minnows"
-logbook_fact = "common shiners flash silver in clear streams and small rivers. breeding males develop rosy fins and a blue-violet sheen."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 4.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.03
-weight_max_lb = 0.3
-sell_value_min = 1
-sell_value_max = 3
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/shiner_emerald/shiner_emerald.tres b/fish/species/shiner_emerald/shiner_emerald.tres
deleted file mode 100644
index eeb3653..0000000
--- a/fish/species/shiner_emerald/shiner_emerald.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"shiner_emerald"
-display_name = "emerald shiner"
-active = true
-catalog_number = 2803
-collection_group = &"Freshwater minnows"
-logbook_fact = "emerald shiners form bright surface schools in large lakes and rivers. slender bodies feed on plankton and drifting insects."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 3.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.03
-weight_max_lb = 0.25
-sell_value_min = 1
-sell_value_max = 3
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/shiner_golden/shiner_golden.tres b/fish/species/shiner_golden/shiner_golden.tres
deleted file mode 100644
index 5ab924f..0000000
--- a/fish/species/shiner_golden/shiner_golden.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"shiner_golden"
-display_name = "golden shiner"
-active = true
-catalog_number = 2806
-collection_group = &"Freshwater minnows"
-logbook_fact = "golden shiners school around dense vegetation in warm water. an upturned mouth takes insects and plankton from near the surface."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 3.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 1.0
-sell_value_min = 2
-sell_value_max = 4
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/shrimp_peacock_mantis/shrimp_peacock_mantis.tres b/fish/species/shrimp_peacock_mantis/shrimp_peacock_mantis.tres
deleted file mode 100644
index daf7662..0000000
--- a/fish/species/shrimp_peacock_mantis/shrimp_peacock_mantis.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"shrimp_peacock_mantis"
-display_name = "peacock mantis shrimp"
-active = true
-catalog_number = 3804
-collection_group = &"Lobsters and shrimp"
-logbook_fact = "peacock mantis shrimp strike hard-shelled prey with club-like limbs. complex eyes move independently and detect colors beyond human vision."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 0.4
-sell_value_min = 22
-sell_value_max = 50
-sell_value_curve = 0.96
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/shrimp_skunk_cleaner/shrimp_skunk_cleaner.tres b/fish/species/shrimp_skunk_cleaner/shrimp_skunk_cleaner.tres
deleted file mode 100644
index fdd6730..0000000
--- a/fish/species/shrimp_skunk_cleaner/shrimp_skunk_cleaner.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"shrimp_skunk_cleaner"
-display_name = "skunk cleaner shrimp"
-active = true
-catalog_number = 3805
-collection_group = &"Lobsters and shrimp"
-logbook_fact = "skunk cleaner shrimp advertise at reef cleaning stations with bright white antennae. visiting fish pause while parasites are picked away."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.75
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.01
-weight_max_lb = 0.15
-sell_value_min = 5
-sell_value_max = 11
-sell_value_curve = 0.98
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/skate_barndoor/skate_barndoor.tres b/fish/species/skate_barndoor/skate_barndoor.tres
deleted file mode 100644
index 5ccb967..0000000
--- a/fish/species/skate_barndoor/skate_barndoor.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"skate_barndoor"
-display_name = "barndoor skate"
-active = true
-catalog_number = 5101
-collection_group = &"Rays and skates"
-logbook_fact = "barndoor skates glide over cold atlantic bottoms with broad wing-like fins. rows of thorny spines protect the back and tail."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 5.0
-weight_max_lb = 150.0
-sell_value_min = 28
-sell_value_max = 68
-sell_value_curve = 0.8
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/smallmouth_bass/smallmouth_bass.tres b/fish/species/smallmouth_bass/smallmouth_bass.tres
deleted file mode 100644
index d840f32..0000000
--- a/fish/species/smallmouth_bass/smallmouth_bass.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"smallmouth_bass"
-display_name = "smallmouth bass"
-active = true
-catalog_number = 2202
-collection_group = &"Freshwater bass"
-logbook_fact = "smallmouth bass favor clear water with rock and moving current. bronze flanks and dark vertical bars distinguish them from largemouth bass."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 1.2
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 12.0
-sell_value_min = 6
-sell_value_max = 15
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/smelt_eulachon/smelt_eulachon.tres b/fish/species/smelt_eulachon/smelt_eulachon.tres
deleted file mode 100644
index f3be455..0000000
--- a/fish/species/smelt_eulachon/smelt_eulachon.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"smelt_eulachon"
-display_name = "eulachon"
-active = true
-catalog_number = 4201
-collection_group = &"Ocean-run forage fish"
-logbook_fact = "eulachon return from the pacific to spawn in coastal rivers. their oil-rich bodies once served as candles when dried and fitted with wicks."
-available_seasons = 1
-allowed_water_types = 3
-rarity = 1
-base_catch_weight = 0.9
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 0.5
-sell_value_min = 4
-sell_value_max = 9
-sell_value_curve = 0.98
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/smelt_rainbow/smelt_rainbow.tres b/fish/species/smelt_rainbow/smelt_rainbow.tres
deleted file mode 100644
index 6a0d08a..0000000
--- a/fish/species/smelt_rainbow/smelt_rainbow.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"smelt_rainbow"
-display_name = "rainbow smelt"
-active = true
-catalog_number = 4101
-collection_group = &"Migratory forage fish"
-logbook_fact = "rainbow smelt move between coastal water and rivers, though some remain landlocked. cucumber-like aromas cling to freshly caught fish."
-available_seasons = 9
-allowed_water_types = 3
-rarity = 0
-base_catch_weight = 2.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 0.8
-sell_value_min = 2
-sell_value_max = 5
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/snakehead_giant/snakehead_giant.tres b/fish/species/snakehead_giant/snakehead_giant.tres
deleted file mode 100644
index 069d723..0000000
--- a/fish/species/snakehead_giant/snakehead_giant.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"snakehead_giant"
-display_name = "giant snakehead"
-active = true
-catalog_number = 7601
-collection_group = &"Tropical freshwater predators"
-logbook_fact = "giant snakeheads breathe air and guard schools of young near the surface. long bodies launch sudden attacks from flooded vegetation."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 3.0
-weight_max_lb = 45.0
-sell_value_min = 28
-sell_value_max = 64
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/snapper_lane/fish_snapper_lane.png b/fish/species/snapper_lane/fish_snapper_lane.png
deleted file mode 100644
index a9c3270..0000000
Binary files a/fish/species/snapper_lane/fish_snapper_lane.png and /dev/null differ
diff --git a/fish/species/snapper_lane/fish_snapper_lane.png.import b/fish/species/snapper_lane/fish_snapper_lane.png.import
deleted file mode 100644
index 323b6a0..0000000
--- a/fish/species/snapper_lane/fish_snapper_lane.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://d3d8s30bxq4tp"
-path="res://.godot/imported/fish_snapper_lane.png-e491c67d8214edb53b8a2005c274c743.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/snapper_lane/fish_snapper_lane.png"
-dest_files=["res://.godot/imported/fish_snapper_lane.png-e491c67d8214edb53b8a2005c274c743.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/snapper_lane/snapper_lane.tres b/fish/species/snapper_lane/snapper_lane.tres
deleted file mode 100644
index 0288e2d..0000000
--- a/fish/species/snapper_lane/snapper_lane.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://b7t3fuykfa6ri"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="SnapperLaneAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"snapper_lane"
-display_name = "lane snapper"
-active = true
-catalog_number = 6001
-collection_group = &"Reef snapper"
-logbook_fact = "lane snapper use reef structure for cover and hunt small crustaceans and fish when the light begins to fade."
-available_seasons = 15
-allowed_water_types = 2
-base_catch_weight = 0.45
-catch_profile = ExtResource("2_profile")
-availability = SubResource("SnapperLaneAvailability")
-weight_max_lb = 5
-display_scale_min = 0.85
-display_scale_max = 1.5
-display_scale_curve = 0.9
-sell_value_min = 5
-sell_value_max = 11
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/snapper_mangrove/fish_snapper_mangrove.png b/fish/species/snapper_mangrove/fish_snapper_mangrove.png
deleted file mode 100644
index 37f2850..0000000
Binary files a/fish/species/snapper_mangrove/fish_snapper_mangrove.png and /dev/null differ
diff --git a/fish/species/snapper_mangrove/fish_snapper_mangrove.png.import b/fish/species/snapper_mangrove/fish_snapper_mangrove.png.import
deleted file mode 100644
index 7567a29..0000000
--- a/fish/species/snapper_mangrove/fish_snapper_mangrove.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://v34sdv1v7c2x"
-path="res://.godot/imported/fish_snapper_mangrove.png-fb4a657d3596495207f499095e50e416.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/snapper_mangrove/fish_snapper_mangrove.png"
-dest_files=["res://.godot/imported/fish_snapper_mangrove.png-fb4a657d3596495207f499095e50e416.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/snapper_mangrove/snapper_mangrove.tres b/fish/species/snapper_mangrove/snapper_mangrove.tres
deleted file mode 100644
index 9a0ede5..0000000
--- a/fish/species/snapper_mangrove/snapper_mangrove.tres
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://d4eh51kwhsac0"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="SnapperMangroveAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"snapper_mangrove"
-display_name = "mangrove snapper"
-active = true
-catalog_number = 6002
-collection_group = &"Reef snapper"
-logbook_fact = "mangrove snapper shelter among roots and docks when young, then move toward reefs as they grow."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.35
-catch_profile = ExtResource("2_profile")
-availability = SubResource("SnapperMangroveAvailability")
-weight_max_lb = 12
-display_scale_min = 0.85
-display_scale_max = 1.7
-display_scale_curve = 0.9
-sell_value_min = 7
-sell_value_max = 16
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/snapper_mutton/fish_snapper_mutton.png b/fish/species/snapper_mutton/fish_snapper_mutton.png
deleted file mode 100644
index 7c99847..0000000
Binary files a/fish/species/snapper_mutton/fish_snapper_mutton.png and /dev/null differ
diff --git a/fish/species/snapper_mutton/fish_snapper_mutton.png.import b/fish/species/snapper_mutton/fish_snapper_mutton.png.import
deleted file mode 100644
index cc283b6..0000000
--- a/fish/species/snapper_mutton/fish_snapper_mutton.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cawvpo8aysw5i"
-path="res://.godot/imported/fish_snapper_mutton.png-c83ebfec373a81cf6c3f34599406aad6.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/snapper_mutton/fish_snapper_mutton.png"
-dest_files=["res://.godot/imported/fish_snapper_mutton.png-c83ebfec373a81cf6c3f34599406aad6.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/snapper_mutton/snapper_mutton.tres b/fish/species/snapper_mutton/snapper_mutton.tres
deleted file mode 100644
index 7c5aee9..0000000
--- a/fish/species/snapper_mutton/snapper_mutton.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://cfvwloicmu53b"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="SnapperMuttonAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"snapper_mutton"
-display_name = "mutton snapper"
-active = true
-catalog_number = 6003
-collection_group = &"Reef snapper"
-logbook_fact = "mutton snapper have canine teeth that help them pick crabs, shrimp, and other hard-shelled prey from the reef."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.2
-catch_profile = ExtResource("2_profile")
-availability = SubResource("SnapperMuttonAvailability")
-weight_min_lb = 2
-weight_max_lb = 30
-display_scale_min = 0.85
-display_scale_max = 1.9
-display_scale_curve = 0.9
-sell_value_min = 12
-sell_value_max = 28
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/snapper_red/fish_snapper_red.png b/fish/species/snapper_red/fish_snapper_red.png
deleted file mode 100644
index 20285eb..0000000
Binary files a/fish/species/snapper_red/fish_snapper_red.png and /dev/null differ
diff --git a/fish/species/snapper_red/fish_snapper_red.png.import b/fish/species/snapper_red/fish_snapper_red.png.import
deleted file mode 100644
index 6412dfc..0000000
--- a/fish/species/snapper_red/fish_snapper_red.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://ei5scljuqklk"
-path="res://.godot/imported/fish_snapper_red.png-b66be0003a9d62c443a185008180dd96.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/snapper_red/fish_snapper_red.png"
-dest_files=["res://.godot/imported/fish_snapper_red.png-b66be0003a9d62c443a185008180dd96.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/snapper_red/snapper_red.tres b/fish/species/snapper_red/snapper_red.tres
deleted file mode 100644
index 247edc0..0000000
--- a/fish/species/snapper_red/snapper_red.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://cpvi3raa6epib"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="SnapperRedAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"snapper_red"
-display_name = "red snapper"
-active = true
-catalog_number = 6004
-collection_group = &"Reef snapper"
-logbook_fact = "red snapper gather around reefs, wrecks, and ledges. their diet includes small fish, shrimp, and squid."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.35
-catch_profile = ExtResource("2_profile")
-availability = SubResource("SnapperRedAvailability")
-weight_min_lb = 1
-weight_max_lb = 25
-display_scale_min = 0.85
-display_scale_max = 1.7
-display_scale_curve = 0.9
-sell_value_min = 8
-sell_value_max = 19
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/snook_common/snook_common.tres b/fish/species/snook_common/snook_common.tres
deleted file mode 100644
index 5b84e99..0000000
--- a/fish/species/snook_common/snook_common.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"snook_common"
-display_name = "common snook"
-active = true
-catalog_number = 7303
-collection_group = &"Tropical estuary"
-logbook_fact = "common snook wait around mangrove roots and current seams to ambush prey. a bold black lateral line divides their silver flanks."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 30.0
-sell_value_min = 13
-sell_value_max = 32
-sell_value_curve = 0.85
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sole_dover/sole_dover.tres b/fish/species/sole_dover/sole_dover.tres
deleted file mode 100644
index 2b522a0..0000000
--- a/fish/species/sole_dover/sole_dover.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sole_dover"
-display_name = "Dover sole"
-active = true
-catalog_number = 1404
-collection_group = &"Cold shelf fish"
-logbook_fact = "dover sole glide over deep muddy bottoms of the eastern pacific. both eyes migrate to the right side as the young fish becomes flat."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 8.0
-sell_value_min = 6
-sell_value_max = 14
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sprat_european/sprat_european.tres b/fish/species/sprat_european/sprat_european.tres
deleted file mode 100644
index 82f4876..0000000
--- a/fish/species/sprat_european/sprat_european.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sprat_european"
-display_name = "European sprat"
-active = true
-catalog_number = 1303
-collection_group = &"Cold schooling fish"
-logbook_fact = "european sprat form compact schools in northern coastal seas. these tiny herring relatives feed on plankton near the surface."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 0
-base_catch_weight = 3.2
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.03
-weight_max_lb = 0.3
-sell_value_min = 1
-sell_value_max = 3
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/squid_bigfin_reef/squid_bigfin_reef.tres b/fish/species/squid_bigfin_reef/squid_bigfin_reef.tres
deleted file mode 100644
index 68f8993..0000000
--- a/fish/species/squid_bigfin_reef/squid_bigfin_reef.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"squid_bigfin_reef"
-display_name = "bigfin reef squid"
-active = true
-catalog_number = 6501
-collection_group = &"Squid"
-logbook_fact = "bigfin reef squid hover in coordinated groups around warm reefs. shifting colors and body patterns signal to nearby squid."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 5.0
-sell_value_min = 13
-sell_value_max = 30
-sell_value_curve = 0.91
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/squid_colossal/squid_colossal.tres b/fish/species/squid_colossal/squid_colossal.tres
deleted file mode 100644
index 4e38896..0000000
--- a/fish/species/squid_colossal/squid_colossal.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"squid_colossal"
-display_name = "colossal squid"
-active = true
-catalog_number = 6502
-collection_group = &"Squid"
-logbook_fact = "colossal squid carry swiveling hooks on their arms and tentacles. enormous eyes gather faint light in the deep southern ocean."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.015
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 400.0
-weight_max_lb = 1100.0
-sell_value_min = 82
-sell_value_max = 190
-sell_value_curve = 0.76
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/squid_giant/squid_giant.tres b/fish/species/squid_giant/squid_giant.tres
deleted file mode 100644
index 4f76512..0000000
--- a/fish/species/squid_giant/squid_giant.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"squid_giant"
-display_name = "giant squid"
-active = true
-catalog_number = 6503
-collection_group = &"Squid"
-logbook_fact = "giant squid hunt deep ocean prey with two long feeding tentacles. the largest eyes in the animal world watch for movement in darkness."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.02
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 100.0
-weight_max_lb = 600.0
-sell_value_min = 70
-sell_value_max = 165
-sell_value_curve = 0.77
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/squid_humboldt/squid_humboldt.tres b/fish/species/squid_humboldt/squid_humboldt.tres
deleted file mode 100644
index 14abcfb..0000000
--- a/fish/species/squid_humboldt/squid_humboldt.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"squid_humboldt"
-display_name = "Humboldt squid"
-active = true
-catalog_number = 6504
-collection_group = &"Squid"
-logbook_fact = "humboldt squid flash red and white while hunting in large groups. powerful beaks and grasping suckers handle fish and other squid."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.09
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 5.0
-weight_max_lb = 110.0
-sell_value_min = 26
-sell_value_max = 64
-sell_value_curve = 0.82
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/stingray_ocellate_river/stingray_ocellate_river.tres b/fish/species/stingray_ocellate_river/stingray_ocellate_river.tres
deleted file mode 100644
index 10bf3dc..0000000
--- a/fish/species/stingray_ocellate_river/stingray_ocellate_river.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"stingray_ocellate_river"
-display_name = "ocellate river stingray"
-active = true
-catalog_number = 5104
-collection_group = &"Rays and skates"
-logbook_fact = "ocellate river stingrays carry bright eye-like spots across a round disc. they hide beneath sand in south american rivers."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 3
-base_catch_weight = 0.09
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 2.0
-weight_max_lb = 35.0
-sell_value_min = 28
-sell_value_max = 65
-sell_value_curve = 0.82
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/stingray_southern/stingray_southern.tres b/fish/species/stingray_southern/stingray_southern.tres
deleted file mode 100644
index c0b03f0..0000000
--- a/fish/species/stingray_southern/stingray_southern.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"stingray_southern"
-display_name = "southern stingray"
-active = true
-catalog_number = 5105
-collection_group = &"Rays and skates"
-logbook_fact = "southern stingrays bury in warm sand with only their eyes and spiracles visible. crushing plates break open clams and crustaceans."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.23
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 2.0
-weight_max_lb = 80.0
-sell_value_min = 15
-sell_value_max = 40
-sell_value_curve = 0.82
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sturgeon_lake/sturgeon_lake.png b/fish/species/sturgeon_lake/sturgeon_lake.png
deleted file mode 100644
index 68c9a74..0000000
Binary files a/fish/species/sturgeon_lake/sturgeon_lake.png and /dev/null differ
diff --git a/fish/species/sturgeon_lake/sturgeon_lake.png.import b/fish/species/sturgeon_lake/sturgeon_lake.png.import
deleted file mode 100644
index 505b308..0000000
--- a/fish/species/sturgeon_lake/sturgeon_lake.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://515k3s5wc3ct"
-path="res://.godot/imported/sturgeon_lake.png-ab8bbee2fea49b2a23bc376500df6ae1.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/sturgeon_lake/sturgeon_lake.png"
-dest_files=["res://.godot/imported/sturgeon_lake.png-ab8bbee2fea49b2a23bc376500df6ae1.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/sturgeon_lake/sturgeon_lake.tres b/fish/species/sturgeon_lake/sturgeon_lake.tres
deleted file mode 100644
index eef7789..0000000
--- a/fish/species/sturgeon_lake/sturgeon_lake.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/102.png" id="4_texture"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sturgeon_lake"
-display_name = "lake sturgeon"
-active = true
-catalog_number = 102
-collection_group = &"Ancient freshwater fish"
-logbook_fact = "lake sturgeon sweep the bottom with four sensitive barbels before extending a tube-like mouth to feed. they can live for decades."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 3
-base_catch_weight = 0.09
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 10.0
-weight_max_lb = 180.0
-sell_value_min = 36
-sell_value_max = 82
-sell_value_curve = 0.8
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/sturgeon_shovelnose/sturgeon_shovelnose.png b/fish/species/sturgeon_shovelnose/sturgeon_shovelnose.png
deleted file mode 100644
index 197c90a..0000000
Binary files a/fish/species/sturgeon_shovelnose/sturgeon_shovelnose.png and /dev/null differ
diff --git a/fish/species/sturgeon_shovelnose/sturgeon_shovelnose.png.import b/fish/species/sturgeon_shovelnose/sturgeon_shovelnose.png.import
deleted file mode 100644
index c66c0ec..0000000
--- a/fish/species/sturgeon_shovelnose/sturgeon_shovelnose.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://d3jk1u2bkisil"
-path="res://.godot/imported/sturgeon_shovelnose.png-abfc7dc07c08b02b361c4ea424ee5ece.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/sturgeon_shovelnose/sturgeon_shovelnose.png"
-dest_files=["res://.godot/imported/sturgeon_shovelnose.png-abfc7dc07c08b02b361c4ea424ee5ece.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/sturgeon_shovelnose/sturgeon_shovelnose.tres b/fish/species/sturgeon_shovelnose/sturgeon_shovelnose.tres
deleted file mode 100644
index 9f02593..0000000
--- a/fish/species/sturgeon_shovelnose/sturgeon_shovelnose.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/105.png" id="4_texture"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sturgeon_shovelnose"
-display_name = "shovelnose sturgeon"
-active = true
-catalog_number = 105
-collection_group = &"Ancient freshwater fish"
-logbook_fact = "shovelnose sturgeon hug flowing river bottoms and probe for insect larvae. a flattened snout helps them hold position in strong current."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.22
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 8.0
-sell_value_min = 12
-sell_value_max = 25
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/sturgeon_white/sturgeon_white.tres b/fish/species/sturgeon_white/sturgeon_white.tres
deleted file mode 100644
index 961411d..0000000
--- a/fish/species/sturgeon_white/sturgeon_white.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sturgeon_white"
-display_name = "white sturgeon"
-active = true
-catalog_number = 4301
-collection_group = &"Ocean-run giants"
-logbook_fact = "white sturgeon are the largest freshwater fish in north america. armored plates line bodies built for long lives in rivers and estuaries."
-available_seasons = 5
-allowed_water_types = 3
-rarity = 4
-base_catch_weight = 0.045
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 20.0
-weight_max_lb = 500.0
-sell_value_min = 58
-sell_value_max = 135
-sell_value_curve = 0.78
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sucker_white/sucker_white.tres b/fish/species/sucker_white/sucker_white.tres
deleted file mode 100644
index ff48770..0000000
--- a/fish/species/sucker_white/sucker_white.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"sucker_white"
-display_name = "white sucker"
-active = true
-catalog_number = 2305
-collection_group = &"Freshwater bottom fish"
-logbook_fact = "white suckers vacuum insects and organic matter from cool freshwater bottoms. spring runs carry large groups into shallow streams."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 2.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 8.0
-sell_value_min = 3
-sell_value_max = 8
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/sunfish/fish_sunfish.png b/fish/species/sunfish/fish_sunfish.png
index ca8a340..138699e 100644
Binary files a/fish/species/sunfish/fish_sunfish.png and b/fish/species/sunfish/fish_sunfish.png differ
diff --git a/fish/species/sunfish/sunfish.tres b/fish/species/sunfish/sunfish.tres
index a8e2290..2346b71 100644
--- a/fish/species/sunfish/sunfish.tres
+++ b/fish/species/sunfish/sunfish.tres
@@ -2,19 +2,13 @@
[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Resource" path="res://fish/day_availability.tres" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
+[ext_resource type="Resource" path="res://fish/default_availability.tres" id="3_availability"]
+[ext_resource type="Texture2D" path="res://fish/species/sunfish/fish_sunfish.png" id="4_texture"]
[resource]
script = ExtResource("1_fish_data")
id = &"sunfish"
display_name = "sunfish"
-active = true
-catalog_number = 5001
-collection_group = &"Open-ocean giants"
-logbook_fact = "ocean sunfish are built more like enormous swimming heads than ordinary fish. they visit the surface and turn sideways to warm up."
-available_seasons = 7
-allowed_water_types = 2
rarity = 0
base_catch_weight = 7.0
catch_profile = ExtResource("2_profile")
diff --git a/fish/species/surgeonfish_blue/surgeonfish_blue.tres b/fish/species/surgeonfish_blue/surgeonfish_blue.tres
deleted file mode 100644
index d0a903e..0000000
--- a/fish/species/surgeonfish_blue/surgeonfish_blue.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"surgeonfish_blue"
-display_name = "blue tang"
-active = true
-catalog_number = 5501
-collection_group = &"Reef grazers"
-logbook_fact = "blue tangs graze algae from coral reefs with small close-set teeth. sharp folding spines near the tail give surgeonfish their name."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.9
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 1.5
-sell_value_min = 5
-sell_value_max = 11
-sell_value_curve = 0.95
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/swordfish/fish_swordfish.png b/fish/species/swordfish/fish_swordfish.png
deleted file mode 100644
index 3b1917b..0000000
Binary files a/fish/species/swordfish/fish_swordfish.png and /dev/null differ
diff --git a/fish/species/swordfish/fish_swordfish.png.import b/fish/species/swordfish/fish_swordfish.png.import
deleted file mode 100644
index 4979773..0000000
--- a/fish/species/swordfish/fish_swordfish.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cs1qmm4galac4"
-path="res://.godot/imported/fish_swordfish.png-c82f3cdc508ed13c9ec1a5b28b6eb830.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/swordfish/fish_swordfish.png"
-dest_files=["res://.godot/imported/fish_swordfish.png-c82f3cdc508ed13c9ec1a5b28b6eb830.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/swordfish/swordfish.tres b/fish/species/swordfish/swordfish.tres
deleted file mode 100644
index 24319f1..0000000
--- a/fish/species/swordfish/swordfish.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://chqva2v2mtm8q"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="SwordfishAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"ocean"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"swordfish"
-display_name = "swordfish"
-active = true
-catalog_number = 4604
-collection_group = &"Oceanic billfish"
-logbook_fact = "swordfish are fast, wide-ranging predators with a flattened bill. they can cross deep water and warm surface layers in one day."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.06
-catch_profile = ExtResource("2_profile")
-availability = SubResource("SwordfishAvailability")
-weight_min_lb = 50
-weight_max_lb = 1000
-display_scale_min = 0.85
-display_scale_max = 3.0
-display_scale_curve = 0.9
-sell_value_min = 55
-sell_value_max = 110
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/tambaqui/tambaqui.tres b/fish/species/tambaqui/tambaqui.tres
deleted file mode 100644
index 4bfd4a1..0000000
--- a/fish/species/tambaqui/tambaqui.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"tambaqui"
-display_name = "tambaqui"
-active = true
-catalog_number = 7509
-collection_group = &"Tropical freshwater fish"
-logbook_fact = "tambaqui follow seasonal floods into forests to feed on fruit and seeds. strong square teeth crack tough plant material."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 8.0
-weight_max_lb = 80.0
-sell_value_min = 30
-sell_value_max = 68
-sell_value_curve = 0.82
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/tang_yellow/tang_yellow.tres b/fish/species/tang_yellow/tang_yellow.tres
deleted file mode 100644
index 501f2a3..0000000
--- a/fish/species/tang_yellow/tang_yellow.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"tang_yellow"
-display_name = "yellow tang"
-active = true
-catalog_number = 5504
-collection_group = &"Reef grazers"
-logbook_fact = "yellow tangs browse algae across hawaiian reefs during daylight. at night their bright bodies darken and show a pale horizontal stripe."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.35
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 0.8
-sell_value_min = 9
-sell_value_max = 20
-sell_value_curve = 0.96
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/tarpon_atlantic/tarpon_atlantic.tres b/fish/species/tarpon_atlantic/tarpon_atlantic.tres
deleted file mode 100644
index 0b8c6aa..0000000
--- a/fish/species/tarpon_atlantic/tarpon_atlantic.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"tarpon_atlantic"
-display_name = "atlantic tarpon"
-active = true
-catalog_number = 7301
-collection_group = &"Tropical estuary"
-logbook_fact = "atlantic tarpon gulp air with a lung-like swim bladder when coastal water runs low on oxygen. giant silver scales flash during leaps."
-available_seasons = 15
-allowed_water_types = 3
-rarity = 3
-base_catch_weight = 0.09
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 10.0
-weight_max_lb = 180.0
-sell_value_min = 34
-sell_value_max = 82
-sell_value_curve = 0.8
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/tautog/tautog.tres b/fish/species/tautog/tautog.tres
deleted file mode 100644
index 7028401..0000000
--- a/fish/species/tautog/tautog.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"tautog"
-display_name = "tautog"
-active = true
-catalog_number = 6906
-collection_group = &"Temperate reef fish"
-logbook_fact = "tautog pry mussels and crabs from rocky structure with thick lips and strong teeth. adults return to familiar shelters year after year."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 20.0
-sell_value_min = 7
-sell_value_max = 18
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/tench/tench.tres b/fish/species/tench/tench.tres
deleted file mode 100644
index 7ffe273..0000000
--- a/fish/species/tench/tench.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"tench"
-display_name = "tench"
-active = true
-catalog_number = 2509
-collection_group = &"Freshwater coarse fish"
-logbook_fact = "tench browse quiet, muddy bottoms with two small mouth barbels. a thick coat of slime protects their olive-green scales."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.75
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 12.0
-sell_value_min = 7
-sell_value_max = 17
-sell_value_curve = 0.88
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/tetra_neon/tetra_neon.tres b/fish/species/tetra_neon/tetra_neon.tres
deleted file mode 100644
index da42e36..0000000
--- a/fish/species/tetra_neon/tetra_neon.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"tetra_neon"
-display_name = "neon tetra"
-active = true
-catalog_number = 2102
-collection_group = &"Freshwater aquarium fish"
-logbook_fact = "neon tetras school through shaded forest streams. an iridescent blue stripe remains visible in the dim, tannin-stained water."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.9
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.01
-weight_max_lb = 0.08
-sell_value_min = 4
-sell_value_max = 8
-sell_value_curve = 0.98
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/tigerfish_goliath/tigerfish_goliath.tres b/fish/species/tigerfish_goliath/tigerfish_goliath.tres
deleted file mode 100644
index f34d449..0000000
--- a/fish/species/tigerfish_goliath/tigerfish_goliath.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"tigerfish_goliath"
-display_name = "goliath tigerfish"
-active = true
-catalog_number = 2703
-collection_group = &"Freshwater giants"
-logbook_fact = "goliath tigerfish patrol powerful african rivers with interlocking dagger-like teeth. a muscular forked tail drives sudden attacks."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 4
-base_catch_weight = 0.035
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 20.0
-weight_max_lb = 150.0
-sell_value_min = 58
-sell_value_max = 132
-sell_value_curve = 0.8
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/tilapia_nile/tilapia_nile.tres b/fish/species/tilapia_nile/tilapia_nile.tres
deleted file mode 100644
index d7a2eed..0000000
--- a/fish/species/tilapia_nile/tilapia_nile.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/7506.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"tilapia_nile"
-display_name = "Nile tilapia"
-active = true
-catalog_number = 7506
-collection_group = &"Tropical freshwater fish"
-logbook_fact = "nile tilapia thrive in warm lakes and rivers on a varied diet. females protect eggs and young inside their mouths."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 2.2
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 8.0
-sell_value_min = 3
-sell_value_max = 9
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/tilefish_blueline/tilefish_blueline.tres b/fish/species/tilefish_blueline/tilefish_blueline.tres
deleted file mode 100644
index aff98b6..0000000
--- a/fish/species/tilefish_blueline/tilefish_blueline.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"tilefish_blueline"
-display_name = "blueline tilefish"
-active = true
-catalog_number = 1601
-collection_group = &"Deep shelf fish"
-logbook_fact = "blueline tilefish shelter in burrows and rocky openings along deep shelves. a narrow blue stripe traces the edge of the dorsal fin."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.7
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 20.0
-sell_value_min = 7
-sell_value_max = 18
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/tilefish_golden/tilefish_golden.tres b/fish/species/tilefish_golden/tilefish_golden.tres
deleted file mode 100644
index ed203c8..0000000
--- a/fish/species/tilefish_golden/tilefish_golden.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"tilefish_golden"
-display_name = "golden tilefish"
-active = true
-catalog_number = 1602
-collection_group = &"Deep shelf fish"
-logbook_fact = "golden tilefish excavate burrows in soft clay along deep ocean slopes. their bright yellow spots contrast with blue-green backs."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.22
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 2.0
-weight_max_lb = 50.0
-sell_value_min = 15
-sell_value_max = 37
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/trevally_bluefin/trevally_bluefin.tres b/fish/species/trevally_bluefin/trevally_bluefin.tres
deleted file mode 100644
index 3cf8dee..0000000
--- a/fish/species/trevally_bluefin/trevally_bluefin.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"trevally_bluefin"
-display_name = "bluefin trevally"
-active = true
-catalog_number = 5802
-collection_group = &"Reef predators"
-logbook_fact = "bluefin trevally hunt reef fish in coordinated groups. electric blue fins and scattered blue spots contrast with a golden body."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 25.0
-sell_value_min = 14
-sell_value_max = 32
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/trevally_giant/trevally_giant.tres b/fish/species/trevally_giant/trevally_giant.tres
deleted file mode 100644
index 62ee42b..0000000
--- a/fish/species/trevally_giant/trevally_giant.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"trevally_giant"
-display_name = "giant trevally"
-active = true
-catalog_number = 3603
-collection_group = &"Large coastal predators"
-logbook_fact = "giant trevally hunt reef edges with bursts of speed and force. large individuals sometimes charge prey from surf and shallow lagoons."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 10.0
-weight_max_lb = 150.0
-sell_value_min = 32
-sell_value_max = 78
-sell_value_curve = 0.8
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/triggerfish_gray/triggerfish_gray.tres b/fish/species/triggerfish_gray/triggerfish_gray.tres
deleted file mode 100644
index 0b205a2..0000000
--- a/fish/species/triggerfish_gray/triggerfish_gray.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"triggerfish_gray"
-display_name = "gray triggerfish"
-active = true
-catalog_number = 5602
-collection_group = &"Reef oddities"
-logbook_fact = "gray triggerfish lock a stout dorsal spine upright when wedged into shelter. strong teeth crack barnacles, crabs, and shellfish."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.3
-weight_max_lb = 12.0
-sell_value_min = 7
-sell_value_max = 17
-sell_value_curve = 0.88
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/triggerfish_queen/triggerfish_queen.tres b/fish/species/triggerfish_queen/triggerfish_queen.tres
deleted file mode 100644
index e747f25..0000000
--- a/fish/species/triggerfish_queen/triggerfish_queen.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"triggerfish_queen"
-display_name = "queen triggerfish"
-active = true
-catalog_number = 5605
-collection_group = &"Reef oddities"
-logbook_fact = "queen triggerfish blow jets of water to uncover buried prey and flip sea urchins. vivid blue lines radiate across the face."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.25
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 15.0
-sell_value_min = 14
-sell_value_max = 32
-sell_value_curve = 0.88
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/tripletail/tripletail.tres b/fish/species/tripletail/tripletail.tres
deleted file mode 100644
index 7bb151b..0000000
--- a/fish/species/tripletail/tripletail.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"tripletail"
-display_name = "tripletail"
-active = true
-catalog_number = 604
-collection_group = &"Coastal oddities"
-logbook_fact = "tripletail often drift sideways near floating debris, resembling a loose leaf. rounded rear fins create the three-tailed silhouette."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.28
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 30.0
-sell_value_min = 13
-sell_value_max = 31
-sell_value_curve = 0.85
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/trout_brown/trout_brown.tres b/fish/species/trout_brown/trout_brown.tres
deleted file mode 100644
index 6beb1ba..0000000
--- a/fish/species/trout_brown/trout_brown.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"trout_brown"
-display_name = "brown trout"
-active = true
-catalog_number = 1103
-collection_group = &"Cold freshwater salmonids"
-logbook_fact = "brown trout often shelter by day and hunt more freely after dark. adaptable populations occupy streams, lakes, and even coastal water."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 25.0
-sell_value_min = 7
-sell_value_max = 20
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/trout_cutthroat/fish_trout_cutthroat.png b/fish/species/trout_cutthroat/fish_trout_cutthroat.png
deleted file mode 100644
index 9287c2f..0000000
Binary files a/fish/species/trout_cutthroat/fish_trout_cutthroat.png and /dev/null differ
diff --git a/fish/species/trout_cutthroat/fish_trout_cutthroat.png.import b/fish/species/trout_cutthroat/fish_trout_cutthroat.png.import
deleted file mode 100644
index 90a255e..0000000
--- a/fish/species/trout_cutthroat/fish_trout_cutthroat.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://nowggrl8wmr"
-path="res://.godot/imported/fish_trout_cutthroat.png-a27813936c24b235f8eea97a7a7dfdc0.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/trout_cutthroat/fish_trout_cutthroat.png"
-dest_files=["res://.godot/imported/fish_trout_cutthroat.png-a27813936c24b235f8eea97a7a7dfdc0.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/trout_cutthroat/trout_cutthroat.tres b/fish/species/trout_cutthroat/trout_cutthroat.tres
deleted file mode 100644
index 6ea9bff..0000000
--- a/fish/species/trout_cutthroat/trout_cutthroat.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://bd2t2ktvkkovd"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="TroutCutthroatAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"river"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"trout_cutthroat"
-display_name = "cutthroat trout"
-active = true
-catalog_number = 1104
-collection_group = &"Cold freshwater salmonids"
-logbook_fact = "cutthroat trout take their name from the red slash beneath the jaw. many populations rely on cold, well-oxygenated streams."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.4
-catch_profile = ExtResource("2_profile")
-availability = SubResource("TroutCutthroatAvailability")
-weight_min_lb = 0.25
-weight_max_lb = 10
-display_scale_min = 0.85
-display_scale_max = 1.4
-display_scale_curve = 0.9
-sell_value_min = 5
-sell_value_max = 12
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/trout_golden/fish_trout_golden.png b/fish/species/trout_golden/fish_trout_golden.png
deleted file mode 100644
index 8b64101..0000000
Binary files a/fish/species/trout_golden/fish_trout_golden.png and /dev/null differ
diff --git a/fish/species/trout_golden/fish_trout_golden.png.import b/fish/species/trout_golden/fish_trout_golden.png.import
deleted file mode 100644
index bdff181..0000000
--- a/fish/species/trout_golden/fish_trout_golden.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b64w3kwf3wins"
-path="res://.godot/imported/fish_trout_golden.png-00b8999ee94694397caec57987d8241d.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/trout_golden/fish_trout_golden.png"
-dest_files=["res://.godot/imported/fish_trout_golden.png-00b8999ee94694397caec57987d8241d.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/trout_golden/trout_golden.tres b/fish/species/trout_golden/trout_golden.tres
deleted file mode 100644
index bb6732b..0000000
--- a/fish/species/trout_golden/trout_golden.tres
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://n3ixatlsfp8y"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="TroutGoldenAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"lake"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"trout_golden"
-display_name = "golden trout"
-active = true
-catalog_number = 1105
-collection_group = &"Cold freshwater salmonids"
-logbook_fact = "golden trout evolved in clear, high-elevation california streams. their bright flanks stand out against rocky alpine water."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.2
-catch_profile = ExtResource("2_profile")
-availability = SubResource("TroutGoldenAvailability")
-weight_min_lb = 0.2
-weight_max_lb = 3
-display_scale_min = 0.85
-display_scale_curve = 0.9
-sell_value_min = 8
-sell_value_max = 16
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/trout_rainbow/fish_trout_rainbow.png b/fish/species/trout_rainbow/fish_trout_rainbow.png
deleted file mode 100644
index 3f4dfe4..0000000
Binary files a/fish/species/trout_rainbow/fish_trout_rainbow.png and /dev/null differ
diff --git a/fish/species/trout_rainbow/fish_trout_rainbow.png.import b/fish/species/trout_rainbow/fish_trout_rainbow.png.import
deleted file mode 100644
index fd8eafe..0000000
--- a/fish/species/trout_rainbow/fish_trout_rainbow.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://mfxehkoxea21"
-path="res://.godot/imported/fish_trout_rainbow.png-e58603ff0df9519b81e6b0d0da55998c.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/trout_rainbow/fish_trout_rainbow.png"
-dest_files=["res://.godot/imported/fish_trout_rainbow.png-e58603ff0df9519b81e6b0d0da55998c.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/trout_rainbow/trout_rainbow.tres b/fish/species/trout_rainbow/trout_rainbow.tres
deleted file mode 100644
index 69d40bd..0000000
--- a/fish/species/trout_rainbow/trout_rainbow.tres
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://c3r8v522etgaq"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="TroutRainbowAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"river"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"trout_rainbow"
-display_name = "rainbow trout"
-active = true
-catalog_number = 1106
-collection_group = &"Cold freshwater salmonids"
-logbook_fact = "rainbow trout are adaptable stream hunters. the pink lateral stripe is especially vivid when the fish is in breeding condition."
-available_seasons = 15
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.45
-catch_profile = ExtResource("2_profile")
-availability = SubResource("TroutRainbowAvailability")
-weight_max_lb = 20
-display_scale_min = 0.85
-display_scale_max = 1.4
-display_scale_curve = 0.9
-sell_value_min = 5
-sell_value_max = 11
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/trout_steelhead/fish_trout_steelhead.png b/fish/species/trout_steelhead/fish_trout_steelhead.png
deleted file mode 100644
index 45cc1b1..0000000
Binary files a/fish/species/trout_steelhead/fish_trout_steelhead.png and /dev/null differ
diff --git a/fish/species/trout_steelhead/fish_trout_steelhead.png.import b/fish/species/trout_steelhead/fish_trout_steelhead.png.import
deleted file mode 100644
index 3b3200e..0000000
--- a/fish/species/trout_steelhead/fish_trout_steelhead.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cabbpc55sf2gw"
-path="res://.godot/imported/fish_trout_steelhead.png-10d14e8a4754db88ccdaf7ffe4e4d9fe.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/trout_steelhead/fish_trout_steelhead.png"
-dest_files=["res://.godot/imported/fish_trout_steelhead.png-10d14e8a4754db88ccdaf7ffe4e4d9fe.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/trout_steelhead/trout_steelhead.tres b/fish/species/trout_steelhead/trout_steelhead.tres
deleted file mode 100644
index ed052d9..0000000
--- a/fish/species/trout_steelhead/trout_steelhead.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://dv2hjtqd5xhfu"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="TroutSteelheadAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"river"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"trout_steelhead"
-display_name = "steelhead trout"
-active = true
-catalog_number = 4501
-collection_group = &"Ocean-run salmonids"
-logbook_fact = "steelhead are the ocean-going form of rainbow trout. adults can leave fresh water and later return to spawn."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 2
-base_catch_weight = 0.2
-catch_profile = ExtResource("2_profile")
-availability = SubResource("TroutSteelheadAvailability")
-weight_min_lb = 2
-weight_max_lb = 35
-display_scale_min = 0.85
-display_scale_max = 1.8
-display_scale_curve = 0.9
-sell_value_min = 10
-sell_value_max = 20
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/trumpetfish_atlantic/trumpetfish_atlantic.tres b/fish/species/trumpetfish_atlantic/trumpetfish_atlantic.tres
deleted file mode 100644
index 3ff3cc3..0000000
--- a/fish/species/trumpetfish_atlantic/trumpetfish_atlantic.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"trumpetfish_atlantic"
-display_name = "atlantic trumpetfish"
-active = true
-catalog_number = 5601
-collection_group = &"Reef oddities"
-logbook_fact = "atlantic trumpetfish hover head-down among corals and sea fans. long tubular snouts expand suddenly to draw in small fish."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.3
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.1
-weight_max_lb = 2.0
-sell_value_min = 10
-sell_value_max = 24
-sell_value_curve = 0.94
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/tuna_albacore/fish_tuna_albacore.png b/fish/species/tuna_albacore/fish_tuna_albacore.png
deleted file mode 100644
index 3fde5de..0000000
Binary files a/fish/species/tuna_albacore/fish_tuna_albacore.png and /dev/null differ
diff --git a/fish/species/tuna_albacore/fish_tuna_albacore.png.import b/fish/species/tuna_albacore/fish_tuna_albacore.png.import
deleted file mode 100644
index f2e6b6f..0000000
--- a/fish/species/tuna_albacore/fish_tuna_albacore.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bxebn81i552fg"
-path="res://.godot/imported/fish_tuna_albacore.png-aaac5d00c8a250b19db99d7069f549e1.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/tuna_albacore/fish_tuna_albacore.png"
-dest_files=["res://.godot/imported/fish_tuna_albacore.png-aaac5d00c8a250b19db99d7069f549e1.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/tuna_albacore/tuna_albacore.tres b/fish/species/tuna_albacore/tuna_albacore.tres
deleted file mode 100644
index c8cdfdf..0000000
--- a/fish/species/tuna_albacore/tuna_albacore.tres
+++ /dev/null
@@ -1,35 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://cjd2mibei3713"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="AlbacoreAvailability"]
-script = ExtResource("3_availability_script")
-preferred_bait_tags = Array[StringName]([&"minnow"])
-preferred_bait_weight_multiplier = 1.4
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"tuna_albacore"
-display_name = "albacore tuna"
-active = true
-catalog_number = 4801
-collection_group = &"Oceanic tuna"
-logbook_fact = "albacore have unusually long pectoral fins. their warm muscles help them cruise across wide stretches of open ocean."
-available_seasons = 7
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 1.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("AlbacoreAvailability")
-weight_min_lb = 10.0
-weight_max_lb = 45.0
-display_scale_min = 1.1
-display_scale_max = 1.9
-display_scale_curve = 0.85
-sell_value_min = 12
-sell_value_max = 24
-sell_value_curve = 0.85
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/tuna_bigeye/fish_tuna_bigeye.png b/fish/species/tuna_bigeye/fish_tuna_bigeye.png
deleted file mode 100644
index ac52961..0000000
Binary files a/fish/species/tuna_bigeye/fish_tuna_bigeye.png and /dev/null differ
diff --git a/fish/species/tuna_bigeye/fish_tuna_bigeye.png.import b/fish/species/tuna_bigeye/fish_tuna_bigeye.png.import
deleted file mode 100644
index 3539d10..0000000
--- a/fish/species/tuna_bigeye/fish_tuna_bigeye.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bypnhqmkhnm2c"
-path="res://.godot/imported/fish_tuna_bigeye.png-1c20be325f0bac055325a6fa0ad17dd1.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/tuna_bigeye/fish_tuna_bigeye.png"
-dest_files=["res://.godot/imported/fish_tuna_bigeye.png-1c20be325f0bac055325a6fa0ad17dd1.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/tuna_bigeye/tuna_bigeye.tres b/fish/species/tuna_bigeye/tuna_bigeye.tres
deleted file mode 100644
index 1c31d1a..0000000
--- a/fish/species/tuna_bigeye/tuna_bigeye.tres
+++ /dev/null
@@ -1,35 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://mxa5d130i5oh"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="BigeyeAvailability"]
-script = ExtResource("3_availability_script")
-preferred_bait_tags = Array[StringName]([&"minnow"])
-preferred_bait_weight_multiplier = 1.4
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"tuna_bigeye"
-display_name = "bigeye tuna"
-active = true
-catalog_number = 4802
-collection_group = &"Oceanic tuna"
-logbook_fact = "bigeye tuna make daily trips between deep, cool water and warmer surface layers. their large eyes suit those dim depths."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.55
-catch_profile = ExtResource("2_profile")
-availability = SubResource("BigeyeAvailability")
-weight_min_lb = 20.0
-weight_max_lb = 120.0
-display_scale_min = 1.2
-display_scale_max = 2.2
-display_scale_curve = 0.8
-sell_value_min = 22
-sell_value_max = 48
-sell_value_curve = 0.8
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/tuna_bluefin/fish_tuna_bluefin.png b/fish/species/tuna_bluefin/fish_tuna_bluefin.png
deleted file mode 100644
index 9f44ad8..0000000
Binary files a/fish/species/tuna_bluefin/fish_tuna_bluefin.png and /dev/null differ
diff --git a/fish/species/tuna_bluefin/fish_tuna_bluefin.png.import b/fish/species/tuna_bluefin/fish_tuna_bluefin.png.import
deleted file mode 100644
index 674a0b7..0000000
--- a/fish/species/tuna_bluefin/fish_tuna_bluefin.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dg7nafydp4flx"
-path="res://.godot/imported/fish_tuna_bluefin.png-04ec876b3a78ea716b7a0673e8497fab.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/tuna_bluefin/fish_tuna_bluefin.png"
-dest_files=["res://.godot/imported/fish_tuna_bluefin.png-04ec876b3a78ea716b7a0673e8497fab.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/tuna_bluefin/tuna_bluefin.tres b/fish/species/tuna_bluefin/tuna_bluefin.tres
deleted file mode 100644
index 4bf5033..0000000
--- a/fish/species/tuna_bluefin/tuna_bluefin.tres
+++ /dev/null
@@ -1,35 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://bkck5gncu7tvc"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="BluefinAvailability"]
-script = ExtResource("3_availability_script")
-preferred_bait_tags = Array[StringName]([&"minnow"])
-preferred_bait_weight_multiplier = 1.4
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"tuna_bluefin"
-display_name = "bluefin tuna"
-active = true
-catalog_number = 4803
-collection_group = &"Oceanic tuna"
-logbook_fact = "bluefin tuna are powerful ocean travelers. heat retained in their swimming muscles lets them thrive in surprisingly cold seas."
-available_seasons = 5
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.18
-catch_profile = ExtResource("2_profile")
-availability = SubResource("BluefinAvailability")
-weight_min_lb = 30.0
-weight_max_lb = 250.0
-display_scale_min = 1.3
-display_scale_max = 2.5
-display_scale_curve = 0.78
-sell_value_min = 35
-sell_value_max = 90
-sell_value_curve = 0.78
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/tuna_skipjack/fish_tuna_skipjack.png b/fish/species/tuna_skipjack/fish_tuna_skipjack.png
deleted file mode 100644
index 3d0be56..0000000
Binary files a/fish/species/tuna_skipjack/fish_tuna_skipjack.png and /dev/null differ
diff --git a/fish/species/tuna_skipjack/fish_tuna_skipjack.png.import b/fish/species/tuna_skipjack/fish_tuna_skipjack.png.import
deleted file mode 100644
index 786e985..0000000
--- a/fish/species/tuna_skipjack/fish_tuna_skipjack.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://vvtr81d2aojs"
-path="res://.godot/imported/fish_tuna_skipjack.png-98d7d6a76fc86bd76f861358be0635c7.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/tuna_skipjack/fish_tuna_skipjack.png"
-dest_files=["res://.godot/imported/fish_tuna_skipjack.png-98d7d6a76fc86bd76f861358be0635c7.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/tuna_skipjack/tuna_skipjack.tres b/fish/species/tuna_skipjack/tuna_skipjack.tres
deleted file mode 100644
index cc1988e..0000000
--- a/fish/species/tuna_skipjack/tuna_skipjack.tres
+++ /dev/null
@@ -1,35 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://dli584n85vpfi"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="SkipjackAvailability"]
-script = ExtResource("3_availability_script")
-preferred_bait_tags = Array[StringName]([&"minnow"])
-preferred_bait_weight_multiplier = 1.4
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"tuna_skipjack"
-display_name = "skipjack tuna"
-active = true
-catalog_number = 4804
-collection_group = &"Oceanic tuna"
-logbook_fact = "skipjack travel in fast-moving schools and have dark stripes along their lower sides. they rarely stop swimming."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 3.0
-catch_profile = ExtResource("2_profile")
-availability = SubResource("SkipjackAvailability")
-weight_min_lb = 5.0
-weight_max_lb = 25.0
-display_scale_min = 1.0
-display_scale_max = 1.7
-display_scale_curve = 0.9
-sell_value_min = 8
-sell_value_max = 16
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/tuna_yellowfin/fish_tuna_yellowfin.png b/fish/species/tuna_yellowfin/fish_tuna_yellowfin.png
deleted file mode 100644
index ad47a68..0000000
Binary files a/fish/species/tuna_yellowfin/fish_tuna_yellowfin.png and /dev/null differ
diff --git a/fish/species/tuna_yellowfin/fish_tuna_yellowfin.png.import b/fish/species/tuna_yellowfin/fish_tuna_yellowfin.png.import
deleted file mode 100644
index a8918aa..0000000
--- a/fish/species/tuna_yellowfin/fish_tuna_yellowfin.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bwpdgagaysdsf"
-path="res://.godot/imported/fish_tuna_yellowfin.png-14cd53683bfec6af599f1e1dc5728b99.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/tuna_yellowfin/fish_tuna_yellowfin.png"
-dest_files=["res://.godot/imported/fish_tuna_yellowfin.png-14cd53683bfec6af599f1e1dc5728b99.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/tuna_yellowfin/tuna_yellowfin.tres b/fish/species/tuna_yellowfin/tuna_yellowfin.tres
deleted file mode 100644
index 665228e..0000000
--- a/fish/species/tuna_yellowfin/tuna_yellowfin.tres
+++ /dev/null
@@ -1,34 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://yjeoqfht5ql1"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/bass_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="YellowfinAvailability"]
-script = ExtResource("3_availability_script")
-preferred_bait_tags = Array[StringName]([&"minnow"])
-preferred_bait_weight_multiplier = 1.4
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"tuna_yellowfin"
-display_name = "yellowfin tuna"
-active = true
-catalog_number = 4805
-collection_group = &"Oceanic tuna"
-logbook_fact = "yellowfin are named for their bright yellow fins and finlets. large schools often gather with other open-ocean hunters."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-catch_profile = ExtResource("2_profile")
-availability = SubResource("YellowfinAvailability")
-weight_min_lb = 15.0
-weight_max_lb = 80.0
-display_scale_min = 1.15
-display_scale_max = 2.1
-display_scale_curve = 0.82
-sell_value_min = 16
-sell_value_max = 36
-sell_value_curve = 0.82
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/turbot/turbot.tres b/fish/species/turbot/turbot.tres
deleted file mode 100644
index c3ab7bd..0000000
--- a/fish/species/turbot/turbot.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"turbot"
-display_name = "turbot"
-active = true
-catalog_number = 3703
-collection_group = &"Large flatfish"
-logbook_fact = "turbot have broad, nearly circular bodies covered with bony bumps instead of scales. they bury into sand before ambushing fish and crustaceans."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.24
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 1.0
-weight_max_lb = 35.0
-sell_value_min = 14
-sell_value_max = 34
-sell_value_curve = 0.85
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/turtle_green/turtle_green.tres b/fish/species/turtle_green/turtle_green.tres
deleted file mode 100644
index b1e8e71..0000000
--- a/fish/species/turtle_green/turtle_green.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"turtle_green"
-display_name = "green sea turtle"
-active = true
-catalog_number = 6201
-collection_group = &"Sea turtles"
-logbook_fact = "green sea turtles graze seagrass meadows with finely serrated jaws. adults migrate long distances between feeding and nesting grounds."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.018
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 65.0
-weight_max_lb = 500.0
-sell_value_min = 62
-sell_value_max = 145
-sell_value_curve = 0.78
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/turtle_hawksbill/turtle_hawksbill.tres b/fish/species/turtle_hawksbill/turtle_hawksbill.tres
deleted file mode 100644
index 5b5a5c8..0000000
--- a/fish/species/turtle_hawksbill/turtle_hawksbill.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"turtle_hawksbill"
-display_name = "hawksbill sea turtle"
-active = true
-catalog_number = 6202
-collection_group = &"Sea turtles"
-logbook_fact = "hawksbill sea turtles reach into reef crevices with narrow hooked beaks. a diet rich in sponges helps shape coral communities."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.014
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 75.0
-weight_max_lb = 250.0
-sell_value_min = 66
-sell_value_max = 150
-sell_value_curve = 0.8
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/turtle_leatherback/turtle_leatherback.tres b/fish/species/turtle_leatherback/turtle_leatherback.tres
deleted file mode 100644
index 3a8acf3..0000000
--- a/fish/species/turtle_leatherback/turtle_leatherback.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"turtle_leatherback"
-display_name = "leatherback sea turtle"
-active = true
-catalog_number = 6203
-collection_group = &"Sea turtles"
-logbook_fact = "leatherback sea turtles cross entire oceans while hunting jellies. a flexible ridged shell and internal heat help them enter cold water."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.012
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 500.0
-weight_max_lb = 2000.0
-sell_value_min = 75
-sell_value_max = 175
-sell_value_curve = 0.76
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/turtle_loggerhead/turtle_loggerhead.tres b/fish/species/turtle_loggerhead/turtle_loggerhead.tres
deleted file mode 100644
index e4bd898..0000000
--- a/fish/species/turtle_loggerhead/turtle_loggerhead.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"turtle_loggerhead"
-display_name = "loggerhead sea turtle"
-active = true
-catalog_number = 6204
-collection_group = &"Sea turtles"
-logbook_fact = "loggerhead sea turtles crush crabs and mollusks with massive jaws. females often return near their birth beaches to nest."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.016
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 150.0
-weight_max_lb = 1200.0
-sell_value_min = 68
-sell_value_max = 155
-sell_value_curve = 0.77
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/urchin_purple/urchin_purple.tres b/fish/species/urchin_purple/urchin_purple.tres
deleted file mode 100644
index 6546627..0000000
--- a/fish/species/urchin_purple/urchin_purple.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"urchin_purple"
-display_name = "purple sea urchin"
-active = true
-catalog_number = 1903
-collection_group = &"Echinoderms"
-logbook_fact = "purple sea urchins scrape algae from rocks with a five-toothed feeding structure. dense groups can clear kelp from entire reefs."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.75
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.05
-weight_max_lb = 1.0
-sell_value_min = 5
-sell_value_max = 12
-sell_value_curve = 0.96
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/wahoo/wahoo.tres b/fish/species/wahoo/wahoo.tres
deleted file mode 100644
index 9e045de..0000000
--- a/fish/species/wahoo/wahoo.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"wahoo"
-display_name = "wahoo"
-active = true
-catalog_number = 4703
-collection_group = &"Oceanic pelagic fish"
-logbook_fact = "wahoo are streamlined open-ocean hunters with rows of sharp teeth. blue vertical bars flicker across their sides during fast pursuits."
-available_seasons = 6
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.11
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 10.0
-weight_max_lb = 100.0
-sell_value_min = 30
-sell_value_max = 70
-sell_value_curve = 0.82
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/walleye/fish_walleye.png b/fish/species/walleye/fish_walleye.png
deleted file mode 100644
index f4fd40a..0000000
Binary files a/fish/species/walleye/fish_walleye.png and /dev/null differ
diff --git a/fish/species/walleye/fish_walleye.png.import b/fish/species/walleye/fish_walleye.png.import
deleted file mode 100644
index 21e025e..0000000
--- a/fish/species/walleye/fish_walleye.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c10j6jv6fv32r"
-path="res://.godot/imported/fish_walleye.png-48ef5c2e01ff8de0726161700ea41c43.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://fish/species/walleye/fish_walleye.png"
-dest_files=["res://.godot/imported/fish_walleye.png-48ef5c2e01ff8de0726161700ea41c43.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/fish/species/walleye/walleye.tres b/fish/species/walleye/walleye.tres
deleted file mode 100644
index 277b3d8..0000000
--- a/fish/species/walleye/walleye.tres
+++ /dev/null
@@ -1,33 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" format=3 uid="uid://8j0f5dchtajt"]
-
-[ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"]
-
-[sub_resource type="Resource" id="WalleyeAvailability"]
-script = ExtResource("3_availability_script")
-allowed_location_tags = Array[StringName]([&"lake"])
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"walleye"
-display_name = "walleye"
-active = true
-catalog_number = 3306
-collection_group = &"Freshwater predators"
-logbook_fact = "walleye have light-sensitive eyes that help them hunt in murky water and around dusk. their reflective layer gives the eyes a pale glow."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.4
-catch_profile = ExtResource("2_profile")
-availability = SubResource("WalleyeAvailability")
-weight_max_lb = 15
-display_scale_min = 0.85
-display_scale_max = 1.7
-display_scale_curve = 0.9
-sell_value_min = 7
-sell_value_max = 15
-sell_value_curve = 0.9
-display_texture = ExtResource("4_texture")
diff --git a/fish/species/walrus/walrus.tres b/fish/species/walrus/walrus.tres
deleted file mode 100644
index ff74bf6..0000000
--- a/fish/species/walrus/walrus.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"walrus"
-display_name = "walrus"
-active = true
-catalog_number = 504
-collection_group = &"Coastal marine mammals"
-logbook_fact = "walruses sweep the seafloor for clams and other soft-bodied prey. long tusks help them haul onto ice and settle disputes."
-available_seasons = 9
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.012
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 800.0
-weight_max_lb = 3700.0
-sell_value_min = 74
-sell_value_max = 170
-sell_value_curve = 0.76
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/warmouth/warmouth.tres b/fish/species/warmouth/warmouth.tres
deleted file mode 100644
index c58e8c5..0000000
--- a/fish/species/warmouth/warmouth.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"warmouth"
-display_name = "warmouth"
-active = true
-catalog_number = 3106
-collection_group = &"Freshwater panfish"
-logbook_fact = "warmouth hide in weeds, roots, and murky backwaters before ambushing prey. red eyes and dark cheek stripes frame a notably large mouth."
-available_seasons = 7
-allowed_water_types = 1
-rarity = 1
-base_catch_weight = 0.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 2.0
-sell_value_min = 5
-sell_value_max = 10
-sell_value_curve = 0.95
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/weakfish/weakfish.tres b/fish/species/weakfish/weakfish.tres
deleted file mode 100644
index 83a5f11..0000000
--- a/fish/species/weakfish/weakfish.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"weakfish"
-display_name = "weakfish"
-active = true
-catalog_number = 6805
-collection_group = &"Temperate estuary"
-logbook_fact = "weakfish are named for delicate mouth tissue that tears easily around a hook. iridescent spots scatter across their bronze backs."
-available_seasons = 7
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.32
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 18.0
-sell_value_min = 12
-sell_value_max = 29
-sell_value_curve = 0.86
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/whale_blue/whale_blue.tres b/fish/species/whale_blue/whale_blue.tres
deleted file mode 100644
index 415008e..0000000
--- a/fish/species/whale_blue/whale_blue.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"whale_blue"
-display_name = "blue whale"
-active = true
-catalog_number = 7701
-collection_group = &"Whales and dolphins"
-logbook_fact = "blue whales are the largest animals known to have lived. pleated throats expand around vast mouthfuls of water that baleen filters for krill."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.005
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 100000.0
-weight_max_lb = 330000.0
-sell_value_min = 120
-sell_value_max = 300
-sell_value_curve = 0.72
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/whale_humpback/whale_humpback.tres b/fish/species/whale_humpback/whale_humpback.tres
deleted file mode 100644
index 7ad7161..0000000
--- a/fish/species/whale_humpback/whale_humpback.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"whale_humpback"
-display_name = "humpback whale"
-active = true
-catalog_number = 7703
-collection_group = &"Whales and dolphins"
-logbook_fact = "humpback whales use long pectoral fins while turning and feeding. males produce layered songs that change across breeding seasons."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.008
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 40000.0
-weight_max_lb = 90000.0
-sell_value_min = 105
-sell_value_max = 260
-sell_value_curve = 0.73
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/whale_killer/whale_killer.tres b/fish/species/whale_killer/whale_killer.tres
deleted file mode 100644
index 7952089..0000000
--- a/fish/species/whale_killer/whale_killer.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-require_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"whale_killer"
-display_name = "killer whale"
-active = true
-catalog_number = 7704
-collection_group = &"Whales and dolphins"
-logbook_fact = "killer whales live in close family groups with distinct calls and hunting traditions. different populations specialize in different prey."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.008
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 3000.0
-weight_max_lb = 12000.0
-sell_value_min = 95
-sell_value_max = 230
-sell_value_curve = 0.74
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/whale_sperm/whale_sperm.tres b/fish/species/whale_sperm/whale_sperm.tres
deleted file mode 100644
index eef3aed..0000000
--- a/fish/species/whale_sperm/whale_sperm.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_day = false
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"whale_sperm"
-display_name = "sperm whale"
-active = true
-catalog_number = 7705
-collection_group = &"Whales and dolphins"
-logbook_fact = "sperm whales make deep dives to hunt squid with powerful echolocation clicks. their enormous square heads contain a waxy spermaceti organ."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 4
-base_catch_weight = 0.006
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 15000.0
-weight_max_lb = 125000.0
-sell_value_min = 115
-sell_value_max = 280
-sell_value_curve = 0.72
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/white_perch/white_perch.tres b/fish/species/white_perch/white_perch.tres
deleted file mode 100644
index 5543307..0000000
--- a/fish/species/white_perch/white_perch.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"white_perch"
-display_name = "white perch"
-active = true
-catalog_number = 6806
-collection_group = &"Temperate estuary"
-logbook_fact = "white perch tolerate fresh and brackish water along the atlantic coast. deep silver bodies gather in schools around river mouths."
-available_seasons = 7
-allowed_water_types = 3
-rarity = 1
-base_catch_weight = 1.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 3.5
-sell_value_min = 5
-sell_value_max = 11
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/whitefish_lake/whitefish_lake.tres b/fish/species/whitefish_lake/whitefish_lake.tres
deleted file mode 100644
index 51246f5..0000000
--- a/fish/species/whitefish_lake/whitefish_lake.tres
+++ /dev/null
@@ -1,30 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"whitefish_lake"
-display_name = "lake whitefish"
-active = true
-catalog_number = 1003
-collection_group = &"Cold freshwater fish"
-logbook_fact = "lake whitefish browse deep, cold bottoms with small downward-pointing mouths. large schools move toward shallows to spawn in autumn."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 1.5
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.5
-weight_max_lb = 12.0
-sell_value_min = 4
-sell_value_max = 11
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/wolffish_atlantic/wolffish_atlantic.tres b/fish/species/wolffish_atlantic/wolffish_atlantic.tres
deleted file mode 100644
index ca67a92..0000000
--- a/fish/species/wolffish_atlantic/wolffish_atlantic.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-forbid_rain = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"wolffish_atlantic"
-display_name = "atlantic wolffish"
-active = true
-catalog_number = 1201
-collection_group = &"Cold reef fish"
-logbook_fact = "atlantic wolffish use heavy jaws and blunt teeth to crush shellfish and urchins. natural antifreeze proteins help them endure icy water."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.25
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 2.0
-weight_max_lb = 40.0
-sell_value_min = 14
-sell_value_max = 34
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/wolffish_spotted/wolffish_spotted.tres b/fish/species/wolffish_spotted/wolffish_spotted.tres
deleted file mode 100644
index 36b4575..0000000
--- a/fish/species/wolffish_spotted/wolffish_spotted.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"wolffish_spotted"
-display_name = "spotted wolffish"
-active = true
-catalog_number = 1203
-collection_group = &"Cold reef fish"
-logbook_fact = "spotted wolffish crush hard-shelled prey with broad molar-like teeth. their pale spots stand out against a dark cold-water body."
-available_seasons = 13
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.11
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 3.0
-weight_max_lb = 50.0
-sell_value_min = 26
-sell_value_max = 60
-sell_value_curve = 0.84
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/wrasse_ballan/wrasse_ballan.tres b/fish/species/wrasse_ballan/wrasse_ballan.tres
deleted file mode 100644
index a1beebc..0000000
--- a/fish/species/wrasse_ballan/wrasse_ballan.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"wrasse_ballan"
-display_name = "ballan wrasse"
-active = true
-catalog_number = 6101
-collection_group = &"Reef wrasses"
-logbook_fact = "ballan wrasse crush shellfish and sea urchins with strong throat teeth. thick lips and mottled colors suit rocky european reefs."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 1
-base_catch_weight = 0.8
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 10.0
-sell_value_min = 6
-sell_value_max = 15
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/wrasse_cleaner/wrasse_cleaner.tres b/fish/species/wrasse_cleaner/wrasse_cleaner.tres
deleted file mode 100644
index 85db0fc..0000000
--- a/fish/species/wrasse_cleaner/wrasse_cleaner.tres
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-forbid_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"wrasse_cleaner"
-display_name = "cleaner wrasse"
-active = true
-catalog_number = 6102
-collection_group = &"Reef wrasses"
-logbook_fact = "cleaner wrasses advertise with a bright stripe and dance at reef stations. larger fish wait while parasites are picked from skin and gills."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 2
-base_catch_weight = 0.35
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.01
-weight_max_lb = 0.2
-sell_value_min = 9
-sell_value_max = 20
-sell_value_curve = 1.0
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/wreckfish/wreckfish.tres b/fish/species/wreckfish/wreckfish.tres
deleted file mode 100644
index 345d274..0000000
--- a/fish/species/wreckfish/wreckfish.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-require_fog = true
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"wreckfish"
-display_name = "wreckfish"
-active = true
-catalog_number = 1606
-collection_group = &"Deep shelf fish"
-logbook_fact = "wreckfish gather around caves, ledges, and shipwrecks in deep water. powerful jaws let them ambush fish and large crustaceans."
-available_seasons = 15
-allowed_water_types = 2
-rarity = 3
-base_catch_weight = 0.1
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 10.0
-weight_max_lb = 180.0
-sell_value_min = 31
-sell_value_max = 76
-sell_value_curve = 0.8
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fish/species/yellow_perch/yellow_perch.tres b/fish/species/yellow_perch/yellow_perch.tres
deleted file mode 100644
index 5098a1e..0000000
--- a/fish/species/yellow_perch/yellow_perch.tres
+++ /dev/null
@@ -1,31 +0,0 @@
-[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3]
-
-[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
-[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
-[ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"]
-[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"]
-
-[sub_resource type="Resource" id="Availability"]
-script = ExtResource("3_availability")
-allow_night = false
-
-[resource]
-script = ExtResource("1_fish_data")
-id = &"yellow_perch"
-display_name = "yellow perch"
-active = true
-catalog_number = 3201
-collection_group = &"Freshwater perch"
-logbook_fact = "yellow perch travel in schools through cool lakes and slow rivers. dark vertical bars and orange lower fins make them easy to recognize."
-available_seasons = 13
-allowed_water_types = 1
-rarity = 0
-base_catch_weight = 2.2
-catch_profile = ExtResource("2_profile")
-availability = SubResource("Availability")
-weight_min_lb = 0.2
-weight_max_lb = 4.0
-sell_value_min = 3
-sell_value_max = 8
-sell_value_curve = 0.9
-display_texture = ExtResource("99_fish_placeholder")
diff --git a/fishing/bass_catch_profile.tres b/fishing/bass_catch_profile.tres
index 24024a7..077b229 100644
--- a/fishing/bass_catch_profile.tres
+++ b/fishing/bass_catch_profile.tres
@@ -6,6 +6,11 @@
script = ExtResource("1_profile")
barrier_count_min = 3
barrier_count_max = 4
+barrier_health_min = 4
+barrier_health_max = 7
first_barrier_margin = 0.14
final_barrier_margin = 0.1
minimum_barrier_spacing = 0.14
+chase_start_delay = 1.0
+chase_start_offset = 0.16
+chase_speed = 0.145
diff --git a/fishing/carp_catch_profile.tres b/fishing/carp_catch_profile.tres
index e1e0efe..bc3f5dc 100644
--- a/fishing/carp_catch_profile.tres
+++ b/fishing/carp_catch_profile.tres
@@ -6,6 +6,11 @@
script = ExtResource("1_profile")
barrier_count_min = 3
barrier_count_max = 4
+barrier_health_min = 5
+barrier_health_max = 8
first_barrier_margin = 0.13
final_barrier_margin = 0.09
minimum_barrier_spacing = 0.14
+chase_start_delay = 1.1
+chase_start_offset = 0.18
+chase_speed = 0.15
diff --git a/fishing/catch_controller.gd b/fishing/catch_controller.gd
index 12cc431..350bafa 100644
--- a/fishing/catch_controller.gd
+++ b/fishing/catch_controller.gd
@@ -4,7 +4,6 @@ extends Node
const CatchDifficultyProfileType = preload(
"res://fishing/catch_difficulty_profile.gd"
)
-const FishQualityType = preload("res://fish/fish_quality.gd")
signal encounter_updated(
progress: float,
@@ -43,13 +42,6 @@ class Barrier:
maximum_health = barrier_health
-# Fight movement uses one shared baseline. Species and quality difficulty comes
-# from barrier placement and health, while reel upgrades affect only the
-# player's progress speed.
-const CHASE_START_DELAY: float = 1.5
-const CHASE_START_OFFSET: float = 0.04
-const CHASE_SPEED: float = 0.07
-
@export_category("Accessibility")
@export var auto_click_enabled: bool = false
@export_range(0.05, 2.0, 0.01) var auto_click_interval: float = 0.20
@@ -68,8 +60,9 @@ var _active_barrier_index: int = -1
var _reel_input_held: bool = false
var _reel_speed: float = 0.0
var _click_power: int = 1
-var _fish_quality: int = FishQualityType.Tier.BORING
+var _chase_start_delay: float = 0.0
var _chase_delay_remaining: float = 0.0
+var _chase_speed: float = 0.0
var _failure_epsilon: float = 0.0001
var _auto_click_accumulator: float = 0.0
var _rng: RandomNumberGenerator = RandomNumberGenerator.new()
@@ -110,7 +103,6 @@ func start_encounter(
profile: CatchDifficultyProfileType,
reel_speed: float,
click_power: int,
- fish_quality: int = FishQualityType.Tier.BORING,
) -> void:
reset()
if profile == null:
@@ -119,13 +111,10 @@ func start_encounter(
_reel_speed = maxf(reel_speed, 0.0)
_click_power = maxi(click_power, 1)
- _fish_quality = (
- fish_quality
- if FishQualityType.is_valid(fish_quality)
- else FishQualityType.Tier.BORING
- )
- _chase_delay_remaining = CHASE_START_DELAY
- chase_progress = -CHASE_START_OFFSET
+ _chase_start_delay = maxf(profile.chase_start_delay, 0.0)
+ _chase_delay_remaining = _chase_start_delay
+ chase_progress = -maxf(profile.chase_start_offset, 0.01)
+ _chase_speed = maxf(profile.chase_speed, 0.0)
_seed_encounter_rng()
_generate_barriers(profile)
progress = 0.0
@@ -133,27 +122,6 @@ func start_encounter(
_emit_encounter_update()
-func start_authoritative_encounter(
- profile: CatchDifficultyProfileType,
- reel_speed: float,
- click_power: int,
- encounter_seed_value: int,
- fish_quality: int = FishQualityType.Tier.BORING,
-) -> void:
- var previous_test_mode: bool = use_deterministic_test_seed
- var previous_seed: int = deterministic_test_seed
- use_deterministic_test_seed = true
- deterministic_test_seed = encounter_seed_value
- start_encounter(
- profile,
- reel_speed,
- click_power,
- fish_quality,
- )
- use_deterministic_test_seed = previous_test_mode
- deterministic_test_seed = previous_seed
-
-
func set_reel_input(held: bool) -> void:
_reel_input_held = held
if not held:
@@ -185,8 +153,9 @@ func reset() -> void:
_reel_input_held = false
_reel_speed = 0.0
_click_power = 1
- _fish_quality = FishQualityType.Tier.BORING
+ _chase_start_delay = 0.0
_chase_delay_remaining = 0.0
+ _chase_speed = 0.0
_auto_click_accumulator = 0.0
emit_signal(
"encounter_updated",
@@ -231,7 +200,7 @@ func _update_chase(delta: float) -> void:
if active_delta <= 0.0:
return
chase_progress = minf(
- chase_progress + CHASE_SPEED * active_delta,
+ chase_progress + _chase_speed * active_delta,
1.0
)
@@ -294,6 +263,7 @@ func _seed_encounter_rng() -> void:
func _generate_barriers(profile: CatchDifficultyProfileType) -> void:
var count_range: Vector2i = profile.get_barrier_count_range()
+ var health_range: Vector2i = profile.get_barrier_health_range()
var interval: Vector2 = profile.get_generation_interval()
var spacing: float = maxf(profile.minimum_barrier_spacing, 0.01)
var available_distance: float = maxf(interval.y - interval.x, 0.0)
@@ -317,9 +287,7 @@ func _generate_barriers(profile: CatchDifficultyProfileType) -> void:
+ spacing * float(barrier_index)
+ random_offsets[barrier_index] * random_slack
)
- var health: int = FishQualityType.roll_barrier_health(
- _rng, _fish_quality
- )
+ var health: int = _rng.randi_range(health_range.x, health_range.y)
_barriers.append(Barrier.new(position, health))
diff --git a/fishing/catch_difficulty_profile.gd b/fishing/catch_difficulty_profile.gd
index 60aa190..3ad20ad 100644
--- a/fishing/catch_difficulty_profile.gd
+++ b/fishing/catch_difficulty_profile.gd
@@ -3,9 +3,14 @@ extends Resource
@export_range(0, 12, 1) var barrier_count_min: int = 2
@export_range(0, 12, 1) var barrier_count_max: int = 4
+@export_range(1, 100, 1) var barrier_health_min: int = 3
+@export_range(1, 100, 1) var barrier_health_max: int = 7
@export_range(0.0, 0.45, 0.01) var first_barrier_margin: float = 0.15
@export_range(0.0, 0.45, 0.01) var final_barrier_margin: float = 0.10
@export_range(0.01, 1.0, 0.01) var minimum_barrier_spacing: float = 0.15
+@export_range(0.0, 5.0, 0.05) var chase_start_delay: float = 1.0
+@export_range(0.01, 0.5, 0.01) var chase_start_offset: float = 0.15
+@export_range(0.0, 2.0, 0.01) var chase_speed: float = 0.14
func get_barrier_count_range() -> Vector2i:
@@ -13,6 +18,11 @@ func get_barrier_count_range() -> Vector2i:
return Vector2i(minimum, maxi(minimum, barrier_count_max))
+func get_barrier_health_range() -> Vector2i:
+ var minimum: int = maxi(1, barrier_health_min)
+ return Vector2i(minimum, maxi(minimum, barrier_health_max))
+
+
func get_generation_interval() -> Vector2:
var start: float = clampf(first_barrier_margin, 0.0, 0.95)
var finish: float = clampf(1.0 - final_barrier_margin, start, 1.0)
diff --git a/fishing/common_catch_profile.tres b/fishing/common_catch_profile.tres
index 86c2b74..1169e2e 100644
--- a/fishing/common_catch_profile.tres
+++ b/fishing/common_catch_profile.tres
@@ -6,6 +6,11 @@
script = ExtResource("1_profile")
barrier_count_min = 2
barrier_count_max = 4
+barrier_health_min = 3
+barrier_health_max = 7
first_barrier_margin = 0.15
final_barrier_margin = 0.1
minimum_barrier_spacing = 0.15
+chase_start_delay = 1.0
+chase_start_offset = 0.15
+chase_speed = 0.14
diff --git a/fishing/fishing_context.gd b/fishing/fishing_context.gd
index d9fcdb5..55b119f 100644
--- a/fishing/fishing_context.gd
+++ b/fishing/fishing_context.gd
@@ -1,14 +1,8 @@
class_name FishingContext
extends RefCounted
-const CalendarSeasonType = preload("res://world/calendar_season.gd")
-
var location_tags: Array[StringName] = []
-var water_type: WaterType.Type = WaterType.Type.FRESH_WATER
var active_event_tags: Array[StringName] = []
var active_bait_tags: Array[StringName] = []
var is_night: bool = false
-var is_day_night_transition: bool = false
var is_raining: bool = false
-var is_foggy: bool = false
-var season: int = CalendarSeasonType.UNKNOWN
diff --git a/fishing/fishing_presentation.gd b/fishing/fishing_presentation.gd
index 947976c..a44ed7f 100644
--- a/fishing/fishing_presentation.gd
+++ b/fishing/fishing_presentation.gd
@@ -3,7 +3,6 @@ extends Node3D
signal cast_completed
signal outcome_completed(outcome: StringName)
-signal presentation_interrupted
enum VisualMode {
NONE,
@@ -19,67 +18,100 @@ enum LineMode {
SLACK,
}
+@export_category("Water")
+@export var water_surface_offset_y: float = -0.72
+@export_range(0.1, 10.0, 0.1) var pickup_distance_from_player: float = 5.5
+
@export_category("Fishing Line")
+@export_flags_3d_physics var line_obstacle_mask: int = 1
+@export_range(1.0, 30.0, 0.5) var routing_probe_height: float = 8.0
+@export_range(0.1, 2.0, 0.05) var obstruction_sample_spacing: float = 0.4
+@export_range(0.01, 1.0, 0.01) var terrain_clearance: float = 0.1
+@export_range(1, 3, 1) var maximum_obstruction_ranges: int = 1
+@export_range(2, 12, 1) var maximum_contact_anchors_per_range: int = 5
+@export_range(0.0, 3.0, 0.05) var route_lead_in_distance: float = 0.8
+@export_range(0.0, 3.0, 0.05) var route_lead_out_distance: float = 0.8
+@export_range(0.05, 2.0, 0.05) var fallback_arch_height: float = 0.35
@export_range(0.0, 0.5, 0.01) var slack_amount: float = 0.08
@export_range(0.0, 2.0, 0.01) var minimum_slack: float = 0.08
@export_range(0.0, 3.0, 0.01) var maximum_slack: float = 0.65
-@export_range(2, 20, 1) var sag_interpolation_count: int = 8
+@export_range(2, 20, 1) var clear_span_interpolation_count: int = 8
+@export_range(1, 8, 1) var contact_span_interpolation_count: int = 2
@export_range(0.1, 30.0, 0.1) var line_transition_speed: float = 7.0
-@export_range(0.002, 0.05, 0.001) var line_thickness: float = 0.016
@export_category("Cast")
+@export_range(0.1, 30.0, 0.1) var target_movement_smoothing: float = 14.0
@export_range(0.1, 3.0, 0.05) var cast_travel_duration: float = 0.65
@export_range(0.1, 5.0, 0.1) var cast_arc_height: float = 2.0
@export_range(0.0, 90.0, 1.0) var maximum_rod_cock_degrees: float = 50.0
@export_range(0.0, 60.0, 1.0) var rod_forward_swing_degrees: float = 22.0
@export_range(0.05, 1.0, 0.01) var rod_forward_swing_duration: float = 0.12
@export_range(0.05, 1.0, 0.01) var rod_recovery_duration: float = 0.22
+@export var valid_target_material: Material
+@export var invalid_target_material: Material
-@export_category("Bobber Idle")
-@export_range(0.0, 0.2, 0.005) var bobber_bob_amplitude: float = 0.035
-@export_range(0.1, 3.0, 0.05) var bobber_bob_cycles_per_second: float = 0.7
-@export_range(0.0, 12.0, 0.5) var bobber_tilt_degrees: float = 4.0
-@onready var _target_marker: Node3D = %CastTargetMarker
-@onready var _valid_target_marker: MeshInstance3D = %ValidTargetMarker
-@onready var _invalid_target_marker: Node3D = %InvalidTargetMarker
+@onready var _target_marker: MeshInstance3D = %CastTargetMarker
+@onready var _invalid_cross_a: MeshInstance3D = %InvalidCrossA
+@onready var _invalid_cross_b: MeshInstance3D = %InvalidCrossB
@onready var _bobber: MeshInstance3D = %Bobber
@onready var _line: MeshInstance3D = %FishingLine
var _mode: VisualMode = VisualMode.NONE
var _line_mode: LineMode = LineMode.HIDDEN
var _line_mesh: ImmediateMesh = ImmediateMesh.new()
+var _route_points: PackedVector3Array = PackedVector3Array()
+var _contact_start_anchor_index: int = -1
+var _contact_end_anchor_index: int = -1
var _current_sag: float = 0.0
var _rod: Node3D
var _rod_tip: Marker3D
var _rod_neutral_rotation: Vector3
-var _cast_arrival_position: Vector3
-var _active_cast_arc_height: float = 0.0
-var _bobber_surface_position: Vector3
-var _bobber_idle_elapsed: float = 0.0
-var _bobber_base_scale: Vector3 = Vector3.ONE
+var _target_goal: Vector3
+var _cast_position: Vector3
+var _pickup_position: Vector3
var _active_tween: Tween
var _rod_tween: Tween
var _bite_tween: Tween
+var _default_water_surface_offset_y: float
func _ready() -> void:
+ _default_water_surface_offset_y = water_surface_offset_y
_line.mesh = _line_mesh
cleanup()
func _process(delta: float) -> void:
- if _mode == VisualMode.FISHING and _bobber.visible:
- _bobber_idle_elapsed += delta
- _apply_bobber_idle_motion()
+ if _mode == VisualMode.AIMING:
+ var weight: float = 1.0 - exp(-target_movement_smoothing * delta)
+ _target_marker.global_position = _target_marker.global_position.lerp(
+ _target_goal,
+ weight
+ )
if _line_mode != LineMode.HIDDEN:
_update_line(delta)
+func get_water_surface_height() -> float:
+ return global_position.y + water_surface_offset_y
+
+
+func set_water_surface_height(surface_height: float) -> void:
+ water_surface_offset_y = surface_height - global_position.y
+
+
+func reset_water_surface_height() -> void:
+ water_surface_offset_y = _default_water_surface_offset_y
+
+
func set_line_mode(mode: LineMode) -> void:
_line_mode = mode
_line.visible = mode != LineMode.HIDDEN
if mode == LineMode.HIDDEN:
_current_sag = 0.0
+ _route_points.clear()
+ _contact_start_anchor_index = -1
+ _contact_end_anchor_index = -1
_line_mesh.clear_surfaces()
@@ -87,20 +119,18 @@ func begin_aim(
rod_tip: Marker3D,
rod: Node3D,
minimum_target: Vector3,
- target_normal: Vector3,
target_is_fishable: bool,
- character_scale: float = 1.0,
) -> void:
cleanup()
if rod_tip == null or rod == null:
return
- _bobber_base_scale = Vector3.ONE * maxf(character_scale, 0.01)
_mode = VisualMode.AIMING
_rod = rod
_rod_tip = rod_tip
_rod_neutral_rotation = _rod.rotation
- _set_target_surface(minimum_target, target_normal)
+ _target_goal = minimum_target
+ _target_marker.global_position = minimum_target
_target_marker.scale = Vector3.ONE
_target_marker.visible = true
_set_target_validity(target_is_fishable)
@@ -117,38 +147,19 @@ func update_rod_charge(charge: float) -> void:
func update_aim_target(
target: Vector3,
- target_normal: Vector3,
charge: float,
target_is_fishable: bool,
) -> void:
if _mode != VisualMode.AIMING:
return
- # The gameplay resolver already advances the horizontal target smoothly.
- # Snap each sample to its resolved surface so interpolation can never draw
- # the marker through a vertical shoreline between two valid endpoints.
- _set_target_surface(target, target_normal)
+ _target_goal = target
var maximum_response: float = smoothstep(0.9, 1.0, clampf(charge, 0.0, 1.0))
_target_marker.scale = Vector3.ONE * lerpf(1.0, 1.15, maximum_response)
_set_target_validity(target_is_fishable)
-func _set_target_surface(target: Vector3, surface_normal: Vector3) -> void:
- _target_marker.global_position = target
- var resolved_normal: Vector3 = surface_normal.normalized()
- if resolved_normal.is_zero_approx():
- resolved_normal = Vector3.UP
- _target_marker.global_basis = Basis(
- Quaternion(Vector3.UP, resolved_normal)
- )
-
-
-func begin_cast(
- target: Vector3,
- blocked_landing_position: Vector3 = Vector3.ZERO,
- cast_is_blocked: bool = false,
- resolved_arc_height: float = -1.0,
-) -> void:
+func begin_cast(target: Vector3) -> void:
if _mode != VisualMode.AIMING or _rod_tip == null:
return
@@ -156,15 +167,10 @@ func begin_cast(
_mode = VisualMode.CASTING
_target_marker.visible = false
_bobber.visible = true
- _bobber.scale = _bobber_base_scale
- _cast_arrival_position = (
- blocked_landing_position if cast_is_blocked else target
- )
- _active_cast_arc_height = (
- maxf(resolved_arc_height, 0.0)
- if resolved_arc_height >= 0.0
- else cast_arc_height
- )
+ _bobber.scale = Vector3.ONE
+ _cast_position = _with_water_height(target)
+ _pickup_position = _calculate_pickup_position(_cast_position)
+
var cast_start: Vector3 = _rod_tip.global_position
_bobber.global_position = cast_start
_begin_rod_release()
@@ -172,15 +178,11 @@ func begin_cast(
_active_tween.set_trans(Tween.TRANS_SINE)
_active_tween.set_ease(Tween.EASE_IN_OUT)
_active_tween.tween_method(
- _set_cast_sample.bind(cast_start, _cast_arrival_position),
+ _set_cast_sample.bind(cast_start, _cast_position),
0.0,
1.0,
cast_travel_duration
)
- if cast_is_blocked:
- # Let the bobber visibly settle on the blocking surface before the
- # normal invalid-cast return animation begins.
- _active_tween.tween_interval(0.16)
_active_tween.finished.connect(_on_cast_tween_finished)
@@ -189,53 +191,58 @@ func show_bite() -> void:
return
_kill_bite_tween()
- _bobber.scale = _bobber_base_scale
+ _bobber.scale = Vector3.ONE
_bite_tween = create_tween()
_bite_tween.set_trans(Tween.TRANS_QUAD)
_bite_tween.set_ease(Tween.EASE_IN_OUT)
_bite_tween.tween_property(
_bobber,
"scale",
- _bobber_base_scale * 0.68,
+ Vector3.ONE * 0.68,
0.08
)
_bite_tween.tween_property(
_bobber,
"scale",
- _bobber_base_scale * 1.2,
+ Vector3.ONE * 1.2,
0.1
)
_bite_tween.tween_property(
_bobber,
"scale",
- _bobber_base_scale,
+ Vector3.ONE,
0.1
)
_bite_tween.finished.connect(_on_bite_tween_finished)
-func show_withdrawal_position(world_position: Vector3) -> void:
+func show_withdrawal_position(position: Vector3) -> void:
if _mode != VisualMode.FISHING:
return
_kill_active_tween()
- _bobber_surface_position = world_position
- _apply_bobber_idle_motion()
+ _bobber.global_position = _with_water_height(position)
-func begin_fight() -> void:
+func begin_reeling() -> void:
if _mode != VisualMode.FISHING:
return
_kill_active_tween()
+ _cast_position = _with_water_height(_bobber.global_position)
+
+
+func show_reel_position(position: Vector3, _input_held: bool) -> void:
+ if _mode != VisualMode.FISHING:
+ return
+
+ _kill_active_tween()
+ _bobber.global_position = _with_water_height(position)
func play_outcome(outcome: StringName) -> void:
if _mode == VisualMode.NONE:
cleanup()
- # Gameplay completion must not depend on a presentation tween still
- # being active. The caller owns idempotency for the outcome.
- outcome_completed.emit(outcome)
return
_kill_active_tween()
@@ -243,12 +250,6 @@ func play_outcome(outcome: StringName) -> void:
_kill_rod_tween()
_restore_rod_neutral()
_mode = VisualMode.OUTCOME
- _bobber.rotation = Vector3.ZERO
- if outcome == &"withdrawal":
- # Withdrawal is a visible return, not an instant cancellation. Keep the
- # bobber present even if the preceding reel update hid or reset it.
- _bobber.visible = true
- _bobber.scale = _bobber_base_scale
_active_tween = create_tween()
_active_tween.set_trans(Tween.TRANS_QUAD)
_active_tween.set_ease(Tween.EASE_IN)
@@ -259,7 +260,12 @@ func play_outcome(outcome: StringName) -> void:
_target_marker.visible = false
match outcome:
&"catch":
- _queue_bobber_return(0.35, 0.45)
+ var catch_target: Vector3 = _bobber.global_position
+ if _rod_tip != null:
+ catch_target = _rod_tip.global_position
+ _active_tween.set_parallel(true)
+ _active_tween.tween_property(_bobber, "global_position", catch_target, 0.35)
+ _active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.35)
&"escape":
var start_x: float = _bobber.global_position.x
_active_tween.tween_property(
@@ -274,9 +280,31 @@ func play_outcome(outcome: StringName) -> void:
start_x + 0.18,
0.08
)
- _queue_bobber_return(0.42, 0.65)
- &"invalid", &"withdrawal":
- _queue_bobber_return(0.42, 0.65)
+ _active_tween.tween_property(
+ _bobber,
+ "global_position:y",
+ get_water_surface_height() - 0.4,
+ 0.2
+ )
+ &"invalid":
+ var return_target: Vector3 = _bobber.global_position
+ if _rod_tip != null:
+ return_target = _rod_tip.global_position
+ _active_tween.set_parallel(true)
+ _active_tween.tween_property(_bobber, "global_position", return_target, 0.28)
+ _active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.28)
+ &"withdrawal":
+ var withdrawal_target: Vector3 = _bobber.global_position
+ if _rod_tip != null:
+ withdrawal_target = _rod_tip.global_position
+ _active_tween.set_parallel(true)
+ _active_tween.tween_property(
+ _bobber,
+ "global_position",
+ withdrawal_target,
+ 0.28
+ )
+ _active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.28)
_:
_active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.18)
@@ -293,16 +321,10 @@ func cleanup() -> void:
_rod_tip = null
_target_marker.visible = false
_target_marker.scale = Vector3.ONE
- _valid_target_marker.visible = true
- _invalid_target_marker.visible = false
+ _invalid_cross_a.visible = false
+ _invalid_cross_b.visible = false
_bobber.visible = false
_bobber.scale = Vector3.ONE
- _bobber_base_scale = Vector3.ONE
- _bobber.rotation = Vector3.ZERO
- _cast_arrival_position = Vector3.ZERO
- _active_cast_arc_height = 0.0
- _bobber_surface_position = Vector3.ZERO
- _bobber_idle_elapsed = 0.0
set_line_mode(LineMode.HIDDEN)
@@ -317,36 +339,7 @@ func _set_cast_sample(
target: Vector3,
) -> void:
var sample: Vector3 = start.lerp(target, progress)
- sample.y += sin(progress * PI) * _active_cast_arc_height
- _bobber.global_position = sample
-
-
-func _queue_bobber_return(duration: float, arc_scale: float) -> void:
- var return_start: Vector3 = _bobber.global_position
- var return_target: Vector3 = return_start
- if _rod_tip != null:
- return_target = _rod_tip.global_position
- _active_tween.tween_method(
- _set_return_sample.bind(return_start, return_target, arc_scale),
- 0.0,
- 1.0,
- duration,
- )
- _active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.1)
-
-
-func _set_return_sample(
- progress: float,
- start: Vector3,
- target: Vector3,
- arc_scale: float,
-) -> void:
- var eased_progress: float = 1.0 - pow(1.0 - progress, 3.0)
- var sample: Vector3 = start.lerp(target, eased_progress)
- sample.y += sin(eased_progress * PI) * maxf(
- cast_arc_height * arc_scale,
- 0.75,
- )
+ sample.y += sin(progress * PI) * cast_arc_height
_bobber.global_position = sample
@@ -355,30 +348,36 @@ func _on_cast_tween_finished() -> void:
return
_active_tween = null
- _bobber.global_position = _cast_arrival_position
- _bobber_surface_position = _cast_arrival_position
- _bobber_idle_elapsed = 0.0
+ _bobber.global_position = _cast_position
_mode = VisualMode.FISHING
cast_completed.emit()
-func _apply_bobber_idle_motion() -> void:
- var phase: float = _bobber_idle_elapsed * bobber_bob_cycles_per_second * TAU
- _bobber.global_position = (
- _bobber_surface_position
- + Vector3.UP * sin(phase) * bobber_bob_amplitude
- )
- _bobber.rotation.z = (
- deg_to_rad(bobber_tilt_degrees) * sin(phase * 0.73)
+func _calculate_pickup_position(target: Vector3) -> Vector3:
+ var player_on_water: Vector3 = target
+ if _rod_tip != null:
+ player_on_water = _with_water_height(_rod_tip.global_position)
+ var outward: Vector3 = target - player_on_water
+ outward.y = 0.0
+ if outward.is_zero_approx():
+ outward = Vector3.FORWARD
+ else:
+ outward = outward.normalized()
+ var target_distance: float = player_on_water.distance_to(target)
+ var pickup_distance: float = minf(
+ pickup_distance_from_player,
+ target_distance * 0.8
)
+ return player_on_water + outward * pickup_distance
+
+
+func _with_water_height(position: Vector3) -> Vector3:
+ return Vector3(position.x, get_water_surface_height(), position.z)
func _update_line(delta: float) -> void:
if _rod_tip == null or not is_instance_valid(_rod_tip):
- var was_active: bool = _mode != VisualMode.NONE
cleanup()
- if was_active:
- presentation_interrupted.emit()
return
var rod_tip_position: Vector3 = _rod_tip.global_position
@@ -393,78 +392,323 @@ func _update_line(delta: float) -> void:
)
var sag_weight: float = 1.0 - exp(-line_transition_speed * delta)
_current_sag = lerpf(_current_sag, target_sag, sag_weight)
- var rendered_points: PackedVector3Array = _build_sagged_line_points(
- rod_tip_position,
- bobber_position,
- _current_sag,
+ _route_points = _build_line_route(rod_tip_position, bobber_position)
+ var rendered_points: PackedVector3Array = _build_rendered_line_points(
+ _route_points,
+ _current_sag
)
_line_mesh.clear_surfaces()
if rendered_points.size() < 2:
return
- _draw_line_ribbon(rendered_points)
-
-
-func _draw_line_ribbon(points: PackedVector3Array) -> void:
- var camera: Camera3D = get_viewport().get_camera_3d()
- if camera == null:
- _line_mesh.surface_begin(Mesh.PRIMITIVE_LINE_STRIP)
- for point: Vector3 in points:
- _line_mesh.surface_add_vertex(to_local(point))
- _line_mesh.surface_end()
- return
-
- var half_thickness: float = line_thickness * 0.5
- var ribbon_vertices := PackedVector3Array()
- for point_index: int in range(1, points.size()):
- var start: Vector3 = points[point_index - 1]
- var end: Vector3 = points[point_index]
- var segment: Vector3 = end - start
- if segment.length_squared() <= 0.000001:
- continue
- var to_camera: Vector3 = (
- camera.global_position - start.lerp(end, 0.5)
- )
- var side: Vector3 = segment.normalized().cross(to_camera.normalized())
- if side.length_squared() <= 0.000001:
- side = segment.normalized().cross(Vector3.UP)
- if side.length_squared() <= 0.000001:
- side = segment.normalized().cross(Vector3.RIGHT)
- side = side.normalized() * half_thickness
-
- var start_left: Vector3 = to_local(start - side)
- var start_right: Vector3 = to_local(start + side)
- var end_right: Vector3 = to_local(end + side)
- var end_left: Vector3 = to_local(end - side)
- ribbon_vertices.append(start_left)
- ribbon_vertices.append(start_right)
- ribbon_vertices.append(end_right)
- ribbon_vertices.append(start_left)
- ribbon_vertices.append(end_right)
- ribbon_vertices.append(end_left)
- if ribbon_vertices.is_empty():
- return
- _line_mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLES)
- for vertex: Vector3 in ribbon_vertices:
- _line_mesh.surface_add_vertex(vertex)
+ _line_mesh.surface_begin(Mesh.PRIMITIVE_LINES)
+ for point_index: int in range(1, rendered_points.size()):
+ _line_mesh.surface_add_vertex(to_local(rendered_points[point_index - 1]))
+ _line_mesh.surface_add_vertex(to_local(rendered_points[point_index]))
_line_mesh.surface_end()
-func _build_sagged_line_points(
- start: Vector3,
- end: Vector3,
+func _build_line_route(start: Vector3, end: Vector3) -> PackedVector3Array:
+ _contact_start_anchor_index = -1
+ _contact_end_anchor_index = -1
+ if _raycast_obstacle(start, end).is_empty():
+ return PackedVector3Array([start, end])
+
+ var horizontal_delta := Vector2(end.x - start.x, end.z - start.z)
+ var horizontal_length: float = horizontal_delta.length()
+ if horizontal_length <= 0.001:
+ return _build_fallback_arch(start, end)
+
+ var sample_count: int = clampi(
+ ceili(horizontal_length / obstruction_sample_spacing),
+ 2,
+ 64
+ )
+ var obstructed_samples := PackedByteArray()
+ for sample_index: int in range(sample_count + 1):
+ var progress: float = float(sample_index) / float(sample_count)
+ var base_point: Vector3 = start.lerp(end, progress)
+ var surface_hit: Dictionary = _probe_surface(base_point)
+ var is_obstructed: bool = false
+ if not surface_hit.is_empty():
+ var surface_position: Vector3 = surface_hit["position"]
+ var required_height: float = surface_position.y + terrain_clearance
+ if required_height > base_point.y:
+ is_obstructed = true
+ obstructed_samples.append(1 if is_obstructed else 0)
+
+ var obstruction_ranges: Array[Vector2i] = _find_obstruction_ranges(
+ obstructed_samples
+ )
+ if obstruction_ranges.is_empty():
+ return _build_fallback_arch(start, end)
+ if obstruction_ranges.size() > maximum_obstruction_ranges:
+ return _build_fallback_arch(start, end)
+
+ var first_sample: int = obstruction_ranges[0].x
+ var last_sample: int = obstruction_ranges[obstruction_ranges.size() - 1].y
+ var first_progress: float = float(first_sample) / float(sample_count)
+ var last_progress: float = float(last_sample) / float(sample_count)
+ var lead_in_progress: float = maxf(
+ 0.0,
+ first_progress - route_lead_in_distance / horizontal_length
+ )
+ var lead_out_progress: float = minf(
+ 1.0,
+ last_progress + route_lead_out_distance / horizontal_length
+ )
+
+ var route := PackedVector3Array([start])
+ _append_unique_route_point(route, start.lerp(end, lead_in_progress))
+ _contact_start_anchor_index = route.size() - 1
+ _append_contact_anchors(
+ route,
+ start,
+ end,
+ first_sample,
+ last_sample,
+ sample_count
+ )
+ _append_unique_route_point(route, start.lerp(end, lead_out_progress))
+ _contact_end_anchor_index = route.size() - 1
+ _append_unique_route_point(route, end)
+ route = _remove_near_duplicate_route_points(route)
+ _update_contact_indices_after_reduction(route)
+ return _validate_reduced_route(route, start, end)
+
+
+func _build_rendered_line_points(
+ route: PackedVector3Array,
sag: float,
) -> PackedVector3Array:
var points := PackedVector3Array()
- var subdivisions: int = 1 if sag <= 0.001 else sag_interpolation_count
- points.append(start)
+ if route.is_empty():
+ return points
+ if (
+ _contact_start_anchor_index < 0
+ or _contact_end_anchor_index < _contact_start_anchor_index
+ or _contact_end_anchor_index >= route.size()
+ ):
+ _append_clear_span(points, route[0], route[route.size() - 1], sag)
+ return points
+
+ _append_clear_span(
+ points,
+ route[0],
+ route[_contact_start_anchor_index],
+ sag
+ )
+ for anchor_index: int in range(
+ _contact_start_anchor_index,
+ _contact_end_anchor_index
+ ):
+ _append_contact_span(
+ points,
+ route[anchor_index],
+ route[anchor_index + 1]
+ )
+ _append_clear_span(
+ points,
+ route[_contact_end_anchor_index],
+ route[route.size() - 1],
+ sag
+ )
+ return points
+
+
+func _raycast_obstacle(from: Vector3, to: Vector3) -> Dictionary:
+ if from.is_equal_approx(to):
+ return {}
+ var query := PhysicsRayQueryParameters3D.create(
+ from,
+ to,
+ line_obstacle_mask
+ )
+ query.collide_with_areas = false
+ query.collide_with_bodies = true
+ return get_world_3d().direct_space_state.intersect_ray(query)
+
+
+func _find_obstruction_ranges(
+ obstructed_samples: PackedByteArray,
+) -> Array[Vector2i]:
+ var ranges: Array[Vector2i] = []
+ var range_start: int = -1
+ for sample_index: int in range(obstructed_samples.size()):
+ var is_obstructed: bool = obstructed_samples[sample_index] != 0
+ if is_obstructed and range_start < 0:
+ range_start = sample_index
+ elif not is_obstructed and range_start >= 0:
+ ranges.append(Vector2i(range_start, sample_index - 1))
+ range_start = -1
+ if range_start >= 0:
+ ranges.append(Vector2i(range_start, obstructed_samples.size() - 1))
+ return ranges
+
+
+func _append_contact_anchors(
+ route: PackedVector3Array,
+ start: Vector3,
+ end: Vector3,
+ first_sample: int,
+ last_sample: int,
+ sample_count: int,
+) -> void:
+ var available_samples: int = last_sample - first_sample + 1
+ var anchor_count: int = mini(
+ available_samples,
+ maximum_contact_anchors_per_range
+ )
+ for anchor_offset: int in range(anchor_count):
+ var weight: float = 0.0
+ if anchor_count > 1:
+ weight = float(anchor_offset) / float(anchor_count - 1)
+ var sample_index: int = roundi(
+ lerpf(float(first_sample), float(last_sample), weight)
+ )
+ var progress: float = float(sample_index) / float(sample_count)
+ var contact: Vector3 = _raise_point_above_obstacle(
+ start.lerp(end, progress)
+ )
+ _append_unique_route_point(route, contact)
+
+
+func _append_unique_route_point(
+ route: PackedVector3Array,
+ point: Vector3,
+) -> void:
+ if route.is_empty() or point.distance_to(route[route.size() - 1]) > 0.03:
+ route.append(point)
+
+
+func _remove_near_duplicate_route_points(
+ route: PackedVector3Array,
+) -> PackedVector3Array:
+ var cleaned := PackedVector3Array()
+ for point: Vector3 in route:
+ _append_unique_route_point(cleaned, point)
+ return cleaned
+
+
+func _update_contact_indices_after_reduction(route: PackedVector3Array) -> void:
+ if route.size() <= 2:
+ _contact_start_anchor_index = -1
+ _contact_end_anchor_index = -1
+ return
+ _contact_start_anchor_index = 1
+ _contact_end_anchor_index = route.size() - 2
+
+
+func _validate_reduced_route(
+ route: PackedVector3Array,
+ start: Vector3,
+ end: Vector3,
+) -> PackedVector3Array:
+ var correction_index: int = -1
+ var correction_hit: Dictionary
+ for segment_index: int in range(route.size() - 1):
+ var hit: Dictionary = _raycast_obstacle(
+ route[segment_index],
+ route[segment_index + 1]
+ )
+ if not hit.is_empty():
+ correction_index = segment_index + 1
+ correction_hit = hit
+ break
+ if correction_index < 0:
+ return route
+
+ var hit_position: Vector3 = correction_hit["position"]
+ var correction: Vector3 = _raise_point_above_obstacle(hit_position)
+ route.insert(correction_index, correction)
+ _contact_start_anchor_index = mini(
+ _contact_start_anchor_index,
+ correction_index
+ )
+ _contact_end_anchor_index = maxi(
+ _contact_end_anchor_index + 1,
+ correction_index
+ )
+ for segment_index: int in range(route.size() - 1):
+ if not _raycast_obstacle(
+ route[segment_index],
+ route[segment_index + 1]
+ ).is_empty():
+ return _build_fallback_arch(start, end)
+ return route
+
+
+func _append_clear_span(
+ points: PackedVector3Array,
+ start: Vector3,
+ end: Vector3,
+ sag: float,
+) -> void:
+ if points.is_empty():
+ points.append(start)
+ elif not points[points.size() - 1].is_equal_approx(start):
+ points.append(start)
+ var subdivisions: int = 1 if sag <= 0.001 else clear_span_interpolation_count
for step: int in range(1, subdivisions + 1):
var progress: float = float(step) / float(subdivisions)
var point: Vector3 = start.lerp(end, progress)
if step < subdivisions and sag > 0.0:
point.y -= 4.0 * progress * (1.0 - progress) * sag
+ point.y = maxf(
+ point.y,
+ get_water_surface_height() + terrain_clearance * 0.25
+ )
+ point = _raise_point_above_obstacle(point)
points.append(point)
- return points
+
+
+func _append_contact_span(
+ points: PackedVector3Array,
+ start: Vector3,
+ end: Vector3,
+) -> void:
+ if points.is_empty():
+ points.append(start)
+ elif not points[points.size() - 1].is_equal_approx(start):
+ points.append(start)
+ for step: int in range(1, contact_span_interpolation_count + 1):
+ var progress: float = (
+ float(step) / float(contact_span_interpolation_count)
+ )
+ var point: Vector3 = start.lerp(end, progress)
+ if step < contact_span_interpolation_count:
+ point = _raise_point_above_obstacle(point)
+ points.append(point)
+
+
+func _build_fallback_arch(
+ start: Vector3,
+ end: Vector3,
+) -> PackedVector3Array:
+ var route := PackedVector3Array([start])
+ for progress: float in [0.25, 0.5, 0.75]:
+ var point: Vector3 = start.lerp(end, progress)
+ point.y += sin(progress * PI) * fallback_arch_height
+ route.append(_raise_point_above_obstacle(point))
+ route.append(end)
+ _contact_start_anchor_index = 1
+ _contact_end_anchor_index = route.size() - 2
+ return route
+
+
+func _probe_surface(point: Vector3) -> Dictionary:
+ var probe_start: Vector3 = point + Vector3.UP * routing_probe_height
+ var probe_end: Vector3 = point - Vector3.UP * routing_probe_height
+ return _raycast_obstacle(probe_start, probe_end)
+
+
+func _raise_point_above_obstacle(point: Vector3) -> Vector3:
+ var hit: Dictionary = _probe_surface(point)
+ if hit.is_empty():
+ return point
+ var surface_position: Vector3 = hit["position"]
+ point.y = maxf(point.y, surface_position.y + terrain_clearance)
+ return point
func _begin_rod_release() -> void:
@@ -498,8 +742,12 @@ func _restore_rod_neutral() -> void:
func _set_target_validity(target_is_fishable: bool) -> void:
- _valid_target_marker.visible = target_is_fishable
- _invalid_target_marker.visible = not target_is_fishable
+ if target_is_fishable:
+ _target_marker.material_override = valid_target_material
+ else:
+ _target_marker.material_override = invalid_target_material
+ _invalid_cross_a.visible = not target_is_fishable
+ _invalid_cross_b.visible = not target_is_fishable
func _kill_active_tween() -> void:
@@ -513,7 +761,7 @@ func _kill_bite_tween() -> void:
_bite_tween.kill()
_bite_tween = null
if is_instance_valid(_bobber):
- _bobber.scale = _bobber_base_scale
+ _bobber.scale = Vector3.ONE
func _on_bite_tween_finished() -> void:
diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd
index 8160eec..e010a4a 100644
--- a/fishing/fishing_spot.gd
+++ b/fishing/fishing_spot.gd
@@ -9,13 +9,8 @@ const CatchControllerType = preload("res://fishing/catch_controller.gd")
const FishingContextType = preload("res://fishing/fishing_context.gd")
const FishingPresentationType = preload("res://fishing/fishing_presentation.gd")
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
-const FishExperienceType = preload("res://fish/fish_experience.gd")
-const PlayerExperienceType = preload(
- "res://progression/player_experience.gd"
-)
const ItemCatalogType = preload("res://items/item_catalog.gd")
const ItemDataType = preload("res://items/item_data.gd")
-const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
const PlayerBagType = preload("res://inventory/player_bag.gd")
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
const PlayerFishingUpgradesType = preload(
@@ -31,23 +26,8 @@ const PlayerType = preload("res://player/player.gd")
const FishableWaterRegionType = preload(
"res://world/fishable_water_region.gd"
)
-const NetworkSessionType = preload("res://network/network_session.gd")
-const NetworkFishingServiceType = preload(
- "res://network/network_fishing_service.gd"
-)
-const FishingSurfaceSampleType = preload(
- "res://fishing/fishing_surface_sample.gd"
-)
-const FishingSurfaceResolverType = preload(
- "res://fishing/fishing_surface_resolver.gd"
-)
-const WorldTimeServiceType = preload("res://world/world_time_service.gd")
-const WorldWeatherServiceType = preload(
- "res://world/world_weather_service.gd"
-)
signal status_changed(status: String)
-signal local_speech_requested(message: String)
signal catch_display_changed(
progress: float,
chase_progress: float,
@@ -61,12 +41,9 @@ signal showcase_changed(
fish_name: String,
rarity_name: String,
weight_lb: float,
- quality: int,
visible: bool,
)
signal bite_activated
-signal bite_prompt_changed(is_visible: bool)
-signal fishing_input_priority_changed(active: bool)
signal ready_for_equipment_refresh
enum FishingState {
@@ -76,7 +53,6 @@ enum FishingState {
WAITING_FOR_BITE,
FIGHTING,
SHOWING_CATCH,
- RETURNING,
COOLDOWN,
}
@@ -89,30 +65,17 @@ enum FishingState {
@export_category("Target Preview")
@export_flags_3d_physics var preview_surface_mask: int = 5
-@export_range(1.0, 200.0, 1.0) var preview_ray_start_height: float = 100.0
-@export_range(1.0, 400.0, 1.0) var preview_surface_ray_length: float = 240.0
+@export_range(1.0, 100.0, 1.0) var preview_ray_start_height: float = 30.0
+@export_range(1.0, 200.0, 1.0) var preview_ray_length: float = 60.0
@export_range(0.01, 1.0, 0.01) var preview_marker_vertical_offset: float = 0.06
-@export_range(0.0, 0.5, 0.01) var solid_probe_clearance: float = 0.04
-@export_range(0.01, 0.5, 0.01) var solid_bobber_clearance: float = 0.13
@export_category("Withdrawal")
-@export_range(0.1, 20.0, 0.1) var withdrawal_rate: float = 2.45
+@export_range(0.1, 20.0, 0.1) var withdrawal_rate: float = 4.9
@export_range(0.1, 10.0, 0.1) var withdrawal_cancel_distance: float = 1.0
-@export_range(0.0, 1.0, 0.01) var withdrawal_surface_clearance: float = 0.4
@export_category("Timing")
-const BITE_QUICK_MIN_SECONDS: float = 6.0
-const BITE_QUICK_MAX_SECONDS: float = 18.0
-const BITE_TYPICAL_MAX_SECONDS: float = 48.0
-const BITE_LONG_MAX_SECONDS: float = 72.0
-const BITE_MAX_SECONDS: float = 90.0
-const BITE_QUICK_PROBABILITY: float = 0.30
-const BITE_TYPICAL_PROBABILITY: float = 0.65
-const BITE_LONG_PROBABILITY: float = 0.04
-const NETWORK_INPUT_RESEND_INTERVAL_SECONDS: float = 0.1
-const ALTERNATE_REEL_ACTION: StringName = &"reel_alternate"
+@export_range(0.1, 30.0, 0.1) var wait_time: float = 2.0
@export_range(0.1, 10.0, 0.1) var cooldown_duration: float = 1.0
-@export_range(0.1, 5.0, 0.05) var minimum_showcase_duration: float = 0.8
@export_category("Selection")
@export_range(0.0, 10.0, 0.05) var undiscovered_weight_multiplier: float = 1.5
@@ -120,161 +83,80 @@ const ALTERNATE_REEL_ACTION: StringName = &"reel_alternate"
@export var deterministic_selection_seed: int = 24680
@export_category("Context Testing")
+@export var context_is_night: bool = false
+@export var context_is_raining: bool = false
@export var context_event_tags: Array[StringName] = []
@export var context_bait_tags: Array[StringName] = []
@onready var _catch_controller: CatchControllerType = %CatchController
@onready var _presentation: FishingPresentationType = %FishingPresentation
-@onready var _bobber_audio: AudioStreamPlayer = %BobberAudio
-@onready var _fight_audio: AudioStreamPlayer = %FightAudio
-@onready var _reeling_audio: AudioStreamPlayer = %ReelingAudio
var state: FishingState = FishingState.READY
var _external_input_blocked: bool = false
var _gameplay_input_enabled: bool = false
-var _fishing_environment_enabled: bool = true
var _local_menu_input_owners: Dictionary[StringName, bool] = {}
var _local_player: PlayerType
var _local_inventory: FishInventoryType
var _local_collection_log: CollectionLogType
-var _local_experience: PlayerExperienceType
var _local_bag: PlayerBagType
var _local_hotbar: PlayerHotbarType
var _item_catalog: ItemCatalogType
var _fishing_upgrades: PlayerFishingUpgradesType
var _item_effects: PlayerItemEffectsType
-var _network_item_use: NetworkItemUseService
var _cooler_capacity: PlayerCoolerCapacityType
-var _network_session: NetworkSessionType
-var _network_fishing: NetworkFishingServiceType
-var _world_time: WorldTimeServiceType
-var _world_weather: WorldWeatherServiceType
-var _fishable_water_regions_resolver := Callable()
var _active_player: PlayerType
var _state_time_remaining: float = 0.0
var _cast_charge: float = 0.0
var _cast_direction: Vector3 = Vector3.FORWARD
var _cast_origin_position: Vector3
var _cast_target: Vector3
-var _preferred_cast_arc_height: float = -1.0
+var _cast_landing_is_fishable: bool = false
var _withdrawal_endpoint: Vector3
var _withdrawal_progress: float = 0.0
var _withdrawal_input_held: bool = false
-var _network_primary_input_held: bool = false
-var _network_input_resend_elapsed: float = 0.0
var _new_cast_press_armed: bool = true
var _bobber_water_position: Vector3
+var _fight_start_position: Vector3
var _cooldown_status: String = ""
+var _fishable_query_shape: CylinderShape3D = CylinderShape3D.new()
var _fish_selector: FishSelectorType = FishSelectorType.new()
-var _surface_resolver: FishingSurfaceResolverType = FishingSurfaceResolverType.new()
-var _aim_surface_sample: FishingSurfaceSampleType = FishingSurfaceSampleType.new()
-var _cast_path_is_clear: bool = false
var _selected_water_region: FishableWaterRegionType
var _selection_context: FishingContextType
var _selected_fish: FishDataType
var _pending_catch: FishCatchType
var _showcase_ready: bool = false
var _put_away_press_armed: bool = false
-var _showcase_input_lock_remaining: float = 0.0
var _showcase_restore_generation: int = 0
-var _showcase_outcome_completed: bool = false
-var _network_auto_click_accumulator: float = 0.0
-var _network_active_barrier_index: int = -1
-var _bite_rng: RandomNumberGenerator = RandomNumberGenerator.new()
-var _pending_cleanup_message: String = ""
-var _bite_confirmation_pending: bool = false
-var _fishing_input_priority_active: bool = false
-var _bite_confirmation_requested: bool = false
func _ready() -> void:
- _bite_rng.randomize()
- _configure_surface_resolver()
- var fight_stream := _fight_audio.stream as AudioStreamWAV
- if fight_stream != null:
- _configure_audio_loop(fight_stream)
- var reeling_stream := _reeling_audio.stream as AudioStreamWAV
- if reeling_stream != null:
- _configure_audio_loop(reeling_stream)
_catch_controller.encounter_updated.connect(_on_catch_encounter_updated)
_catch_controller.caught.connect(_on_catch_completed)
_catch_controller.escaped.connect(_on_catch_escaped)
_presentation.cast_completed.connect(_on_cast_completed)
_presentation.outcome_completed.connect(_on_outcome_completed)
- _presentation.presentation_interrupted.connect(
- _on_presentation_interrupted
- )
func setup(
local_player: PlayerType,
local_inventory: FishInventoryType,
local_collection_log: CollectionLogType,
- local_experience: PlayerExperienceType,
local_bag: PlayerBagType,
local_hotbar: PlayerHotbarType,
item_catalog: ItemCatalogType,
fishing_upgrades: PlayerFishingUpgradesType,
item_effects: PlayerItemEffectsType,
- network_item_use: NetworkItemUseService,
cooler_capacity: PlayerCoolerCapacityType,
- network_session: NetworkSessionType = null,
- network_fishing: NetworkFishingServiceType = null,
- world_time: WorldTimeServiceType = null,
- world_weather: WorldWeatherServiceType = null,
- fishable_water_regions_resolver: Callable = Callable(),
) -> void:
_local_player = local_player
_local_inventory = local_inventory
_local_collection_log = local_collection_log
- _local_experience = local_experience
_local_bag = local_bag
_local_hotbar = local_hotbar
_item_catalog = item_catalog
_fishing_upgrades = fishing_upgrades
_item_effects = item_effects
- _network_item_use = network_item_use
- if _network_item_use != null:
- _network_item_use.local_item_use_finished.connect(
- _on_network_item_use_finished
- )
_cooler_capacity = cooler_capacity
- _network_session = network_session
- _network_fishing = network_fishing
- _world_time = world_time
- _world_weather = world_weather
- _fishable_water_regions_resolver = fishable_water_regions_resolver
- if (
- _network_session != null
- and not _network_session.state_changed.is_connected(
- _on_network_session_state_changed
- )
- ):
- _network_session.state_changed.connect(
- _on_network_session_state_changed
- )
- if _network_fishing != null:
- _network_fishing.local_cast_accepted.connect(
- _on_network_cast_accepted
- )
- _network_fishing.local_cast_rejected.connect(
- _on_network_cast_rejected
- )
- _network_fishing.local_bite_started.connect(
- _on_network_bite_started
- )
- _network_fishing.local_bite_pending.connect(
- _on_network_bite_pending
- )
- _network_fishing.local_snapshot_received.connect(
- _on_network_fishing_snapshot
- )
- _network_fishing.local_catch_received.connect(
- _on_network_catch_received
- )
- _network_fishing.local_attempt_ended.connect(
- _on_network_attempt_ended
- )
if not _local_inventory.catches_changed.is_connected(
_on_cooler_availability_changed
):
@@ -313,8 +195,6 @@ func can_open_system_menu() -> bool:
func can_open_fishing_shop() -> bool:
return (
- _can_use_shop_gameplay()
- and
_gameplay_input_enabled
and not _external_input_blocked
and _local_menu_input_owners.is_empty()
@@ -322,19 +202,8 @@ func can_open_fishing_shop() -> bool:
)
-func can_use_surface_drawing() -> bool:
- return (
- _gameplay_input_enabled
- and not _external_input_blocked
- and state == FishingState.READY
- and _active_player == null
- )
-
-
func is_ready_for_shop_transaction() -> bool:
return (
- _can_use_shop_gameplay()
- and
_gameplay_input_enabled
and not _external_input_blocked
and state == FishingState.READY
@@ -370,18 +239,17 @@ func handle_escape_action() -> bool:
func set_local_menu_input_suppressed(
- input_owner: StringName,
+ owner: StringName,
suppressed: bool,
) -> void:
- if input_owner.is_empty():
+ if owner.is_empty():
return
if suppressed:
- _local_menu_input_owners[input_owner] = true
+ _local_menu_input_owners[owner] = true
else:
- _local_menu_input_owners.erase(input_owner)
+ _local_menu_input_owners.erase(owner)
if suppressed and state == FishingState.WAITING_FOR_BITE:
_withdrawal_input_held = false
- _stop_reeling_audio()
_presentation.set_line_mode(
FishingPresentationType.LineMode.SLACK
)
@@ -391,41 +259,6 @@ func set_gameplay_input_enabled(enabled: bool) -> void:
_gameplay_input_enabled = enabled
if not enabled:
_local_menu_input_owners.clear()
- _stop_reeling_audio()
- # FishingSpot survives session and gameplay transitions. Restore the
- # player's canonical movement state before input is disabled so a cast
- # lock cannot follow that player into the next session.
- reset_transient_state()
-
-
-func set_fishing_environment_enabled(enabled: bool) -> void:
- if _fishing_environment_enabled == enabled:
- return
- _fishing_environment_enabled = enabled
- if enabled:
- _new_cast_press_armed = not Input.is_action_pressed("fish_primary")
- return
- if _network_fishing != null and _network_fishing.has_local_attempt():
- _network_fishing.cancel_local_attempt("")
- reset_transient_state()
-
-
-func is_fishing_environment_enabled() -> bool:
- return _fishing_environment_enabled
-
-
-func reset_transient_state(restore_movement: bool = true) -> void:
- var local_state_was_active: bool = (
- state != FishingState.READY or _active_player != null
- )
- if local_state_was_active and _active_player == null and _local_player != null:
- _local_player.set_fighting_visual(false)
- _local_player.set_fishing_visual(false)
- _local_player.end_catch_showcase(Callable(), true)
- if restore_movement:
- _local_player.set_movement_enabled(true)
- _finalize_attempt_cleanup("", restore_movement)
- _new_cast_press_armed = not Input.is_action_pressed("fish_primary")
func configure_accessibility_auto_click(
@@ -437,21 +270,15 @@ func configure_accessibility_auto_click(
func begin_water_recovery() -> void:
_external_input_blocked = true
- if state == FishingState.SHOWING_CATCH:
+ if state in [
+ FishingState.AIMING_CAST,
+ FishingState.CASTING,
+ FishingState.WAITING_FOR_BITE,
+ FishingState.FIGHTING,
+ ]:
+ _cancel_attempt()
+ elif state == FishingState.SHOWING_CATCH:
_secure_showcase_catch_for_recovery()
- elif is_fishing_sequence_active():
- if state in [
- FishingState.AIMING_CAST,
- FishingState.CASTING,
- FishingState.WAITING_FOR_BITE,
- FishingState.FIGHTING,
- ]:
- _cancel_attempt()
- # Recovery replaces every in-flight rod return. This includes RETURNING,
- # which can be entered by Escape a frame before the water trigger fires.
- # Finish cleanup now so recovery never snapshots the cast movement lock.
- if state != FishingState.READY or _active_player != null:
- _finalize_attempt_cleanup("")
func end_water_recovery() -> void:
@@ -464,21 +291,18 @@ func _secure_showcase_catch_for_recovery() -> void:
and _local_inventory != null
and _local_collection_log != null
):
- _store_catch_progression(_pending_catch)
+ _local_inventory.add_catch(_pending_catch)
+ _local_collection_log.mark_discovered(_pending_catch.fish_id)
_pending_catch = null
_showcase_ready = false
_put_away_press_armed = false
- _showcase_input_lock_remaining = 0.0
- showcase_changed.emit("", "", 0.0, 0, false)
+ showcase_changed.emit("", "", 0.0, false)
if _active_player != null:
_active_player.end_catch_showcase(Callable(), true)
_cleanup_attempt()
func _exit_tree() -> void:
- _stop_fight_audio()
- _stop_reeling_audio()
- _set_fishing_input_priority(false)
_showcase_restore_generation += 1
if (
state == FishingState.SHOWING_CATCH
@@ -488,7 +312,8 @@ func _exit_tree() -> void:
and _local_collection_log != null
and is_instance_valid(_local_collection_log)
):
- _store_catch_progression(_pending_catch)
+ _local_inventory.add_catch(_pending_catch)
+ _local_collection_log.mark_discovered(_pending_catch.fish_id)
_pending_catch = null
if _active_player != null and is_instance_valid(_active_player):
_active_player.end_catch_showcase(Callable(), true)
@@ -499,21 +324,14 @@ func _exit_tree() -> void:
_selected_fish = null
_showcase_ready = false
_put_away_press_armed = false
- _showcase_input_lock_remaining = 0.0
func _process(delta: float) -> void:
if state == FishingState.READY and not Input.is_action_pressed("fish_primary"):
_new_cast_press_armed = true
- if state == FishingState.SHOWING_CATCH:
- _showcase_input_lock_remaining = maxf(
- _showcase_input_lock_remaining - delta,
- 0.0,
- )
if (
state == FishingState.SHOWING_CATCH
and _showcase_ready
- and _showcase_input_lock_remaining <= 0.0
and not Input.is_action_pressed("fish_primary")
):
_put_away_press_armed = true
@@ -524,21 +342,14 @@ func _process(delta: float) -> void:
if not Input.is_action_pressed("fish_primary"):
_confirm_cast()
FishingState.WAITING_FOR_BITE:
- if _network_fishing == null:
- _update_waiting_for_bite(delta)
- else:
- _resend_network_input_state(delta)
+ _update_waiting_for_bite(delta)
FishingState.FIGHTING:
- if _network_fishing == null:
- _catch_controller.set_effective_stats(
- _get_effective_reel_speed(),
- _get_effective_barrier_damage()
- )
- if not _is_reel_input_pressed():
- _catch_controller.set_reel_input(false)
- else:
- _resend_network_input_state(delta)
- _update_network_auto_click(delta)
+ _catch_controller.set_effective_stats(
+ _get_effective_reel_speed(),
+ _get_effective_barrier_damage()
+ )
+ if not Input.is_action_pressed("fish_primary"):
+ _catch_controller.set_reel_input(false)
FishingState.COOLDOWN:
_state_time_remaining -= delta
if _state_time_remaining <= 0.0:
@@ -548,78 +359,59 @@ func _process(delta: float) -> void:
func _unhandled_input(event: InputEvent) -> void:
if (
not _gameplay_input_enabled
- or not _fishing_environment_enabled
or _external_input_blocked
or not _local_menu_input_owners.is_empty()
):
return
if _local_player == null or not _local_player.is_local_control_enabled():
return
- var primary_action: bool = event.is_action("fish_primary")
- var alternate_reel: bool = event.is_action(ALTERNATE_REEL_ACTION)
- if not primary_action and not (
- alternate_reel and state == FishingState.FIGHTING
- ):
- return
- # Fishing owns the primary action only after a rod starts a cast, or while
- # completing an already-active fishing sequence. An empty hand, decor,
- # gathering tools, and other items must remain available to their own world
- # interaction controllers.
- if state == FishingState.READY and not has_active_fishing_rod():
- return
- # A held keyboard key emits echo events. Treat the alternate reel exactly
- # like a mouse button: one barrier hit per physical press, while the normal
- # held-input path continues driving free reeling between barriers.
- if event is InputEventKey and event.echo:
- get_viewport().set_input_as_handled()
+ if not event.is_action("fish_primary"):
return
+
if event.is_pressed():
match state:
FishingState.READY:
if not _new_cast_press_armed:
return
+ var active_item: ItemDataType = _get_active_item()
if (
- _is_inventory_full()
+ active_item != null
+ and active_item.category == ItemDataType.Category.CONSUMABLE
+ ):
+ if _item_effects.use_consumable(active_item, _local_bag):
+ status_changed.emit(
+ _item_effects.get_feedback(active_item.item_id)
+ )
+ get_viewport().set_input_as_handled()
+ return
+ if not has_active_fishing_rod():
+ status_changed.emit("select a fishing rod to cast.")
+ get_viewport().set_input_as_handled()
+ return
+ if (
+ _is_cooler_full()
):
status_changed.emit(
- "inventory full. store or sell something before casting."
+ "cooler full. sell fish before casting again."
)
get_viewport().set_input_as_handled()
return
_begin_aiming(_local_player)
+ FishingState.WAITING_FOR_BITE:
+ _withdrawal_input_held = true
+ _presentation.set_line_mode(
+ FishingPresentationType.LineMode.TAUT
+ )
FishingState.FIGHTING:
- if _network_fishing != null:
- _network_primary_input_held = true
- _network_input_resend_elapsed = 0.0
- _network_fishing.submit_local_input(true, true)
- else:
- _catch_controller.handle_primary_pressed()
+ _catch_controller.handle_primary_pressed()
_presentation.set_line_mode(
FishingPresentationType.LineMode.TAUT
)
FishingState.SHOWING_CATCH:
- if (
- _showcase_ready
- and _showcase_input_lock_remaining <= 0.0
- and _put_away_press_armed
- ):
+ if _showcase_ready and _put_away_press_armed:
_put_away_catch()
else:
return
- FishingState.WAITING_FOR_BITE:
- if _bite_confirmation_pending:
- confirm_pending_bite()
- get_viewport().set_input_as_handled()
- return
- _withdrawal_input_held = true
- _start_reeling_audio()
- if _network_fishing != null:
- _network_primary_input_held = true
- _network_input_resend_elapsed = 0.0
- _network_fishing.submit_local_input(true, true)
- _presentation.set_line_mode(
- FishingPresentationType.LineMode.TAUT
- )
_:
return
get_viewport().set_input_as_handled()
@@ -627,107 +419,34 @@ func _unhandled_input(event: InputEvent) -> void:
_confirm_cast()
get_viewport().set_input_as_handled()
elif state == FishingState.WAITING_FOR_BITE:
- if _bite_confirmation_pending:
- get_viewport().set_input_as_handled()
- return
_withdrawal_input_held = false
- _stop_reeling_audio()
- if _network_fishing != null:
- _network_primary_input_held = false
- _network_input_resend_elapsed = 0.0
- _network_fishing.submit_local_input(false, false)
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
get_viewport().set_input_as_handled()
elif state == FishingState.FIGHTING:
- var reel_input_held: bool = _is_reel_input_pressed()
- if _network_fishing != null:
- _network_primary_input_held = reel_input_held
- _network_input_resend_elapsed = 0.0
- _network_fishing.submit_local_input(reel_input_held, false)
- else:
- _catch_controller.set_reel_input(reel_input_held)
+ _catch_controller.set_reel_input(false)
get_viewport().set_input_as_handled()
-func try_use_active_non_fishing_item() -> bool:
- if (
- not _gameplay_input_enabled
- or _external_input_blocked
- or not _local_menu_input_owners.is_empty()
- or state != FishingState.READY
- ):
- return false
- var active_item: ItemDataType = _get_active_item()
- if (
- active_item == null
- or (
- active_item.category != ItemDataType.Category.CONSUMABLE
- and active_item.item_id != PlayerItemEffectsType.FISH_FINDER_ID
- )
- ):
- return false
- if _network_item_use != null:
- _network_item_use.request_use(active_item.item_id)
- elif _item_effects.use_item(active_item, _local_bag):
- status_changed.emit(_item_effects.get_feedback(active_item.item_id))
- elif active_item.item_id == PlayerItemEffectsType.FISH_FINDER_ID:
- local_speech_requested.emit(
- PlayerItemEffectsType.FISH_FINDER_DEAD_MESSAGE
- )
- return true
-
-
-func _is_reel_input_pressed() -> bool:
- return (
- Input.is_action_pressed("fish_primary")
- or Input.is_action_pressed(ALTERNATE_REEL_ACTION)
- )
-
-
func _begin_aiming(player: PlayerType) -> void:
if state != FishingState.READY or _active_player != null:
return
+ _presentation.reset_water_surface_height()
_active_player = player
+ _active_player.set_movement_enabled(false)
_cast_direction = _capture_cast_direction(_active_player)
_cast_origin_position = _active_player.get_cast_origin_position()
_cast_charge = 0.0
_cast_target = _calculate_cast_target(minimum_cast_distance)
state = FishingState.AIMING_CAST
- _active_player.set_casting_visual()
- status_changed.emit("")
- _cast_path_is_clear = is_cast_path_clear(
- _get_cast_launch_position(),
- _cast_target,
- )
- var target_is_fishable: bool = (
- _aim_surface_sample.is_fishable() and _cast_path_is_clear
- )
- var preview_position: Vector3 = _aim_surface_sample.get_marker_position(
- preview_marker_vertical_offset
- )
+ status_changed.emit("hold left click to aim • release to cast")
+ var target_is_fishable: bool = is_target_fishable(_cast_target)
+ var preview_position: Vector3 = _resolve_preview_surface_position(_cast_target)
_presentation.begin_aim(
_active_player.get_fishing_rod_tip(),
_active_player.get_fishing_rod(),
preview_position,
- _aim_surface_sample.normal,
- target_is_fishable,
- _active_player.get_character_visual_scale(),
- )
-
-
-func _can_use_shared_gameplay() -> bool:
- return _network_session == null or _network_session.can_use_host_gameplay()
-
-
-func _can_use_shop_gameplay() -> bool:
- return (
- _network_session == null
- or _network_session.is_host()
- or (
- _network_session.is_joined_client()
- and _network_session.supports_server_capability(&"shop_v1")
- )
+ target_is_fishable
)
@@ -741,7 +460,7 @@ func has_active_fishing_rod() -> bool:
var item: ItemDataType = _get_active_item()
return (
item != null
- and item.is_available()
+ and item.is_valid()
and item.category == ItemDataType.Category.ROD
and item.usable
and item.equippable
@@ -759,22 +478,20 @@ func _get_active_item() -> ItemDataType:
if item_id.is_empty() or not _local_bag.owns_item(item_id):
return null
var item: ItemDataType = _item_catalog.get_item_by_id(item_id)
- return item if item != null and item.is_available() else null
+ return item if item != null and item.is_valid() else null
-func _get_active_rod() -> FishingRodDataType:
- return _get_active_item() as FishingRodDataType
-
-
-func _is_inventory_full() -> bool:
+func _is_cooler_full() -> bool:
return (
- _local_inventory != null
- and not _local_inventory.can_accept_catch()
+ _cooler_capacity != null
+ and _local_inventory != null
+ and _local_inventory.get_all_catches().size()
+ >= _cooler_capacity.get_capacity()
)
func _on_cooler_availability_changed() -> void:
- if state == FishingState.READY and not _is_inventory_full():
+ if state == FishingState.READY and not _is_cooler_full():
refresh_active_item_status()
@@ -786,120 +503,23 @@ func is_fighting() -> bool:
return state == FishingState.FIGHTING
-func confirm_pending_bite() -> void:
- if state != FishingState.WAITING_FOR_BITE or not _bite_confirmation_pending:
- return
- _bite_confirmation_requested = true
- if _network_fishing != null:
- _network_primary_input_held = false
- _network_input_resend_elapsed = 0.0
- _network_fishing.submit_local_input(false, true)
- else:
- _activate_bite(true)
-
-
-func _set_bite_confirmation_pending(is_pending: bool) -> void:
- if is_pending:
- # The Standby deliberately pauses before the fight begins. Keep chat and
- # menus available during that pause; fishing only owns input once the
- # player confirms the bite and enters the actual minigame.
- _set_fishing_input_priority(false)
- if _bite_confirmation_pending == is_pending:
- if not is_pending:
- _bite_confirmation_requested = false
- return
- _bite_confirmation_pending = is_pending
- _bite_confirmation_requested = false
- bite_prompt_changed.emit(is_pending)
-
-
-func _set_fishing_input_priority(active: bool) -> void:
- if _fishing_input_priority_active == active:
- return
- _fishing_input_priority_active = active
- fishing_input_priority_changed.emit(active)
-
-
-func is_fishing_input_priority_active() -> bool:
- return _fishing_input_priority_active
-
-
-func is_returning() -> bool:
- return state == FishingState.RETURNING
-
-
-func is_fishing_sequence_active() -> bool:
- return state in [
- FishingState.AIMING_CAST,
- FishingState.CASTING,
- FishingState.WAITING_FOR_BITE,
- FishingState.FIGHTING,
- FishingState.SHOWING_CATCH,
- FishingState.RETURNING,
- ]
-
-
func refresh_active_item_status() -> void:
if state == FishingState.READY and has_active_fishing_rod():
status_changed.emit("")
-func report_external_status(message: String) -> void:
- status_changed.emit(message)
-
-
-func present_external_catch(fish_catch: FishCatchType) -> bool:
- if (
- fish_catch == null
- or not fish_catch.is_valid()
- or state != FishingState.READY
- or _local_player == null
- ):
- return false
- _active_player = _local_player
- _pending_catch = fish_catch
- _active_player.set_movement_enabled(false)
- state = FishingState.SHOWING_CATCH
- _showcase_ready = true
- _showcase_outcome_completed = true
- _put_away_press_armed = false
- _showcase_input_lock_remaining = minimum_showcase_duration
- _active_player.begin_catch_showcase(fish_catch)
- showcase_changed.emit(
- fish_catch.fish.display_name,
- fish_catch.fish.get_rarity_name(),
- fish_catch.weight_lb,
- fish_catch.quality,
- true,
- )
- return true
-
-
func _update_cast_charge(delta: float) -> void:
if state != FishingState.AIMING_CAST:
return
- # Movement remains available during aiming. Re-sample the player transform
- # so the cast origin and facing follow the player until release.
- _cast_origin_position = _active_player.get_cast_origin_position()
- _cast_direction = _capture_cast_direction(_active_player)
_cast_charge = minf(_cast_charge + delta / cast_charge_duration, 1.0)
var maximum_distance: float = maxf(minimum_cast_distance, maximum_cast_distance)
var distance: float = lerpf(minimum_cast_distance, maximum_distance, _cast_charge)
_cast_target = _calculate_cast_target(distance)
- _cast_path_is_clear = is_cast_path_clear(
- _get_cast_launch_position(),
- _cast_target,
- )
- var target_is_fishable: bool = (
- _aim_surface_sample.is_fishable() and _cast_path_is_clear
- )
- var preview_position: Vector3 = _aim_surface_sample.get_marker_position(
- preview_marker_vertical_offset
- )
+ var target_is_fishable: bool = is_target_fishable(_cast_target)
+ var preview_position: Vector3 = _resolve_preview_surface_position(_cast_target)
_presentation.update_aim_target(
preview_position,
- _aim_surface_sample.normal,
_cast_charge,
target_is_fishable
)
@@ -910,71 +530,23 @@ func _confirm_cast() -> void:
if state != FishingState.AIMING_CAST:
return
- var cast_launch_position := _get_cast_launch_position()
- var resolved_cast_arc: Dictionary = _surface_resolver.resolve_cast_arc(
- get_world_3d().direct_space_state,
- cast_launch_position,
- _cast_target,
- _presentation.cast_arc_height,
- )
- var resolved_arc_height: float = float(
- resolved_cast_arc.get("arc_height", _presentation.cast_arc_height)
- )
- var resolved_cast_collision: Dictionary = resolved_cast_arc.get(
- "collision",
- {},
- )
- _cast_path_is_clear = resolved_cast_collision.is_empty()
- var cast_is_invalid: bool = (
- not _aim_surface_sample.is_fishable() or not _cast_path_is_clear
- )
- var arrival_position: Vector3 = _cast_target
- if not _cast_path_is_clear:
- var collision_position: Variant = resolved_cast_collision.get(
- "position",
- _cast_target,
- )
- if typeof(collision_position) == TYPE_VECTOR3:
- arrival_position = collision_position
- if _active_player != null:
- # Movement is available while aiming, then locks once the cast is
- # released so the active bobber/fishing event remains anchored.
- _active_player.set_movement_enabled(false)
state = FishingState.CASTING
- if _active_player != null:
- _active_player.set_release_visual()
- status_changed.emit("")
- _presentation.begin_cast(
- _cast_target,
- arrival_position,
- cast_is_invalid,
- resolved_arc_height,
- )
+ status_changed.emit("casting...")
+ _presentation.begin_cast(_cast_target)
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
func _on_cast_completed() -> void:
if state != FishingState.CASTING:
return
- if not _is_cast_target_valid(_cast_target):
- _cleanup_attempt("", &"invalid")
- return
- _play_bobber_audio()
- if _network_fishing != null:
- _network_fishing.request_local_cast(
- _cast_origin_position,
- _cast_target,
- _cast_charge,
- _build_network_evidence()
- )
- status_changed.emit("")
- return
_selected_water_region = get_fishable_water_region(_cast_target)
+ _cast_landing_is_fishable = _selected_water_region != null
+ if not _cast_landing_is_fishable:
+ _cleanup_attempt("can't fish there.", &"invalid")
+ return
_selection_context = _build_fishing_context(_selected_water_region)
- var active_rod: FishingRodDataType = _get_active_rod()
_fish_selector.undiscovered_weight_multiplier = undiscovered_weight_multiplier
- _fish_selector.active_rod = active_rod
_fish_selector.rarity_weight_multipliers = []
for rarity: int in range(FishDataType.Rarity.size()):
_fish_selector.rarity_weight_multipliers.append(
@@ -995,75 +567,29 @@ func _on_cast_completed() -> void:
return
state = FishingState.WAITING_FOR_BITE
- _active_player.set_fishing_visual(true)
- _state_time_remaining = roll_bite_wait_time() * (
+ _state_time_remaining = wait_time * (
_item_effects.get_bite_time_multiplier()
if _item_effects != null
else 1.0
- ) * (
- active_rod.bite_time_multiplier
- if active_rod != null
- else 1.0
)
_withdrawal_progress = 0.0
_withdrawal_input_held = false
- _network_auto_click_accumulator = 0.0
- _network_active_barrier_index = -1
_bobber_water_position = _cast_target
_withdrawal_endpoint = Vector3(
_cast_origin_position.x + _cast_direction.x * withdrawal_cancel_distance,
- _cast_target.y,
+ _presentation.get_water_surface_height(),
_cast_origin_position.z + _cast_direction.z * withdrawal_cancel_distance
)
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
- status_changed.emit("")
-
-
-func _consume_active_bait() -> bool:
- if _active_player == null or _active_player.active_bait_id.is_empty():
- return true
- if _local_bag == null or not _local_bag.remove_item(_active_player.active_bait_id, 1):
- _active_player.unequip_bait()
- return false
- return true
-
-
-func roll_bite_wait_time() -> float:
- var bucket: float = _bite_rng.randf()
- if bucket < BITE_QUICK_PROBABILITY:
- return _bite_rng.randf_range(
- BITE_QUICK_MIN_SECONDS,
- BITE_QUICK_MAX_SECONDS,
- )
- if bucket < BITE_QUICK_PROBABILITY + BITE_TYPICAL_PROBABILITY:
- return _bite_rng.randf_range(
- BITE_QUICK_MAX_SECONDS,
- BITE_TYPICAL_MAX_SECONDS,
- )
- if bucket < (
- BITE_QUICK_PROBABILITY
- + BITE_TYPICAL_PROBABILITY
- + BITE_LONG_PROBABILITY
- ):
- return _bite_rng.randf_range(
- BITE_TYPICAL_MAX_SECONDS,
- BITE_LONG_MAX_SECONDS,
- )
- return _bite_rng.randf_range(
- BITE_LONG_MAX_SECONDS,
- BITE_MAX_SECONDS,
- )
+ status_changed.emit("waiting for a bite...")
func _update_waiting_for_bite(delta: float) -> void:
- if _bite_confirmation_pending:
- return
if (
_withdrawal_input_held
and not Input.is_action_pressed("fish_primary")
):
_withdrawal_input_held = false
- _stop_reeling_audio()
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
if not _withdrawal_input_held:
@@ -1104,18 +630,16 @@ func _update_withdrawal(delta: float) -> void:
_withdrawal_endpoint,
next_progress
)
- var withdrawal_surface: FishingSurfaceSampleType = (
- resolve_safe_withdrawal_surface(
- _bobber_water_position,
- desired_position,
- )
+ var clamped_position: Vector3 = find_last_fishable_position(
+ _bobber_water_position,
+ desired_position
)
- if not withdrawal_surface.is_fishable():
+ _withdrawal_progress = next_progress
+ _bobber_water_position = clamped_position
+ _presentation.show_withdrawal_position(_bobber_water_position)
+ if not clamped_position.is_equal_approx(desired_position):
_resolve_withdrawal_at_shore()
return
- _withdrawal_progress = next_progress
- _bobber_water_position = withdrawal_surface.position
- _presentation.show_withdrawal_position(_bobber_water_position)
if is_equal_approx(_withdrawal_progress, 1.0):
_cancel_from_withdrawal()
@@ -1133,14 +657,20 @@ func _get_withdrawal_completion_time(delta: float) -> float:
_withdrawal_endpoint,
next_progress
)
- var withdrawal_surface: FishingSurfaceSampleType = (
- resolve_safe_withdrawal_surface(
- _bobber_water_position,
- desired_position,
- )
+ var clamped_position: Vector3 = find_last_fishable_position(
+ _bobber_water_position,
+ desired_position
)
- if not withdrawal_surface.is_fishable():
- return 0.0
+ if not clamped_position.is_equal_approx(desired_position):
+ var segment_length: float = _bobber_water_position.distance_to(
+ desired_position
+ )
+ if segment_length <= 0.0:
+ return 0.0
+ return delta * (
+ _bobber_water_position.distance_to(clamped_position)
+ / segment_length
+ )
if is_equal_approx(next_progress, 1.0):
return delta * (1.0 - _withdrawal_progress) / progress_step
return INF
@@ -1152,11 +682,10 @@ func _get_withdrawable_distance() -> float:
return landing_offset.length() - withdrawal_cancel_distance
-func _activate_bite(confirmation_override: bool = false) -> void:
+func _activate_bite() -> void:
if state != FishingState.WAITING_FOR_BITE:
return
- _stop_reeling_audio()
if (
_active_player == null
or _selected_fish == null
@@ -1164,45 +693,22 @@ func _activate_bite(confirmation_override: bool = false) -> void:
):
_cancel_attempt()
return
- if (
- not confirmation_override
- and _active_lure_has_effect(&"deferred_fight")
- ):
- _state_time_remaining = 0.0
- _withdrawal_input_held = false
- _set_bite_confirmation_pending(true)
- status_changed.emit("")
- _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
- _presentation.show_bite()
- return
- _set_fishing_input_priority(true)
- _set_bite_confirmation_pending(false)
- _pending_catch = _fish_selector.create_catch(_selected_fish)
- if _pending_catch == null or not _pending_catch.is_valid():
- _cancel_attempt()
- return
- if not _consume_active_bait():
- _pending_catch = null
- _cleanup_attempt("No bait available.", &"cancel")
- return
state = FishingState.FIGHTING
- _active_player.set_fighting_visual(true)
_state_time_remaining = 0.0
_withdrawal_input_held = false
- _start_fight_audio()
+ _fight_start_position = _bobber_water_position
bite_activated.emit()
status_changed.emit("fish on!")
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
- _presentation.begin_fight()
+ _presentation.begin_reeling()
_catch_controller.start_encounter(
_selected_fish.catch_profile,
_get_effective_reel_speed(),
- _get_effective_barrier_damage(),
- _pending_catch.quality,
+ _get_effective_barrier_damage()
)
_catch_controller.set_reel_input(
- _is_reel_input_pressed()
+ Input.is_action_pressed("fish_primary")
)
_presentation.show_bite()
@@ -1210,7 +716,6 @@ func _activate_bite(confirmation_override: bool = false) -> void:
func _get_effective_reel_speed() -> float:
if _active_player == null:
return 0.0
- var rod: FishingRodDataType = _get_active_rod()
return _active_player.reel_speed * (
_fishing_upgrades.get_reel_speed_multiplier()
if _fishing_upgrades != null
@@ -1219,16 +724,13 @@ func _get_effective_reel_speed() -> float:
_item_effects.get_reel_multiplier()
if _item_effects != null
else 1.0
- ) * (
- rod.reel_speed_multiplier if rod != null else 1.0
)
func _get_effective_barrier_damage() -> int:
if _active_player == null:
return 1
- var rod: FishingRodDataType = _get_active_rod()
- var damage: int = (
+ return (
_fishing_upgrades.get_barrier_damage()
if _fishing_upgrades != null
else _active_player.click_power
@@ -1237,13 +739,6 @@ func _get_effective_barrier_damage() -> int:
if _item_effects != null
else 0
)
- return maxi(
- roundi(
- float(damage)
- * (rod.barrier_power_multiplier if rod != null else 1.0)
- ),
- 1,
- )
func _on_catch_encounter_updated(
@@ -1253,7 +748,7 @@ func _on_catch_encounter_updated(
barrier_health: PackedInt32Array,
barrier_max_health: PackedInt32Array,
active_barrier_index: int,
- encounter_visible: bool,
+ visible: bool,
) -> void:
catch_display_changed.emit(
progress,
@@ -1262,11 +757,24 @@ func _on_catch_encounter_updated(
barrier_health,
barrier_max_health,
active_barrier_index,
- encounter_visible
+ visible
)
- if state != FishingState.FIGHTING or not encounter_visible:
+ if state != FishingState.FIGHTING or not visible:
return
+ var desired_position: Vector3 = _fight_start_position.lerp(
+ _withdrawal_endpoint,
+ progress
+ )
+ _bobber_water_position = find_last_fishable_position(
+ _fight_start_position,
+ desired_position
+ )
+ _presentation.show_reel_position(
+ _bobber_water_position,
+ Input.is_action_pressed("fish_primary")
+ )
+
func _on_catch_completed() -> void:
if (
@@ -1277,17 +785,13 @@ func _on_catch_completed() -> void:
_cancel_attempt()
return
+ _pending_catch = _fish_selector.create_catch(_selected_fish)
if _pending_catch == null or not _pending_catch.is_valid():
_cancel_attempt()
return
- _stop_fight_audio()
- _active_player.set_fighting_visual(false)
- _active_player.set_retract_visual()
state = FishingState.SHOWING_CATCH
_showcase_ready = false
- _showcase_outcome_completed = false
_put_away_press_armed = false
- _showcase_input_lock_remaining = 0.0
_catch_controller.reset()
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
_presentation.play_outcome(&"catch")
@@ -1297,50 +801,29 @@ func _on_catch_escaped() -> void:
if state != FishingState.FIGHTING:
return
- _stop_fight_audio()
- _cleanup_attempt("", &"escape")
+ var fish_name: String = "the fish"
+ if _selected_fish != null and not _selected_fish.display_name.is_empty():
+ fish_name = "the %s" % _selected_fish.display_name
+ _cleanup_attempt("%s got away!" % fish_name, &"escape")
func _on_outcome_completed(outcome: StringName) -> void:
- var retracting_player := _active_player
- if (
- retracting_player != null
- and not retracting_player.is_retract_visual_complete()
- ):
- await retracting_player.retract_visual_finished
- if state == FishingState.RETURNING:
- var cleanup_message: String = _pending_cleanup_message
- _pending_cleanup_message = ""
- _finalize_attempt_cleanup(cleanup_message)
- return
if (
outcome != &"catch"
or state != FishingState.SHOWING_CATCH
or _active_player == null
or _pending_catch == null
- or _showcase_outcome_completed
):
return
- _showcase_outcome_completed = true
_showcase_ready = true
- _showcase_input_lock_remaining = minimum_showcase_duration
_active_player.begin_catch_showcase(_pending_catch)
showcase_changed.emit(
_pending_catch.fish.display_name,
_pending_catch.fish.get_rarity_name(),
_pending_catch.weight_lb,
- _pending_catch.quality,
true
)
-
-
-func _on_presentation_interrupted() -> void:
- if state in [FishingState.READY, FishingState.COOLDOWN]:
- return
- if _network_fishing != null and _network_fishing.has_local_attempt():
- _network_fishing.cancel_local_attempt("")
- return
- _finalize_attempt_cleanup("")
+ status_changed.emit("left click or escape to put away")
func _put_away_catch() -> void:
@@ -1352,46 +835,23 @@ func _put_away_catch() -> void:
or _pending_catch == null
):
return
- _store_catch_progression(_pending_catch)
+ var caught_name: String = _pending_catch.fish.display_name
+ _local_inventory.add_catch(_pending_catch)
+ _local_collection_log.mark_discovered(_pending_catch.fish_id)
_pending_catch = null
_showcase_ready = false
- _showcase_outcome_completed = false
_put_away_press_armed = false
- _showcase_input_lock_remaining = 0.0
- showcase_changed.emit("", "", 0.0, 0, false)
+ showcase_changed.emit("", "", 0.0, false)
_showcase_restore_generation += 1
var restore_generation: int = _showcase_restore_generation
_active_player.end_catch_showcase(
_finish_showcase_put_away.bind(
restore_generation,
- ""
+ "caught %s!" % caught_name
)
)
-func _store_catch_progression(fish_catch: FishCatchType) -> void:
- if (
- fish_catch == null
- or _local_inventory == null
- or _local_collection_log == null
- or _local_experience == null
- or _local_inventory.contains_catch_id(fish_catch.catch_id)
- ):
- return
- var experience_award: int = (
- FishExperienceType.calculate_for_collection(
- fish_catch,
- _local_collection_log,
- )
- )
- _local_inventory.add_catch(fish_catch)
- _local_collection_log.record_catch(
- fish_catch.fish_id,
- fish_catch.quality,
- )
- _local_experience.award_experience(experience_award)
-
-
func _finish_showcase_put_away(
restore_generation: int,
cooldown_message: String,
@@ -1409,34 +869,35 @@ func _cleanup_attempt(
cooldown_message: String = "",
visual_outcome: StringName = &"",
) -> void:
- _set_bite_confirmation_pending(false)
- _stop_fight_audio()
- _stop_reeling_audio()
+ _showcase_restore_generation += 1
if _active_player != null:
- _active_player.set_fighting_visual(false)
- if visual_outcome.is_empty():
- _active_player.set_fishing_visual(false)
- elif visual_outcome == &"catch":
- # A successful catch already retracted the line before the fish
- # showcase. Returning from the pocket transition should settle back
- # to idle instead of replaying the retract animation.
- _active_player.set_fishing_visual(false)
- else:
- _active_player.set_retract_visual()
- if not visual_outcome.is_empty():
- if state == FishingState.RETURNING:
- return
- _showcase_restore_generation += 1
- _pending_cleanup_message = cooldown_message
- _showcase_ready = false
- _showcase_outcome_completed = false
- _put_away_press_armed = false
- _showcase_input_lock_remaining = 0.0
- showcase_changed.emit("", "", 0.0, 0, false)
- _catch_controller.reset()
- state = FishingState.RETURNING
- status_changed.emit("")
- if visual_outcome in [&"invalid", &"withdrawal", &"escape"]:
+ _active_player.end_catch_showcase()
+ _active_player.set_movement_enabled(true)
+ _active_player = null
+ _state_time_remaining = 0.0
+ _cast_charge = 0.0
+ _cast_direction = Vector3.FORWARD
+ _cast_origin_position = Vector3.ZERO
+ _cast_target = Vector3.ZERO
+ _cast_landing_is_fishable = false
+ _withdrawal_endpoint = Vector3.ZERO
+ _withdrawal_progress = 0.0
+ _withdrawal_input_held = false
+ _bobber_water_position = Vector3.ZERO
+ _fight_start_position = Vector3.ZERO
+ _selected_water_region = null
+ _selection_context = null
+ _selected_fish = null
+ _pending_catch = null
+ _showcase_ready = false
+ _put_away_press_armed = false
+ showcase_changed.emit("", "", 0.0, false)
+ _catch_controller.reset()
+ if visual_outcome.is_empty():
+ _presentation.cleanup()
+ _presentation.reset_water_surface_height()
+ else:
+ if visual_outcome in [&"catch", &"invalid", &"withdrawal"]:
_presentation.set_line_mode(
FishingPresentationType.LineMode.TAUT
)
@@ -1445,48 +906,6 @@ func _cleanup_attempt(
FishingPresentationType.LineMode.HIDDEN
)
_presentation.play_outcome(visual_outcome)
- return
- _finalize_attempt_cleanup(cooldown_message)
-
-
-func _finalize_attempt_cleanup(
- cooldown_message: String,
- restore_movement: bool = true,
-) -> void:
- _set_bite_confirmation_pending(false)
- _showcase_restore_generation += 1
- if _active_player != null:
- _active_player.set_fighting_visual(false)
- _active_player.set_fishing_visual(false)
- _active_player.end_catch_showcase()
- if restore_movement:
- _active_player.set_movement_enabled(true)
- _active_player = null
- _state_time_remaining = 0.0
- _cast_charge = 0.0
- _cast_direction = Vector3.FORWARD
- _cast_origin_position = Vector3.ZERO
- _cast_target = Vector3.ZERO
- _aim_surface_sample = FishingSurfaceSampleType.new()
- _cast_path_is_clear = false
- _withdrawal_endpoint = Vector3.ZERO
- _withdrawal_progress = 0.0
- _withdrawal_input_held = false
- _network_primary_input_held = false
- _network_input_resend_elapsed = 0.0
- _bobber_water_position = Vector3.ZERO
- _selected_water_region = null
- _selection_context = null
- _selected_fish = null
- _pending_catch = null
- _showcase_ready = false
- _showcase_outcome_completed = false
- _put_away_press_armed = false
- _showcase_input_lock_remaining = 0.0
- showcase_changed.emit("", "", 0.0, 0, false)
- _catch_controller.reset()
- _presentation.cleanup()
- _pending_cleanup_message = ""
if cooldown_message.is_empty():
state = FishingState.READY
@@ -1498,32 +917,25 @@ func _finalize_attempt_cleanup(
_state_time_remaining = cooldown_duration
_cooldown_status = cooldown_message
status_changed.emit(_cooldown_status)
- _set_fishing_input_priority(false)
func _return_to_ready() -> void:
+ _presentation.reset_water_surface_height()
state = FishingState.READY
_state_time_remaining = 0.0
_cooldown_status = ""
status_changed.emit("")
ready_for_equipment_refresh.emit()
- _set_fishing_input_priority(false)
func _cancel_attempt() -> void:
- if _network_fishing != null and _network_fishing.has_local_attempt():
- _network_fishing.cancel_local_attempt("")
- return
- _cleanup_attempt("", &"cancel")
+ _cleanup_attempt("fishing cancelled.", &"cancel")
func _cancel_from_withdrawal() -> void:
if state != FishingState.WAITING_FOR_BITE:
return
_new_cast_press_armed = false
- if _network_fishing != null and _network_fishing.has_local_attempt():
- _network_fishing.cancel_local_attempt("")
- return
_cleanup_attempt("", &"withdrawal")
@@ -1544,119 +956,62 @@ func _capture_cast_direction(player: PlayerType) -> Vector3:
return direction.normalized()
-func _get_cast_launch_position() -> Vector3:
- if _active_player != null:
- var rod_tip: Marker3D = _active_player.get_fishing_rod_tip()
- if rod_tip != null and is_instance_valid(rod_tip):
- return rod_tip.global_position
- return _cast_origin_position
-
-
func _calculate_cast_target(distance: float) -> Vector3:
- var query_position := Vector3(
+ var target := Vector3(
_cast_origin_position.x + _cast_direction.x * distance,
- _cast_origin_position.y,
+ _presentation.get_water_surface_height(),
_cast_origin_position.z + _cast_direction.z * distance
)
- _aim_surface_sample = resolve_fishing_surface(
- query_position,
- _cast_origin_position.y,
- )
- if _aim_surface_sample.has_surface:
- return _aim_surface_sample.get_bobber_position(solid_bobber_clearance)
- return query_position
-
-
-func _configure_surface_resolver() -> void:
- _surface_resolver.fishable_surface_mask = fishable_surface_mask
- _surface_resolver.solid_surface_mask = (
- preview_surface_mask & ~fishable_surface_mask
- )
- _surface_resolver.query_radius = fishable_query_radius
- _surface_resolver.ray_start_height = preview_ray_start_height
- _surface_resolver.ray_length = preview_surface_ray_length
-
-
-func resolve_fishing_surface(
- target: Vector3,
- reference_y: float = NAN,
-) -> FishingSurfaceSampleType:
- if not is_inside_tree():
- var empty_sample := FishingSurfaceSampleType.new()
- empty_sample.position = target
- return empty_sample
- var resolved_reference_y: float = reference_y
- if is_nan(resolved_reference_y):
- resolved_reference_y = (
- _active_player.global_position.y
- if _active_player != null
- else target.y
- )
- return _surface_resolver.resolve_surface(
- get_world_3d().direct_space_state,
- target,
- resolved_reference_y,
- solid_probe_clearance,
+ var water_region: FishableWaterRegionType = get_fishable_water_region(
+ target
)
+ if water_region != null:
+ target.y = water_region.get_surface_height()
+ _presentation.set_water_surface_height(target.y)
+ else:
+ _presentation.reset_water_surface_height()
+ target.y = _presentation.get_water_surface_height()
+ return target
func is_target_fishable(target: Vector3) -> bool:
- return resolve_fishing_surface(target).is_fishable()
-
-
-func _is_cast_target_valid(target: Vector3) -> bool:
- return (
- is_target_fishable(target)
- and is_cast_path_clear(_get_cast_launch_position(), target)
- )
-
-
-func is_cast_path_clear(origin: Vector3, target: Vector3) -> bool:
- if _preferred_cast_arc_height < 0.0:
- _preferred_cast_arc_height = maxf(
- _presentation.cast_arc_height,
- 0.0,
- )
- var resolved_arc: Dictionary = _surface_resolver.resolve_cast_arc(
- get_world_3d().direct_space_state,
- origin,
- target,
- _preferred_cast_arc_height,
- )
- _presentation.cast_arc_height = float(resolved_arc["arc_height"])
- var collision: Dictionary = resolved_arc["collision"]
- return collision.is_empty()
-
-
-func _resolve_cast_impact_position(origin: Vector3, target: Vector3) -> Vector3:
- var result: Dictionary = _surface_resolver.find_first_cast_collision(
- get_world_3d().direct_space_state,
- origin,
- target,
- _presentation.cast_arc_height,
- )
- if result.is_empty():
- return target
- var hit_position: Vector3 = result["position"]
- var hit_normal: Vector3 = result.get("normal", Vector3.UP)
- if hit_normal.is_zero_approx():
- hit_normal = Vector3.UP
- return hit_position + hit_normal.normalized() * 0.12
+ return get_fishable_water_region(target) != null
func get_fishable_water_region(
target: Vector3,
) -> FishableWaterRegionType:
- var reference_y: float = (
- _active_player.global_position.y
- if _active_player != null
- else target.y
+ _fishable_query_shape.radius = fishable_query_radius
+ _fishable_query_shape.height = preview_ray_length
+ var query := PhysicsShapeQueryParameters3D.new()
+ query.shape = _fishable_query_shape
+ query.transform = Transform3D(
+ Basis.IDENTITY,
+ Vector3(target.x, target.y, target.z)
)
- var surface: FishingSurfaceSampleType = resolve_fishing_surface(
- target,
- reference_y,
+ query.collision_mask = fishable_surface_mask
+ query.collide_with_areas = true
+ query.collide_with_bodies = false
+ var results: Array[Dictionary] = get_world_3d().direct_space_state.intersect_shape(
+ query,
+ 32
)
- return surface.water_region if surface.is_fishable() else null
+ var selected_region: FishableWaterRegionType
+ for result: Dictionary in results:
+ var collider: Object = result.get("collider")
+ var region: FishableWaterRegionType = collider as FishableWaterRegionType
+ if region == null or region.fish_pool == null:
+ continue
+ if (
+ selected_region == null
+ or region.selection_priority > selected_region.selection_priority
+ or (
+ region.selection_priority == selected_region.selection_priority
+ and region.get_instance_id() < selected_region.get_instance_id()
+ )
+ ):
+ selected_region = region
+ return selected_region
func _build_fishing_context(
@@ -1664,404 +1019,68 @@ func _build_fishing_context(
) -> FishingContextType:
var context := FishingContextType.new()
context.location_tags = region.location_tags.duplicate()
- context.water_type = region.water_type
context.active_event_tags = context_event_tags.duplicate()
- context.active_bait_tags = _get_active_bait_tags()
- if _world_time != null:
- context.is_night = _world_time.is_night_period()
- context.is_day_night_transition = _world_time.is_transition()
- context.season = _world_time.get_season()
- if _world_weather != null:
- context.is_raining = _world_weather.is_raining()
- context.is_foggy = _world_weather.is_foggy()
+ context.active_bait_tags = context_bait_tags.duplicate()
+ context.is_night = context_is_night
+ context.is_raining = context_is_raining
return context
-func build_network_context(
- region: FishableWaterRegionType,
-) -> FishingContextType:
- return _build_fishing_context(region)
+func find_last_fishable_position(from: Vector3, to: Vector3) -> Vector3:
+ if from.is_equal_approx(to):
+ return from
+ var segment_length: float = from.distance_to(to)
+ var sample_spacing: float = maxf(fishable_query_radius, 0.05)
+ var sample_count: int = maxi(1, ceili(segment_length / sample_spacing))
+ var last_valid_fraction: float = 0.0
-func is_species_available_now(fish: FishDataType) -> bool:
- if fish == null or not fish.is_selectable():
- return false
- var season := (
- _world_time.get_season()
- if _world_time != null
- else CalendarSeason.UNKNOWN
- )
- if not fish.is_available_in_season(season):
- return false
- if fish.collection_method != FishDataType.CollectionMethod.FISHING:
- return true
- for region: FishableWaterRegionType in _get_logbook_water_regions():
- if (
- region == null
- or region.fish_pool == null
- or region.fish_pool.get_fish_by_id(fish.id) == null
- ):
+ for sample_index: int in range(1, sample_count + 1):
+ var sample_fraction: float = float(sample_index) / float(sample_count)
+ var sample_position: Vector3 = from.lerp(to, sample_fraction)
+ if is_target_fishable(sample_position):
+ last_valid_fraction = sample_fraction
continue
- var context := _build_fishing_context(region)
- if not fish.is_allowed_in_water(context.water_type):
- continue
- if fish.availability == null or fish.availability.is_available(context):
- return true
- return false
+
+ var invalid_fraction: float = sample_fraction
+ for _iteration: int in range(10):
+ var midpoint: float = (
+ last_valid_fraction + invalid_fraction
+ ) * 0.5
+ if is_target_fishable(from.lerp(to, midpoint)):
+ last_valid_fraction = midpoint
+ else:
+ invalid_fraction = midpoint
+ return from.lerp(to, last_valid_fraction)
+
+ return to
-func _get_logbook_water_regions() -> Array[FishableWaterRegionType]:
- var regions: Array[FishableWaterRegionType] = []
- if not _fishable_water_regions_resolver.is_valid():
- return regions
- var resolved: Variant = _fishable_water_regions_resolver.call()
- if resolved is not Array:
- return regions
- for value: Variant in resolved:
- var region := value as FishableWaterRegionType
- if region != null and is_instance_valid(region):
- regions.append(region)
- return regions
-
-
-func _get_active_bait_tags() -> Array[StringName]:
- if _local_player == null or _item_catalog == null:
- return []
- var bait_id: StringName = _local_player.active_bait_id
- if bait_id.is_empty() or _local_bag == null or not _local_bag.owns_item(bait_id):
- return []
- var item: ItemDataType = _item_catalog.get_item_by_id(bait_id)
- return (
- item.bait_tags.duplicate()
- if item != null and item.is_available() and item.is_bait()
- else []
+func _resolve_preview_surface_position(target: Vector3) -> Vector3:
+ var ray_start := Vector3(
+ target.x,
+ target.y + preview_ray_start_height,
+ target.z
)
-
-
-func _active_lure_has_effect(effect_id: StringName) -> bool:
- if (
- _local_player == null
- or _item_catalog == null
- or _local_bag == null
- or _local_player.active_lure_id.is_empty()
- or not _local_bag.owns_item(_local_player.active_lure_id)
- ):
- return false
- var lure: ItemDataType = _item_catalog.get_item_by_id(
- _local_player.active_lure_id
+ var ray_end := Vector3(
+ target.x,
+ ray_start.y - preview_ray_length,
+ target.z
)
- return (
- lure != null
- and lure.is_available()
- and lure.is_lure()
- and effect_id in lure.lure_effects
+ var query := PhysicsRayQueryParameters3D.create(
+ ray_start,
+ ray_end,
+ preview_surface_mask
)
+ query.collide_with_areas = true
+ query.collide_with_bodies = true
+ var result: Dictionary = get_world_3d().direct_space_state.intersect_ray(query)
+ if result.is_empty():
+ return target
-
-func _build_network_evidence() -> Dictionary:
- var rarity_multipliers: Array[float] = []
- for rarity: int in range(FishDataType.Rarity.size()):
- rarity_multipliers.append(1.0)
- var discovered_ids: Array[String] = []
- if _local_collection_log != null:
- for fish_id: StringName in _local_collection_log.get_discovered_ids():
- discovered_ids.append(str(fish_id))
- var item: ItemDataType = _get_active_item()
- return {
- "bait_id": str(_active_player.active_bait_id),
- "lure_id": str(_active_player.active_lure_id),
- "rod_id": str(item.item_id) if item != null else "",
- "reel_speed": _active_player.reel_speed * (
- _fishing_upgrades.get_reel_speed_multiplier()
- if _fishing_upgrades != null else 1.0
- ),
- "barrier_damage": (
- _fishing_upgrades.get_barrier_damage()
- if _fishing_upgrades != null else _active_player.click_power
- ),
- "bite_multiplier": 1.0,
- "rarity_multipliers": rarity_multipliers,
- "discovered_fish_ids": discovered_ids,
- "capacity_available": not _is_inventory_full(),
- }
-
-
-func _on_network_item_use_finished(
- accepted: bool,
- message: String,
-) -> void:
- if (
- not accepted
- and message == PlayerItemEffectsType.FISH_FINDER_DEAD_MESSAGE
- ):
- status_changed.emit("")
- local_speech_requested.emit(message)
- return
- status_changed.emit(message)
- if accepted:
- refresh_active_item_status()
-
-
-func _on_network_cast_accepted(
- _attempt_id: String,
- target: Vector3,
-) -> void:
- if state != FishingState.CASTING:
- # A cast response can arrive after water recovery has already cancelled
- # its local presentation. Do not leave that late authoritative attempt
- # alive with its movement lock still applied.
- if _network_fishing != null and _network_fishing.has_local_attempt():
- _network_fishing.cancel_local_attempt("")
- return
- _cast_target = target
- _bobber_water_position = target
- state = FishingState.WAITING_FOR_BITE
- if _active_player != null:
- _active_player.set_fishing_visual(true)
- _network_primary_input_held = false
- _network_input_resend_elapsed = 0.0
- _presentation.show_withdrawal_position(_bobber_water_position)
- _presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
- status_changed.emit("")
-
-
-func _on_network_cast_rejected(message: String) -> void:
- if state not in [FishingState.CASTING, FishingState.AIMING_CAST]:
- return
- var visible_message: String = message
- if message.strip_edges().to_lower() in [
- "cannot fish here.",
- "cannot fish here",
- "can't fish here.",
- "can't fish here",
- "can't fish there.",
- "can't fish there",
- ]:
- visible_message = ""
- _cleanup_attempt(visible_message, &"invalid")
-
-
-func _on_network_bite_pending(_attempt_id: String) -> void:
- if state != FishingState.WAITING_FOR_BITE:
- return
- _stop_reeling_audio()
- _withdrawal_input_held = false
- _network_primary_input_held = false
- _network_input_resend_elapsed = 0.0
- _set_bite_confirmation_pending(true)
- status_changed.emit("")
- _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
- _presentation.show_bite()
-
-
-func _on_network_bite_started(_attempt_id: String) -> void:
- if state != FishingState.WAITING_FOR_BITE:
- return
- _stop_reeling_audio()
- _set_fishing_input_priority(true)
- _set_bite_confirmation_pending(false)
- state = FishingState.FIGHTING
- if _active_player != null:
- _active_player.set_fighting_visual(true)
- _withdrawal_input_held = false
- _network_primary_input_held = _is_reel_input_pressed()
- _network_input_resend_elapsed = 0.0
- _network_auto_click_accumulator = 0.0
- _network_active_barrier_index = -1
- _start_fight_audio()
- bite_activated.emit()
- status_changed.emit("fish on!")
- _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
- _presentation.begin_fight()
- _presentation.show_bite()
- _network_fishing.submit_local_input(
- _network_primary_input_held,
- false
- )
-
-
-func _resend_network_input_state(delta: float) -> void:
- if _network_fishing == null or not _network_fishing.has_local_attempt():
- return
- _network_input_resend_elapsed += delta
- if _network_input_resend_elapsed < NETWORK_INPUT_RESEND_INTERVAL_SECONDS:
- return
- _network_input_resend_elapsed = fmod(
- _network_input_resend_elapsed,
- NETWORK_INPUT_RESEND_INTERVAL_SECONDS,
- )
- _network_fishing.submit_local_input(
- false if _bite_confirmation_pending else _network_primary_input_held,
- _bite_confirmation_pending and _bite_confirmation_requested,
- )
-
-
-func _on_network_fishing_snapshot(snapshot: Dictionary) -> void:
- if state not in [
- FishingState.WAITING_FOR_BITE,
- FishingState.FIGHTING,
- ]:
- return
- var positions := PackedFloat32Array(snapshot.get("barrier_positions", []))
- var health := PackedInt32Array(snapshot.get("barrier_health", []))
- var maximum_health := PackedInt32Array(
- snapshot.get("barrier_max_health", [])
- )
- var progress: float = float(snapshot.get("progress", 0.0))
- var chase_progress: float = float(snapshot.get("chase_progress", 0.0))
- if state == FishingState.FIGHTING:
- _network_active_barrier_index = int(
- snapshot.get("active_barrier_index", -1)
- )
- catch_display_changed.emit(
- progress,
- chase_progress,
- positions,
- health,
- maximum_health,
- int(snapshot.get("active_barrier_index", -1)),
- bool(snapshot.get("visible", false))
- )
- var bobber_data: Array = snapshot.get("bobber_position", [])
- if bobber_data.size() == 3:
- _bobber_water_position = Vector3(
- float(bobber_data[0]),
- float(bobber_data[1]),
- float(bobber_data[2])
- )
- if state == FishingState.WAITING_FOR_BITE:
- _presentation.show_withdrawal_position(_bobber_water_position)
-
-
-func _update_network_auto_click(delta: float) -> void:
- if (
- not _catch_controller.auto_click_enabled
- or not _is_reel_input_pressed()
- or _network_active_barrier_index < 0
- ):
- _network_auto_click_accumulator = 0.0
- return
- _network_auto_click_accumulator += delta
- var interval: float = maxf(
- _catch_controller.auto_click_interval,
- 0.05
- )
- while _network_auto_click_accumulator >= interval:
- _network_auto_click_accumulator -= interval
- _network_fishing.submit_local_input(true, true)
-
-
-func _on_network_catch_received(fish_catch: FishCatchType) -> void:
- if state != FishingState.FIGHTING or fish_catch == null:
- return
- _stop_fight_audio()
- _pending_catch = fish_catch
- if _active_player != null:
- _active_player.set_fighting_visual(false)
- _active_player.set_fishing_visual(false)
- state = FishingState.SHOWING_CATCH
- _showcase_ready = false
- _showcase_outcome_completed = false
- _put_away_press_armed = false
- _showcase_input_lock_remaining = 0.0
- catch_display_changed.emit(
- 0.0, 0.0, PackedFloat32Array(), PackedInt32Array(),
- PackedInt32Array(), -1, false
- )
- _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
- if _active_player != null:
- _active_player.set_retract_visual()
- _presentation.play_outcome(&"catch")
-
-
-func _on_network_attempt_ended(
- outcome: StringName,
- message: String,
-) -> void:
- if state not in [
- FishingState.CASTING,
- FishingState.WAITING_FOR_BITE,
- FishingState.FIGHTING,
- ]:
- return
- var visual_outcome: StringName = &"cancel"
- if outcome == &"escape":
- visual_outcome = &"escape"
- elif outcome == &"withdrawal":
- visual_outcome = &"withdrawal"
- _cleanup_attempt(message, visual_outcome)
-
-
-func _on_network_session_state_changed(state_value: NetworkSession.State) -> void:
- if state_value in [
- NetworkSession.State.INACTIVE,
- NetworkSession.State.DISCONNECTING,
- NetworkSession.State.CONNECTION_FAILED,
- NetworkSession.State.SERVER_LOST,
- ]:
- _stop_fight_audio()
- _stop_reeling_audio()
- reset_transient_state()
-
-
-func _start_fight_audio() -> void:
- if _fight_audio == null or _fight_audio.stream == null:
- return
- if not _fight_audio.playing:
- _fight_audio.play()
-
-
-func _play_bobber_audio() -> void:
- if _bobber_audio == null or _bobber_audio.stream == null:
- return
- _bobber_audio.play()
-
-
-func _stop_fight_audio() -> void:
- if _fight_audio != null and _fight_audio.playing:
- _fight_audio.stop()
-
-
-func _configure_audio_loop(stream: AudioStreamWAV) -> void:
- stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
- if stream.loop_end <= stream.loop_begin:
- stream.loop_begin = 0
- stream.loop_end = maxi(
- 1,
- roundi(stream.get_length() * float(stream.mix_rate)),
- )
-
-
-func _start_reeling_audio() -> void:
- if _reeling_audio == null or _reeling_audio.stream == null:
- return
- if not _reeling_audio.playing:
- _reeling_audio.play()
-
-
-func _stop_reeling_audio() -> void:
- if _reeling_audio != null and _reeling_audio.playing:
- _reeling_audio.stop()
-
-
-func resolve_safe_withdrawal_surface(
- from: Vector3,
- to: Vector3,
- toward_player: Vector3 = Vector3.ZERO,
-) -> FishingSurfaceSampleType:
- var direction_to_player: Vector3 = toward_player
- if direction_to_player.is_zero_approx():
- direction_to_player = to - from
- var reference_y: float = (
- _active_player.global_position.y
- if _active_player != null
- else maxf(from.y, to.y)
- )
- return _surface_resolver.resolve_withdrawal_surface(
- get_world_3d().direct_space_state,
- from,
- to,
- direction_to_player,
- reference_y,
- withdrawal_surface_clearance,
+ var hit_position: Vector3 = result["position"]
+ return Vector3(
+ target.x,
+ hit_position.y + preview_marker_vertical_offset,
+ target.z
)
diff --git a/fishing/fishing_spot.tscn b/fishing/fishing_spot.tscn
index ef6d0bf..d9ba7ad 100644
--- a/fishing/fishing_spot.tscn
+++ b/fishing/fishing_spot.tscn
@@ -1,9 +1,6 @@
-[gd_scene load_steps=15 format=3]
+[gd_scene load_steps=11 format=3]
[ext_resource type="Script" path="res://fishing/fishing_spot.gd" id="1_spot"]
-[ext_resource type="AudioStream" path="res://audio/sfx/fishing/fighting.wav" id="2_fighting_audio"]
-[ext_resource type="AudioStream" path="res://audio/sfx/fishing/reeling.wav" id="2_reeling_audio"]
-[ext_resource type="AudioStream" path="res://audio/sfx/fishing/bobber.wav" id="2_bobber_audio"]
[ext_resource type="Script" path="res://fishing/fishing_presentation.gd" id="3_presentation"]
[ext_resource type="Script" path="res://fishing/catch_controller.gd" id="4_catch"]
@@ -17,7 +14,6 @@ roughness = 0.45
[sub_resource type="StandardMaterial3D" id="LineMaterial"]
shading_mode = 0
-cull_mode = 2
albedo_color = Color(0.9, 0.9, 0.82, 1)
[sub_resource type="CylinderMesh" id="CastTargetMesh"]
@@ -40,29 +36,11 @@ emission = Color(1, 0.01, 0.08, 1)
emission_energy_multiplier = 2.2
[sub_resource type="BoxMesh" id="InvalidCrossMesh"]
-size = Vector3(0.62, 0.04, 0.09)
+size = Vector3(0.72, 0.06, 0.12)
[node name="FishingSpot" type="Node3D"]
script = ExtResource("1_spot")
-[node name="BobberAudio" type="AudioStreamPlayer" parent="."]
-unique_name_in_owner = true
-stream = ExtResource("2_bobber_audio")
-volume_db = -14.0
-bus = &"SFX"
-
-[node name="FightAudio" type="AudioStreamPlayer" parent="."]
-unique_name_in_owner = true
-stream = ExtResource("2_fighting_audio")
-volume_db = -10.0
-bus = &"SFX"
-
-[node name="ReelingAudio" type="AudioStreamPlayer" parent="."]
-unique_name_in_owner = true
-stream = ExtResource("2_reeling_audio")
-volume_db = -10.0
-bus = &"SFX"
-
[node name="CatchController" type="Node" parent="."]
unique_name_in_owner = true
script = ExtResource("4_catch")
@@ -70,24 +48,24 @@ script = ExtResource("4_catch")
[node name="FishingPresentation" type="Node3D" parent="."]
unique_name_in_owner = true
script = ExtResource("3_presentation")
+valid_target_material = SubResource("CastTargetMaterial")
+invalid_target_material = SubResource("InvalidTargetMaterial")
-[node name="CastTargetMarker" type="Node3D" parent="FishingPresentation"]
-unique_name_in_owner = true
-
-[node name="ValidTargetMarker" type="MeshInstance3D" parent="FishingPresentation/CastTargetMarker"]
+[node name="CastTargetMarker" type="MeshInstance3D" parent="FishingPresentation"]
unique_name_in_owner = true
mesh = SubResource("CastTargetMesh")
material_override = SubResource("CastTargetMaterial")
-[node name="InvalidTargetMarker" type="Node3D" parent="FishingPresentation/CastTargetMarker"]
+[node name="InvalidCrossA" type="MeshInstance3D" parent="FishingPresentation/CastTargetMarker"]
unique_name_in_owner = true
-
-[node name="StrokeA" type="MeshInstance3D" parent="FishingPresentation/CastTargetMarker/InvalidTargetMarker"]
+position = Vector3(0, 0.075, 0)
rotation = Vector3(0, 0.785398, 0)
mesh = SubResource("InvalidCrossMesh")
material_override = SubResource("InvalidTargetMaterial")
-[node name="StrokeB" type="MeshInstance3D" parent="FishingPresentation/CastTargetMarker/InvalidTargetMarker"]
+[node name="InvalidCrossB" type="MeshInstance3D" parent="FishingPresentation/CastTargetMarker"]
+unique_name_in_owner = true
+position = Vector3(0, 0.075, 0)
rotation = Vector3(0, -0.785398, 0)
mesh = SubResource("InvalidCrossMesh")
material_override = SubResource("InvalidTargetMaterial")
diff --git a/fishing/fishing_surface_resolver.gd b/fishing/fishing_surface_resolver.gd
deleted file mode 100644
index c439aac..0000000
--- a/fishing/fishing_surface_resolver.gd
+++ /dev/null
@@ -1,270 +0,0 @@
-class_name FishingSurfaceResolver
-extends RefCounted
-
-const FishableWaterRegionType = preload(
- "res://world/fishable_water_region.gd"
-)
-const FishingSurfaceSampleType = preload(
- "res://fishing/fishing_surface_sample.gd"
-)
-
-var fishable_surface_mask: int = 4
-var solid_surface_mask: int = 1
-var query_radius: float = 0.08
-var ray_start_height: float = 100.0
-var ray_length: float = 240.0
-var arc_sample_spacing: float = 0.3
-
-var _water_query_shape: CylinderShape3D = CylinderShape3D.new()
-
-
-func resolve_surface(
- space_state: PhysicsDirectSpaceState3D,
- query_position: Vector3,
- reference_y: float,
- maximum_solid_rise: float = INF,
-) -> FishingSurfaceSampleType:
- var sample := FishingSurfaceSampleType.new()
- var ray_top: float = maxf(query_position.y, reference_y) + ray_start_height
- var ray_bottom: float = ray_top - ray_length
- var water_region: FishableWaterRegionType = _find_highest_water_region(
- space_state,
- query_position,
- ray_top,
- ray_bottom,
- )
- var solid_ray_top: float = minf(
- ray_top,
- reference_y + maximum_solid_rise,
- )
- var solid_hit: Dictionary = {}
- if solid_ray_top > ray_bottom:
- solid_hit = _find_top_solid_surface(
- space_state,
- query_position,
- solid_ray_top,
- ray_bottom,
- )
-
- if water_region != null:
- var water_height: float = water_region.get_surface_height()
- var solid_is_above_water: bool = false
- if not solid_hit.is_empty():
- var solid_position: Vector3 = solid_hit["position"]
- # The water body's authored height is the exact shoreline authority:
- # submerged terrain is fishable and terrain at or above it is dry.
- solid_is_above_water = solid_position.y >= water_height
- if not solid_is_above_water:
- sample.has_surface = true
- sample.position = Vector3(
- query_position.x,
- water_height,
- query_position.z,
- )
- sample.normal = Vector3.UP
- sample.water_region = water_region
- sample.is_water_surface = true
- return sample
-
- if not solid_hit.is_empty():
- sample.has_surface = true
- sample.position = solid_hit["position"]
- var hit_normal: Vector3 = solid_hit.get("normal", Vector3.UP)
- sample.normal = (
- hit_normal.normalized()
- if not hit_normal.is_zero_approx()
- else Vector3.UP
- )
-
- return sample
-
-
-func find_first_cast_collision(
- space_state: PhysicsDirectSpaceState3D,
- origin: Vector3,
- target: Vector3,
- arc_height: float,
-) -> Dictionary:
- if origin.is_equal_approx(target):
- return {}
- var segment_count: int = maxi(
- 12,
- ceili(origin.distance_to(target) / maxf(arc_sample_spacing, 0.05)),
- )
- var previous_position: Vector3 = origin
- for segment_index: int in range(1, segment_count + 1):
- var progress: float = float(segment_index) / float(segment_count)
- var next_position: Vector3 = origin.lerp(target, progress)
- next_position.y += sin(progress * PI) * arc_height
- var hit: Dictionary = _intersect_solid_segment(
- space_state,
- previous_position,
- next_position,
- )
- if not hit.is_empty():
- return hit
- previous_position = next_position
- return {}
-
-
-func resolve_cast_arc(
- space_state: PhysicsDirectSpaceState3D,
- origin: Vector3,
- target: Vector3,
- preferred_arc_height: float,
-) -> Dictionary:
- var raised_arc_height: float = maxf(preferred_arc_height, 0.0)
- var raised_collision := find_first_cast_collision(
- space_state,
- origin,
- target,
- raised_arc_height,
- )
- if raised_collision.is_empty() or is_zero_approx(raised_arc_height):
- return {
- "arc_height": raised_arc_height,
- "collision": raised_collision,
- }
-
- # A canopy or pier roof can intersect the decorative raised arc even when
- # the direct path out toward the water is unobstructed. Prefer that low cast
- # instead of visibly sending the bobber and line upward into the ceiling.
- var direct_collision := find_first_cast_collision(
- space_state,
- origin,
- target,
- 0.0,
- )
- return {
- "arc_height": 0.0,
- "collision": direct_collision,
- }
-
-
-func resolve_withdrawal_surface(
- space_state: PhysicsDirectSpaceState3D,
- current_position: Vector3,
- desired_position: Vector3,
- toward_player: Vector3,
- reference_y: float,
- shore_clearance: float,
-) -> FishingSurfaceSampleType:
- var desired_sample: FishingSurfaceSampleType = resolve_surface(
- space_state,
- desired_position,
- reference_y,
- )
- if not desired_sample.is_fishable():
- return _blocked_withdrawal_sample(current_position)
-
- var horizontal_direction: Vector3 = toward_player
- horizontal_direction.y = 0.0
- if not horizontal_direction.is_zero_approx() and shore_clearance > 0.0:
- horizontal_direction = horizontal_direction.normalized()
- var lookahead_position: Vector3 = (
- desired_sample.position + horizontal_direction * shore_clearance
- )
- var lookahead_sample: FishingSurfaceSampleType = resolve_surface(
- space_state,
- lookahead_position,
- reference_y,
- )
- if not lookahead_sample.is_fishable():
- return _blocked_withdrawal_sample(current_position)
-
- return desired_sample
-
-
-func _blocked_withdrawal_sample(
- current_position: Vector3,
-) -> FishingSurfaceSampleType:
- var sample := FishingSurfaceSampleType.new()
- sample.position = current_position
- return sample
-
-
-func _find_highest_water_region(
- space_state: PhysicsDirectSpaceState3D,
- query_position: Vector3,
- ray_top: float,
- ray_bottom: float,
-) -> FishableWaterRegionType:
- _water_query_shape.radius = maxf(query_radius, 0.01)
- _water_query_shape.height = maxf(ray_top - ray_bottom, 0.1)
- var query := PhysicsShapeQueryParameters3D.new()
- query.shape = _water_query_shape
- query.transform = Transform3D(
- Basis.IDENTITY,
- Vector3(
- query_position.x,
- (ray_top + ray_bottom) * 0.5,
- query_position.z,
- ),
- )
- query.collision_mask = fishable_surface_mask
- query.collide_with_areas = true
- query.collide_with_bodies = false
- var results: Array[Dictionary] = space_state.intersect_shape(query, 64)
- var selected_region: FishableWaterRegionType = null
- for result: Dictionary in results:
- var region: FishableWaterRegionType = (
- result.get("collider") as FishableWaterRegionType
- )
- # A water surface remains the visible top surface even when it has no
- # fish pool. Fishability is a separate policy checked by the sample.
- if region == null:
- continue
- var surface_height: float = region.get_surface_height()
- if surface_height < ray_bottom or surface_height > ray_top:
- continue
- if selected_region == null:
- selected_region = region
- continue
- var selected_height: float = selected_region.get_surface_height()
- if (
- surface_height > selected_height + 0.001
- or (
- is_equal_approx(surface_height, selected_height)
- and (
- region.selection_priority > selected_region.selection_priority
- or (
- region.selection_priority
- == selected_region.selection_priority
- and region.get_instance_id()
- < selected_region.get_instance_id()
- )
- )
- )
- ):
- selected_region = region
- return selected_region
-
-
-func _find_top_solid_surface(
- space_state: PhysicsDirectSpaceState3D,
- query_position: Vector3,
- ray_top: float,
- ray_bottom: float,
-) -> Dictionary:
- return _intersect_solid_segment(
- space_state,
- Vector3(query_position.x, ray_top, query_position.z),
- Vector3(query_position.x, ray_bottom, query_position.z),
- )
-
-
-func _intersect_solid_segment(
- space_state: PhysicsDirectSpaceState3D,
- from: Vector3,
- to: Vector3,
-) -> Dictionary:
- if from.is_equal_approx(to) or solid_surface_mask == 0:
- return {}
- var query := PhysicsRayQueryParameters3D.create(
- from,
- to,
- solid_surface_mask,
- )
- query.collide_with_areas = false
- query.collide_with_bodies = true
- return space_state.intersect_ray(query)
diff --git a/fishing/fishing_surface_resolver.gd.uid b/fishing/fishing_surface_resolver.gd.uid
deleted file mode 100644
index 606a2fa..0000000
--- a/fishing/fishing_surface_resolver.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dcyvrak5pvlc
diff --git a/fishing/fishing_surface_sample.gd b/fishing/fishing_surface_sample.gd
deleted file mode 100644
index 4d2dd2c..0000000
--- a/fishing/fishing_surface_sample.gd
+++ /dev/null
@@ -1,28 +0,0 @@
-class_name FishingSurfaceSample
-extends RefCounted
-
-const FishableWaterRegionType = preload(
- "res://world/fishable_water_region.gd"
-)
-
-var has_surface: bool = false
-var position: Vector3 = Vector3.ZERO
-var normal: Vector3 = Vector3.UP
-var water_region: FishableWaterRegionType
-var is_water_surface: bool = false
-
-
-func is_fishable() -> bool:
- return is_water_surface and water_region != null and water_region.fish_pool != null
-
-
-func get_marker_position(vertical_offset: float) -> Vector3:
- if not has_surface:
- return position
- return position + normal.normalized() * vertical_offset
-
-
-func get_bobber_position(solid_clearance: float) -> Vector3:
- if not has_surface or is_water_surface:
- return position
- return position + normal.normalized() * solid_clearance
diff --git a/fishing/fishing_surface_sample.gd.uid b/fishing/fishing_surface_sample.gd.uid
deleted file mode 100644
index 8c00c2b..0000000
--- a/fishing/fishing_surface_sample.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://5n0ku0fxh7xt
diff --git a/fishing/remote_fishing_presentation.gd b/fishing/remote_fishing_presentation.gd
deleted file mode 100644
index 380afdf..0000000
--- a/fishing/remote_fishing_presentation.gd
+++ /dev/null
@@ -1,358 +0,0 @@
-class_name RemoteFishingPresentation
-extends Node3D
-
-const LINE_THICKNESS: float = 0.016
-
-signal return_completed
-
-var _owner: Player
-var _bobber: MeshInstance3D
-var _line: MeshInstance3D
-var _line_mesh := ImmediateMesh.new()
-var _target: Vector3
-var _pending_target: Vector3
-var _active: bool = false
-var _cast_tween: Tween
-var _return_tween: Tween
-var _showcase_tween: Tween
-var _return_showcase_catch: FishCatch
-var _bobber_idle_elapsed: float = 0.0
-var _bobber_base_scale: Vector3 = Vector3.ONE
-var _attempt_id: String = ""
-var _observer_fighting_active: bool = false
-
-
-func setup(owning_player: Player) -> void:
- _owner = owning_player
- _bobber = MeshInstance3D.new()
- var bobber_mesh := SphereMesh.new()
- bobber_mesh.radius = 0.12
- bobber_mesh.height = 0.24
- _bobber.mesh = bobber_mesh
- var bobber_material := StandardMaterial3D.new()
- bobber_material.albedo_color = Color(0.92, 0.12, 0.08, 1.0)
- bobber_material.roughness = 0.45
- _bobber.material_override = bobber_material
- add_child(_bobber)
- _line = MeshInstance3D.new()
- _line.mesh = _line_mesh
- var line_material := StandardMaterial3D.new()
- line_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
- line_material.cull_mode = BaseMaterial3D.CULL_DISABLED
- line_material.albedo_color = Color(0.9, 0.9, 0.82, 1.0)
- _line.material_override = line_material
- add_child(_line)
- cleanup()
-
-
-func show_cast(
- origin: Vector3,
- target: Vector3,
- attempt_id: String = "",
-) -> void:
- if (
- _owner == null
- or not origin.is_finite()
- or not target.is_finite()
- ):
- return
- _kill_cast_tween()
- _active = true
- _attempt_id = attempt_id
- _observer_fighting_active = false
- _bobber_base_scale = Vector3.ONE * _owner.get_character_visual_scale()
- _target = origin
- _pending_target = target
- _bobber_idle_elapsed = 0.0
- _bobber.global_position = origin
- _bobber.scale = _bobber_base_scale
- _bobber.visible = true
- _line.visible = true
- _owner.set_active_item_is_rod(true)
- _owner.set_release_visual()
- _redraw_line()
- _cast_tween = create_tween()
- _cast_tween.set_trans(Tween.TRANS_SINE)
- _cast_tween.set_ease(Tween.EASE_IN_OUT)
- _cast_tween.tween_method(
- _set_cast_sample.bind(origin, target),
- 0.0,
- 1.0,
- 0.65,
- )
- _cast_tween.finished.connect(_on_cast_finished)
-
-
-func update_bobber(world_position: Vector3) -> void:
- if not _active or not world_position.is_finite():
- return
- _pending_target = world_position
- if _cast_tween != null:
- return
- _target = world_position
- _bobber.global_position = world_position
- _redraw_line()
-
-
-func synchronize_active(
- attempt_id: String,
- world_position: Vector3,
- fighting: bool,
-) -> void:
- if (
- _owner == null
- or attempt_id.is_empty()
- or not world_position.is_finite()
- ):
- return
- if _active and not _attempt_id.is_empty() and _attempt_id != attempt_id:
- cleanup()
- if _attempt_id.is_empty():
- _attempt_id = attempt_id
- if not _active:
- _active = true
- _bobber_base_scale = Vector3.ONE * _owner.get_character_visual_scale()
- _target = world_position
- _pending_target = world_position
- _bobber_idle_elapsed = 0.0
- _bobber.global_position = world_position
- _bobber.scale = _bobber_base_scale
- _bobber.visible = true
- _line.visible = true
- _owner.set_active_item_is_rod(true)
- _owner.set_fishing_visual(true)
- _redraw_line()
- else:
- update_bobber(world_position)
- if fighting != _observer_fighting_active:
- _observer_fighting_active = fighting
- _owner.set_fighting_visual(fighting)
- if not fighting:
- _owner.set_fishing_visual(true)
-
-
-func show_bite() -> void:
- if not _active:
- return
- if _owner != null and is_instance_valid(_owner):
- _observer_fighting_active = true
- _owner.set_fighting_visual(true)
- var tween: Tween = create_tween()
- tween.tween_property(_bobber, "scale", _bobber_base_scale * 0.7, 0.08)
- tween.tween_property(_bobber, "scale", _bobber_base_scale, 0.12)
-
-
-func play_return(showcase_catch: FishCatch = null) -> void:
- if _owner != null and is_instance_valid(_owner):
- _owner.set_fighting_visual(false)
- _owner.set_retract_visual()
- if not _active or _bobber == null:
- cleanup()
- return_completed.emit()
- return
- _kill_cast_tween()
- _kill_return_tween()
- _return_showcase_catch = showcase_catch
- var start: Vector3 = _bobber.global_position
- var target: Vector3 = start
- if _owner != null and is_instance_valid(_owner):
- var tip: Marker3D = _owner.get_fishing_rod_tip()
- if tip != null:
- target = tip.global_position
- _return_tween = create_tween()
- _return_tween.set_trans(Tween.TRANS_QUAD)
- _return_tween.set_ease(Tween.EASE_IN)
- _return_tween.tween_method(
- _set_return_sample.bind(start, target),
- 0.0,
- 1.0,
- 0.42,
- )
- _return_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.1)
- _return_tween.finished.connect(_on_return_finished)
-
-
-func cleanup() -> void:
- _kill_cast_tween()
- _kill_return_tween()
- _kill_showcase_tween()
- _active = false
- _attempt_id = ""
- _observer_fighting_active = false
- _pending_target = Vector3.ZERO
- _return_showcase_catch = null
- _bobber_idle_elapsed = 0.0
- if _owner != null and is_instance_valid(_owner):
- _owner.set_fighting_visual(false)
- _owner.set_fishing_visual(false)
- _owner.end_catch_showcase(Callable(), true)
- if _bobber != null:
- _bobber.visible = false
- _bobber.scale = Vector3.ONE
- _bobber_base_scale = Vector3.ONE
- if _line != null:
- _line.visible = false
- _line_mesh.clear_surfaces()
-
-
-func _set_cast_sample(
- progress: float,
- start: Vector3,
- target: Vector3,
-) -> void:
- _target = start.lerp(target, progress)
- _target.y += sin(progress * PI) * 2.0
- _bobber.global_position = _target
-
-
-func _on_cast_finished() -> void:
- _cast_tween = null
- _bobber_idle_elapsed = 0.0
- if _owner != null and is_instance_valid(_owner):
- _owner.set_fishing_visual(true)
- _apply_bobber_idle_motion()
- _redraw_line()
-
-
-func _apply_bobber_idle_motion() -> void:
- var phase: float = _bobber_idle_elapsed * 0.7 * TAU
- _target = _pending_target + Vector3.UP * sin(phase) * 0.035
- _bobber.global_position = _target
- _bobber.rotation.z = deg_to_rad(4.0) * sin(phase * 0.73)
-
-
-func _set_return_sample(
- progress: float,
- start: Vector3,
- target: Vector3,
-) -> void:
- var eased_progress: float = 1.0 - pow(1.0 - progress, 3.0)
- _target = start.lerp(target, eased_progress)
- _target.y += sin(eased_progress * PI) * 1.3
- _bobber.global_position = _target
- _bobber.rotation = Vector3.ZERO
-
-
-func _on_return_finished() -> void:
- _return_tween = null
- _active = false
- _bobber.visible = false
- _line.visible = false
- _line_mesh.clear_surfaces()
- if (
- _owner != null
- and is_instance_valid(_owner)
- and not _owner.is_retract_visual_complete()
- ):
- await _owner.retract_visual_finished
- if (
- _return_showcase_catch != null
- and _owner != null
- and is_instance_valid(_owner)
- ):
- _owner.begin_remote_catch_showcase(_return_showcase_catch)
- _return_showcase_catch = null
- _showcase_tween = create_tween()
- _showcase_tween.tween_interval(2.5)
- _showcase_tween.finished.connect(_on_showcase_finished)
- return
- return_completed.emit()
-
-
-func _on_showcase_finished() -> void:
- _showcase_tween = null
- if _owner != null and is_instance_valid(_owner):
- _owner.end_catch_showcase(_on_showcase_put_away_finished)
- return
- _on_showcase_put_away_finished()
-
-
-func _on_showcase_put_away_finished() -> void:
- return_completed.emit()
-
-
-func _kill_cast_tween() -> void:
- if _cast_tween != null and _cast_tween.is_valid():
- _cast_tween.kill()
- _cast_tween = null
-
-
-func _kill_return_tween() -> void:
- if _return_tween != null and _return_tween.is_valid():
- _return_tween.kill()
- _return_tween = null
-
-
-func _kill_showcase_tween() -> void:
- if _showcase_tween != null and _showcase_tween.is_valid():
- _showcase_tween.kill()
- _showcase_tween = null
-
-
-func _process(delta: float) -> void:
- if _owner != null and not _owner.is_remote_presentation_visible():
- _bobber.visible = false
- _line.visible = false
- return
- if _active:
- if _cast_tween == null and _return_tween == null:
- _bobber_idle_elapsed += delta
- _apply_bobber_idle_motion()
- _redraw_line()
-
-
-func _redraw_line() -> void:
- if _owner == null or not is_instance_valid(_owner):
- cleanup()
- return
- var tip: Marker3D = _owner.get_fishing_rod_tip()
- if tip == null:
- return
- _line_mesh.clear_surfaces()
- var midpoint: Vector3 = tip.global_position.lerp(_target, 0.5)
- midpoint.y -= 0.12
- _draw_line_ribbon(PackedVector3Array([
- tip.global_position,
- midpoint,
- _target,
- ]))
-
-
-func _draw_line_ribbon(points: PackedVector3Array) -> void:
- var camera: Camera3D = get_viewport().get_camera_3d()
- if camera == null:
- _line_mesh.surface_begin(Mesh.PRIMITIVE_LINE_STRIP)
- for point: Vector3 in points:
- _line_mesh.surface_add_vertex(to_local(point))
- _line_mesh.surface_end()
- return
-
- var half_thickness: float = LINE_THICKNESS * 0.5
- _line_mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLES)
- for point_index: int in range(1, points.size()):
- var start: Vector3 = points[point_index - 1]
- var end: Vector3 = points[point_index]
- var segment: Vector3 = end - start
- if segment.length_squared() <= 0.000001:
- continue
- var to_camera: Vector3 = (
- camera.global_position - start.lerp(end, 0.5)
- )
- var side: Vector3 = segment.normalized().cross(to_camera.normalized())
- if side.length_squared() <= 0.000001:
- side = segment.normalized().cross(Vector3.UP)
- if side.length_squared() <= 0.000001:
- side = segment.normalized().cross(Vector3.RIGHT)
- side = side.normalized() * half_thickness
-
- var start_left: Vector3 = to_local(start - side)
- var start_right: Vector3 = to_local(start + side)
- var end_right: Vector3 = to_local(end + side)
- var end_left: Vector3 = to_local(end - side)
- _line_mesh.surface_add_vertex(start_left)
- _line_mesh.surface_add_vertex(start_right)
- _line_mesh.surface_add_vertex(end_right)
- _line_mesh.surface_add_vertex(start_left)
- _line_mesh.surface_add_vertex(end_right)
- _line_mesh.surface_add_vertex(end_left)
- _line_mesh.surface_end()
diff --git a/fishing/remote_fishing_presentation.gd.uid b/fishing/remote_fishing_presentation.gd.uid
deleted file mode 100644
index 62f590d..0000000
--- a/fishing/remote_fishing_presentation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://db3t8mrcaw062
diff --git a/gathering/catalog/beetle_stag_common.tres b/gathering/catalog/beetle_stag_common.tres
deleted file mode 100644
index c8b7d85..0000000
--- a/gathering/catalog/beetle_stag_common.tres
+++ /dev/null
@@ -1,28 +0,0 @@
-[gd_resource type="Resource" script_class="GatherableData" load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://gathering/gatherable_data.gd" id="1_data"]
-[ext_resource type="Resource" path="res://fish/species/beetle_stag_common/beetle_stag_common.tres" id="2_catch"]
-
-[resource]
-script = ExtResource("1_data")
-type_id = &"beetle_stag_common"
-catch_data = ExtResource("2_catch")
-required_tool_id = &"crab_net"
-spawn_anchor_set_id = &"starter_reachable_tree_trunks"
-population = 3
-spawn_anchor_occupancy_ratio = 0.35
-maximum_anchor_population = 64
-requires_sneaking = false
-movement_speed = 0.0
-roam_radius = 0.1
-scare_radius = 0.1
-capture_radius = 0.34
-interaction_range = 2.8
-charge_duration = 1.0
-sprite_pixel_size = 0.0075
-sprite_tilt_degrees = 0.0
-capture_respawn_min_seconds = 90.0
-capture_respawn_max_seconds = 150.0
-scare_respawn_min_seconds = 0.0
-scare_respawn_max_seconds = 0.0
-minimum_respawn_spacing_seconds = 30.0
diff --git a/gathering/catalog/clam_manila.tres b/gathering/catalog/clam_manila.tres
deleted file mode 100644
index 69a9c89..0000000
--- a/gathering/catalog/clam_manila.tres
+++ /dev/null
@@ -1,27 +0,0 @@
-[gd_resource type="Resource" script_class="GatherableData" load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://gathering/gatherable_data.gd" id="1_data"]
-[ext_resource type="Resource" path="res://fish/species/clam_manila/clam_manila.tres" id="2_catch"]
-
-[resource]
-script = ExtResource("1_data")
-type_id = &"clam_manila"
-catch_data = ExtResource("2_catch")
-required_tool_id = &"standard_shovel"
-diggable_area_id = &"starter_beach"
-minimum_surface_y = -0.44
-population = 3
-presentation_mode = 1
-requires_sneaking = false
-active_lifetime_seconds = 10.0
-movement_speed = 0.0
-roam_radius = 0.1
-scare_radius = 0.1
-capture_radius = 0.38
-interaction_range = 2.4
-charge_duration = 0.55
-capture_respawn_min_seconds = 8.0
-capture_respawn_max_seconds = 14.0
-scare_respawn_min_seconds = 0.0
-scare_respawn_max_seconds = 0.0
-minimum_respawn_spacing_seconds = 4.0
diff --git a/gathering/catalog/crab_blue.tres b/gathering/catalog/crab_blue.tres
deleted file mode 100644
index 1ccabe8..0000000
--- a/gathering/catalog/crab_blue.tres
+++ /dev/null
@@ -1,14 +0,0 @@
-[gd_resource type="Resource" script_class="GatherableData" load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://gathering/gatherable_data.gd" id="1_data"]
-[ext_resource type="Resource" path="res://fish/species/crab_blue/crab_blue.tres" id="2_catch"]
-
-[resource]
-script = ExtResource("1_data")
-type_id = &"crab_blue"
-catch_data = ExtResource("2_catch")
-required_tool_id = &"crab_net"
-surface_materials = Array[StringName]([&"sand"])
-minimum_surface_y = -0.8
-population = 4
-sprite_pixel_size = 0.0005
diff --git a/gathering/catalog/crab_brown.tres b/gathering/catalog/crab_brown.tres
deleted file mode 100644
index 83d20c8..0000000
--- a/gathering/catalog/crab_brown.tres
+++ /dev/null
@@ -1,26 +0,0 @@
-[gd_resource type="Resource" script_class="GatherableData" load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://gathering/gatherable_data.gd" id="1_data"]
-[ext_resource type="Resource" path="res://fish/species/crab_brown/crab_brown.tres" id="2_catch"]
-
-[resource]
-script = ExtResource("1_data")
-type_id = &"crab_brown"
-catch_data = ExtResource("2_catch")
-required_tool_id = &"crab_net"
-surface_materials = Array[StringName]([&"sand"])
-minimum_surface_y = -0.44
-population = 2
-movement_speed = 0.35
-roam_radius = 3.5
-scare_radius = 2.8
-capture_radius = 0.7
-interaction_range = 2.6
-charge_duration = 2.0
-sprite_pixel_size = 0.0005
-sprite_tilt_degrees = -45.0
-capture_respawn_min_seconds = 480.0
-capture_respawn_max_seconds = 720.0
-scare_respawn_min_seconds = 45.0
-scare_respawn_max_seconds = 90.0
-minimum_respawn_spacing_seconds = 180.0
diff --git a/gathering/catalog/crab_dungeness.tres b/gathering/catalog/crab_dungeness.tres
deleted file mode 100644
index 12eb7a6..0000000
--- a/gathering/catalog/crab_dungeness.tres
+++ /dev/null
@@ -1,14 +0,0 @@
-[gd_resource type="Resource" script_class="GatherableData" load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://gathering/gatherable_data.gd" id="1_data"]
-[ext_resource type="Resource" path="res://fish/species/crab_dungeness/crab_dungeness.tres" id="2_catch"]
-
-[resource]
-script = ExtResource("1_data")
-type_id = &"crab_dungeness"
-catch_data = ExtResource("2_catch")
-required_tool_id = &"crab_net"
-surface_materials = Array[StringName]([&"sand"])
-minimum_surface_y = -0.5
-population = 4
-sprite_pixel_size = 0.0005
diff --git a/gathering/catalog/crab_ghost.tres b/gathering/catalog/crab_ghost.tres
deleted file mode 100644
index 62ce103..0000000
--- a/gathering/catalog/crab_ghost.tres
+++ /dev/null
@@ -1,14 +0,0 @@
-[gd_resource type="Resource" script_class="GatherableData" load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://gathering/gatherable_data.gd" id="1_data"]
-[ext_resource type="Resource" path="res://fish/species/crab_ghost/crab_ghost.tres" id="2_catch"]
-
-[resource]
-script = ExtResource("1_data")
-type_id = &"crab_ghost"
-catch_data = ExtResource("2_catch")
-required_tool_id = &"crab_net"
-surface_materials = Array[StringName]([&"sand"])
-minimum_surface_y = 0.08
-population = 4
-sprite_pixel_size = 0.0005
diff --git a/gathering/catalog/gatherable_catalog.tres b/gathering/catalog/gatherable_catalog.tres
deleted file mode 100644
index 4ea1ff4..0000000
--- a/gathering/catalog/gatherable_catalog.tres
+++ /dev/null
@@ -1,13 +0,0 @@
-[gd_resource type="Resource" script_class="GatherableCatalog" load_steps=8 format=3]
-
-[ext_resource type="Script" path="res://gathering/gatherable_catalog.gd" id="1_catalog"]
-[ext_resource type="Resource" path="res://gathering/catalog/crab_brown.tres" id="2_brown"]
-[ext_resource type="Resource" path="res://gathering/catalog/crab_ghost.tres" id="3_ghost"]
-[ext_resource type="Resource" path="res://gathering/catalog/crab_blue.tres" id="4_blue"]
-[ext_resource type="Resource" path="res://gathering/catalog/crab_dungeness.tres" id="5_dungeness"]
-[ext_resource type="Resource" path="res://gathering/catalog/clam_manila.tres" id="6_clam"]
-[ext_resource type="Resource" path="res://gathering/catalog/beetle_stag_common.tres" id="7_beetle"]
-
-[resource]
-script = ExtResource("1_catalog")
-entries = [ExtResource("2_brown"), ExtResource("3_ghost"), ExtResource("4_blue"), ExtResource("5_dungeness"), ExtResource("6_clam"), ExtResource("7_beetle")]
diff --git a/gathering/gatherable_catalog.gd b/gathering/gatherable_catalog.gd
deleted file mode 100644
index 3fbb06a..0000000
--- a/gathering/gatherable_catalog.gd
+++ /dev/null
@@ -1,26 +0,0 @@
-class_name GatherableCatalog
-extends Resource
-
-const GatherableDataType = preload("res://gathering/gatherable_data.gd")
-const CalendarSeasonType = preload("res://world/calendar_season.gd")
-
-@export var entries: Array[GatherableDataType] = []
-
-
-func get_entry(type_id: StringName) -> GatherableDataType:
- if type_id.is_empty():
- return null
- for entry: GatherableDataType in entries:
- if entry != null and entry.type_id == type_id:
- return entry
- return null
-
-
-func get_available_entries(
- season: int = CalendarSeasonType.UNKNOWN,
-) -> Array[GatherableDataType]:
- var available: Array[GatherableDataType] = []
- for entry: GatherableDataType in entries:
- if entry != null and entry.is_available(season):
- available.append(entry)
- return available
diff --git a/gathering/gatherable_catalog.gd.uid b/gathering/gatherable_catalog.gd.uid
deleted file mode 100644
index 7f74b07..0000000
--- a/gathering/gatherable_catalog.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bpsncwh7qlfk1
diff --git a/gathering/gatherable_data.gd b/gathering/gatherable_data.gd
deleted file mode 100644
index d60dcc9..0000000
--- a/gathering/gatherable_data.gd
+++ /dev/null
@@ -1,184 +0,0 @@
-class_name GatherableData
-extends Resource
-
-const FishDataType = preload("res://fish/fish_data.gd")
-const FishQualityType = preload("res://fish/fish_quality.gd")
-const CalendarSeasonType = preload("res://world/calendar_season.gd")
-
-enum PresentationMode {
- VISIBLE_CREATURE,
- WATER_SPURT,
-}
-
-@export var type_id: StringName
-@export var catch_data: FishDataType
-@export var required_tool_id: StringName
-@export var surface_materials: Array[StringName] = []
-@export var diggable_area_id: StringName
-@export var spawn_anchor_set_id: StringName
-@export_range(-100.0, 100.0, 0.01) var minimum_surface_y: float = 0.08
-@export_range(0, 64, 1) var population: int = 0
-## Anchored gatherables can scale with authored/generated attachment geometry.
-## Zero preserves the fixed population above.
-@export_range(0.0, 1.0, 0.01) var spawn_anchor_occupancy_ratio := 0.0
-## Zero leaves the anchor count as the only ceiling.
-@export_range(0, 256, 1) var maximum_anchor_population := 0
-@export var presentation_mode: PresentationMode = PresentationMode.VISIBLE_CREATURE
-@export var requires_sneaking: bool = true
-@export_range(0.0, 120.0, 0.1) var active_lifetime_seconds: float = 0.0
-@export_range(0.0, 10.0, 0.05) var movement_speed: float = 0.35
-@export_range(0.1, 20.0, 0.1) var roam_radius: float = 3.5
-@export_range(0.1, 20.0, 0.1) var scare_radius: float = 2.8
-@export var quality_movement_speed_multipliers: Array[float] = [
- 1.0,
- 1.15,
- 1.35,
- 1.6,
- 2.0,
-]
-@export var quality_scare_radius_multipliers: Array[float] = [
- 1.0,
- 1.1,
- 1.25,
- 1.45,
- 1.7,
-]
-@export_range(0.1, 5.0, 0.05) var capture_radius: float = 0.7
-@export_range(0.1, 10.0, 0.05) var interaction_range: float = 2.6
-@export_range(0.1, 10.0, 0.05) var charge_duration: float = 2.0
-@export_range(0.0001, 0.01, 0.00005) var sprite_pixel_size: float = 0.001
-@export_range(-90.0, 90.0, 1.0) var sprite_tilt_degrees: float = -45.0
-@export_category("Respawn Budget")
-@export_range(0.0, 3600.0, 1.0) var capture_respawn_min_seconds: float = 480.0
-@export_range(0.0, 3600.0, 1.0) var capture_respawn_max_seconds: float = 720.0
-@export_range(0.0, 3600.0, 1.0) var scare_respawn_min_seconds: float = 45.0
-@export_range(0.0, 3600.0, 1.0) var scare_respawn_max_seconds: float = 90.0
-@export_range(0.0, 3600.0, 1.0) var minimum_respawn_spacing_seconds: float = 180.0
-
-
-func is_valid() -> bool:
- var uses_diggable_area: bool = not diggable_area_id.is_empty()
- var uses_spawn_anchors: bool = not spawn_anchor_set_id.is_empty()
- var movement_parameters_valid: bool = (
- movement_speed >= 0.0
- and (
- presentation_mode == PresentationMode.WATER_SPURT
- or (roam_radius > 0.0 and scare_radius > 0.0)
- )
- )
- return (
- not type_id.is_empty()
- and catch_data != null
- and catch_data.is_valid_catalog_entry()
- and catch_data.collection_method != FishDataType.CollectionMethod.FISHING
- and not required_tool_id.is_empty()
- and (
- uses_diggable_area
- or uses_spawn_anchors
- or not surface_materials.is_empty()
- )
- and (
- not uses_diggable_area
- or catch_data.collection_method
- == FishDataType.CollectionMethod.DIGGING
- )
- and population > 0
- and (
- maximum_anchor_population == 0
- or maximum_anchor_population >= population
- )
- and movement_parameters_valid
- and _quality_multipliers_are_valid(
- quality_movement_speed_multipliers
- )
- and _quality_multipliers_are_valid(
- quality_scare_radius_multipliers
- )
- and capture_radius > 0.0
- and interaction_range > 0.0
- and charge_duration > 0.0
- and sprite_pixel_size > 0.0
- and capture_respawn_max_seconds >= capture_respawn_min_seconds
- and scare_respawn_max_seconds >= scare_respawn_min_seconds
- )
-
-
-func is_stationary_hotspot() -> bool:
- return presentation_mode == PresentationMode.WATER_SPURT
-
-
-func is_stationary_spawn() -> bool:
- return is_stationary_hotspot() or not spawn_anchor_set_id.is_empty()
-
-
-func target_population_for_anchor_count(anchor_count: int) -> int:
- if spawn_anchor_set_id.is_empty() or spawn_anchor_occupancy_ratio <= 0.0:
- return population
- if anchor_count <= 0:
- return 0
- var target := maxi(
- population,
- ceili(float(anchor_count) * spawn_anchor_occupancy_ratio),
- )
- target = mini(target, anchor_count)
- if maximum_anchor_population > 0:
- target = mini(target, maximum_anchor_population)
- return target
-
-
-func can_be_scared() -> bool:
- return not is_stationary_spawn() and scare_radius > 0.0
-
-
-func get_movement_speed_for_quality(quality: int) -> float:
- return movement_speed * _quality_multiplier(
- quality_movement_speed_multipliers,
- quality,
- )
-
-
-func get_scare_radius_for_quality(quality: int) -> float:
- return scare_radius * _quality_multiplier(
- quality_scare_radius_multipliers,
- quality,
- )
-
-
-func get_respawn_delay(reason: StringName, rng: RandomNumberGenerator) -> float:
- var minimum_seconds: float = capture_respawn_min_seconds
- var maximum_seconds: float = capture_respawn_max_seconds
- if reason == &"scared":
- minimum_seconds = scare_respawn_min_seconds
- maximum_seconds = scare_respawn_max_seconds
- if rng == null or maximum_seconds <= minimum_seconds:
- return minimum_seconds
- return rng.randf_range(minimum_seconds, maximum_seconds)
-
-
-func is_available(season: int = CalendarSeasonType.UNKNOWN) -> bool:
- return (
- catch_data != null
- and catch_data.active
- and catch_data.is_available_in_season(season)
- and is_valid()
- )
-
-
-func _quality_multiplier(values: Array[float], quality: int) -> float:
- if values.size() != FishQualityType.TIER_COUNT:
- return 1.0
- var safe_quality: int = (
- quality
- if FishQualityType.is_valid(quality)
- else FishQualityType.Tier.BORING
- )
- return maxf(values[safe_quality], 0.0)
-
-
-func _quality_multipliers_are_valid(values: Array[float]) -> bool:
- if values.size() != FishQualityType.TIER_COUNT:
- return false
- for multiplier: float in values:
- if not is_finite(multiplier) or multiplier <= 0.0:
- return false
- return true
diff --git a/gathering/gatherable_data.gd.uid b/gathering/gatherable_data.gd.uid
deleted file mode 100644
index 5875fff..0000000
--- a/gathering/gatherable_data.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://doheo0308illx
diff --git a/gathering/gathering_controller.gd b/gathering/gathering_controller.gd
deleted file mode 100644
index c061db5..0000000
--- a/gathering/gathering_controller.gd
+++ /dev/null
@@ -1,367 +0,0 @@
-class_name GatheringController
-extends Node3D
-
-const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
-const FishingSpotType = preload("res://fishing/fishing_spot.gd")
-const NetworkWorldSpawnServiceType = preload(
- "res://network/network_world_spawn_service.gd"
-)
-const MARKER_INVALID_COLOR := Color(1.0, 1.0, 1.0, 1.0)
-const MARKER_VALID_COLOR := Color(0.25, 0.9, 0.36, 1.0)
-const MARKER_DISC_THICKNESS := 0.006
-
-signal status_changed(message: String)
-
-@export_range(0.5, 5.0, 0.05) var marker_distance: float = 0.85
-@export_range(0.025, 2.0, 0.025) var marker_radius: float = 0.175
-@export_range(0.0, 1.0, 0.01) var marker_surface_offset: float = 0.045
-@export_range(0.0, 2.0, 0.05) var forward_probe_extra_distance: float = 0.6
-@export_flags_3d_physics var terrain_collision_mask: int = 1
-@export_range(0.5, 20.0, 0.5) var ray_height: float = 5.0
-@export_range(0.5, 20.0, 0.5) var ray_depth: float = 8.0
-
-var _player: Player
-var _bag: PlayerBag
-var _hotbar: PlayerHotbar
-var _fishing_spot: FishingSpotType
-var _service: NetworkWorldSpawnServiceType
-var _marker: MeshInstance3D
-var _marker_invalid_material: StandardMaterial3D
-var _marker_valid_material: StandardMaterial3D
-var _marker_position: Vector3
-var _marker_has_surface: bool = false
-var _target_entity_id: String = ""
-var _charging: bool = false
-var _charge_elapsed: float = 0.0
-var _charge_duration: float = 2.0
-var _request_id: String = ""
-var _active_tool_id: StringName
-var _gameplay_input_enabled: bool = false
-
-
-func _ready() -> void:
- _build_marker()
- set_process_unhandled_input(true)
-
-
-func setup(
- player: Player,
- bag: PlayerBag,
- hotbar: PlayerHotbar,
- fishing_spot: FishingSpotType,
- service: NetworkWorldSpawnServiceType,
-) -> void:
- _player = player
- _bag = bag
- _hotbar = hotbar
- _fishing_spot = fishing_spot
- _service = service
- _service.local_interaction_finished.connect(
- _on_interaction_finished
- )
-
-
-func set_gameplay_input_enabled(enabled: bool) -> void:
- _gameplay_input_enabled = enabled
- if not enabled:
- _cancel_charge()
-
-
-func is_net_selected() -> bool:
- return (
- _bag != null
- and _hotbar != null
- and _hotbar.get_selected_item_id()
- == FishingShopStockType.CRAB_NET_ID
- and _bag.owns_item(FishingShopStockType.CRAB_NET_ID)
- )
-
-
-func is_shovel_selected() -> bool:
- return _is_owned_tool_selected(FishingShopStockType.STANDARD_SHOVEL_ID)
-
-
-func _is_owned_tool_selected(tool_id: StringName) -> bool:
- return (
- _bag != null
- and _hotbar != null
- and _hotbar.get_selected_item_id() == tool_id
- and _bag.owns_item(tool_id)
- )
-
-
-func _get_selected_gathering_tool_id() -> StringName:
- if is_net_selected():
- return FishingShopStockType.CRAB_NET_ID
- if is_shovel_selected():
- return FishingShopStockType.STANDARD_SHOVEL_ID
- return StringName()
-
-
-func _process(delta: float) -> void:
- var selected_tool_id := _get_selected_gathering_tool_id()
- var input_available: bool = (
- _gameplay_input_enabled
- and not selected_tool_id.is_empty()
- and _player != null
- and _player.is_local_control_enabled()
- and _fishing_spot != null
- and _fishing_spot.can_change_hotbar_selection()
- )
- if _charging and selected_tool_id != _active_tool_id:
- _cancel_charge()
- input_available = false
- if not input_available:
- if _charging:
- _cancel_charge()
- _set_marker_visible(false)
- return
- if not _charging:
- _set_marker_visible(false)
- return
- _charge_elapsed += delta
- _update_marker_target()
- _update_target_entity()
- var target_entry: GatherableData = _service.get_entry_for_entity(
- _target_entity_id
- )
- if target_entry != null:
- _charge_duration = target_entry.charge_duration
- var fully_charged: bool = _charge_elapsed >= _charge_duration
- var valid_target: bool = (
- fully_charged
- and not _target_entity_id.is_empty()
- and (
- target_entry == null
- or not target_entry.requires_sneaking
- or _player.is_sneaking()
- )
- )
- _marker.material_override = (
- _marker_valid_material if valid_target else _marker_invalid_material
- )
- _set_marker_visible(_marker_has_surface)
-
-
-func _unhandled_input(event: InputEvent) -> void:
- if not event.is_action("fish_primary"):
- return
- if event.is_pressed():
- if (
- _gameplay_input_enabled
- and _player != null
- and _player.release_net_strike_hold()
- ):
- get_viewport().set_input_as_handled()
- return
- if (
- _charging
- or not _gameplay_input_enabled
- or _get_selected_gathering_tool_id().is_empty()
- or _fishing_spot == null
- or not _fishing_spot.can_change_hotbar_selection()
- ):
- return
- _request_id = _service.begin_local_interaction()
- if _request_id.is_empty():
- return
- _charging = true
- _active_tool_id = _get_selected_gathering_tool_id()
- _charge_elapsed = 0.0
- _charge_duration = maxf(
- _service.get_charge_duration_for_tool(
- _active_tool_id
- ),
- 0.1,
- )
- _update_marker_target()
- _update_target_entity()
- _player.begin_net_draw_visual()
- get_viewport().set_input_as_handled()
- return
- if not _charging:
- return
- var fully_charged: bool = _charge_elapsed >= _charge_duration
- var target_entry: GatherableData = _service.get_entry_for_entity(
- _target_entity_id
- )
- var valid_capture: bool = (
- fully_charged
- and _marker_has_surface
- and not _target_entity_id.is_empty()
- and (
- target_entry == null
- or not target_entry.requires_sneaking
- or _player.is_sneaking()
- )
- )
- _player.play_net_strike_visual()
- if valid_capture:
- _service.finish_local_interaction(
- _request_id,
- _target_entity_id,
- _marker_position,
- )
- else:
- _service.cancel_local_interaction(_request_id)
- _player.resolve_net_strike_visual(false)
- _reset_charge_state()
- get_viewport().set_input_as_handled()
-
-
-func _update_marker_target() -> void:
- _marker_has_surface = false
- if _player == null or get_world_3d() == null:
- return
- var facing: Vector3 = _player.get_facing_direction()
- facing.y = 0.0
- if facing.is_zero_approx():
- facing = -_player.global_basis.z
- facing.y = 0.0
- if facing.is_zero_approx():
- return
- facing = facing.normalized()
- var probe_origin: Vector3 = _player.get_body_center_position()
- var probe_direction: Vector3 = _get_surface_probe_direction(facing)
- var forward_hit: Dictionary = _cast_marker_ray(
- probe_origin,
- probe_origin
- + probe_direction
- * (marker_distance + forward_probe_extra_distance),
- )
- if _apply_marker_surface(forward_hit, facing):
- return
- var query_center: Vector3 = _player.global_position + facing * marker_distance
- var ground_hit: Dictionary = _cast_marker_ray(
- query_center + Vector3.UP * ray_height,
- query_center - Vector3.UP * ray_depth,
- )
- _apply_marker_surface(ground_hit, facing)
-
-
-func _get_surface_probe_direction(facing: Vector3) -> Vector3:
- var vertical_aim: float = 0.0
- var camera := _player.get_active_gameplay_camera()
- if camera != null:
- vertical_aim = clampf((-camera.global_basis.z).y, -0.75, 0.75)
- return Vector3(facing.x, vertical_aim, facing.z).normalized()
-
-
-func _cast_marker_ray(from: Vector3, to: Vector3) -> Dictionary:
- var query := PhysicsRayQueryParameters3D.create(
- from,
- to,
- terrain_collision_mask,
- [_player.get_rid()],
- )
- query.collide_with_areas = false
- query.collide_with_bodies = true
- return get_world_3d().direct_space_state.intersect_ray(query)
-
-
-func _apply_marker_surface(hit: Dictionary, facing: Vector3) -> bool:
- if hit.is_empty():
- return false
- var position: Variant = hit.get("position")
- var normal_value: Variant = hit.get("normal")
- if (
- typeof(position) != TYPE_VECTOR3
- or typeof(normal_value) != TYPE_VECTOR3
- ):
- return false
- var surface_normal := normal_value as Vector3
- if not surface_normal.is_finite() or surface_normal.is_zero_approx():
- return false
- surface_normal = surface_normal.normalized()
- _marker_position = (
- (position as Vector3) + surface_normal * marker_surface_offset
- )
- _marker.global_position = _marker_position
- _marker.global_basis = marker_basis_for_surface(surface_normal, facing)
- _marker_has_surface = true
- return true
-
-
-static func marker_basis_for_surface(
- surface_normal: Vector3,
- facing: Vector3,
-) -> Basis:
- var normal := surface_normal.normalized()
- var reference := Vector3.UP
- if absf(normal.dot(Vector3.UP)) > 0.95:
- reference = facing.normalized()
- if reference.is_zero_approx() or absf(normal.dot(reference)) > 0.95:
- reference = Vector3.FORWARD
- var x_axis := reference.cross(normal).normalized()
- if x_axis.is_zero_approx():
- x_axis = Vector3.RIGHT
- var z_axis := x_axis.cross(normal).normalized()
- return Basis(x_axis, normal, z_axis).orthonormalized()
-
-
-func _update_target_entity() -> void:
- _target_entity_id = (
- _service.find_capture_target(
- _marker_position,
- _active_tool_id,
- )
- if _marker_has_surface and _service != null
- else ""
- )
-
-
-func _cancel_charge() -> void:
- if _charging and not _request_id.is_empty() and _service != null:
- _service.cancel_local_interaction(_request_id)
- if _charging and _player != null:
- _player.cancel_net_action_visual()
- _reset_charge_state()
-
-
-func _reset_charge_state() -> void:
- _charging = false
- _charge_elapsed = 0.0
- _request_id = ""
- _active_tool_id = StringName()
- _target_entity_id = ""
- _marker_has_surface = false
- _set_marker_visible(false)
-
-
-func _on_interaction_finished(accepted: bool, message: String) -> void:
- if _player != null:
- _player.resolve_net_strike_visual(accepted)
- _reset_charge_state()
- if not accepted or not message.is_empty():
- status_changed.emit(message)
-
-
-func _build_marker() -> void:
- _marker_invalid_material = StandardMaterial3D.new()
- _marker_invalid_material.albedo_color = MARKER_INVALID_COLOR
- _marker_invalid_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
- _marker_invalid_material.roughness = 1.0
- _marker_invalid_material.no_depth_test = true
- _marker_valid_material = StandardMaterial3D.new()
- _marker_valid_material.albedo_color = MARKER_VALID_COLOR
- _marker_valid_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
- _marker_valid_material.roughness = 1.0
- _marker_valid_material.no_depth_test = true
- var disc := CylinderMesh.new()
- disc.top_radius = marker_radius
- disc.bottom_radius = marker_radius
- disc.height = MARKER_DISC_THICKNESS
- disc.radial_segments = 32
- disc.rings = 1
- _marker = MeshInstance3D.new()
- _marker.name = "GatheringTargetMarker"
- _marker.mesh = disc
- _marker.material_override = _marker_invalid_material
- _marker.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
- _marker.visible = false
- add_child(_marker)
-
-
-func _set_marker_visible(value: bool) -> void:
- if _marker != null:
- _marker.visible = value
diff --git a/gathering/gathering_controller.gd.uid b/gathering/gathering_controller.gd.uid
deleted file mode 100644
index 30e9ead..0000000
--- a/gathering/gathering_controller.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cec7nwk815d8a
diff --git a/gathering/world_gatherable.gd b/gathering/world_gatherable.gd
deleted file mode 100644
index 1a71477..0000000
--- a/gathering/world_gatherable.gd
+++ /dev/null
@@ -1,212 +0,0 @@
-class_name WorldGatherable
-extends Node3D
-
-const GatherableDataType = preload("res://gathering/gatherable_data.gd")
-const REFERENCE_SPRITE_PIXEL_SIZE: float = 0.001
-const REFERENCE_SPRITE_HEIGHT: float = 0.22
-const WATER_SPURT_INTERVAL_SECONDS: float = 1.15
-const WATER_SPURT_COLOR := Color(0.48, 0.82, 0.84, 1.0)
-const WATER_SPURT_HOLE_COLOR := Color(0.23, 0.27, 0.24, 1.0)
-
-var entity_id: String = ""
-var type_id: StringName
-var data: GatherableDataType
-var _sprite: Sprite3D
-var _water_spurt_root: Node3D
-var _water_spurt_elapsed: float = 0.0
-var _target_position: Vector3
-var _target_yaw: float = 0.0
-var _has_state: bool = false
-var _despawning: bool = false
-
-
-func configure(
- configured_entity_id: String,
- configured_data: GatherableDataType,
- position: Vector3,
- yaw: float,
-) -> void:
- entity_id = configured_entity_id
- data = configured_data
- type_id = data.type_id if data != null else StringName()
- # Establish the authoritative transform before constructing transient visuals.
- # Effects emitted before this point would begin at the default world origin.
- apply_network_state(position, yaw, true)
- if data != null and data.is_stationary_hotspot():
- _ensure_water_spurt_visual()
- _water_spurt_elapsed = (
- float(abs(hash(entity_id)) % 1000) / 1000.0
- * WATER_SPURT_INTERVAL_SECONDS
- )
- _emit_water_spurt()
- else:
- _ensure_visual()
- if data != null and data.catch_data != null and _sprite != null:
- _sprite.texture = data.catch_data.display_texture
- _sprite.pixel_size = data.sprite_pixel_size
- _sprite.position.y = (
- 0.0
- if not data.spawn_anchor_set_id.is_empty()
- else (
- REFERENCE_SPRITE_HEIGHT
- * data.sprite_pixel_size
- / REFERENCE_SPRITE_PIXEL_SIZE
- )
- )
- _sprite.rotation_degrees.x = data.sprite_tilt_degrees
-
-
-func apply_network_state(
- position: Vector3,
- yaw: float,
- immediate: bool = false,
-) -> void:
- if _despawning or not position.is_finite() or not is_finite(yaw):
- return
- _target_position = position
- _target_yaw = yaw
- if immediate or not _has_state:
- global_position = position
- rotation.y = yaw
- _has_state = true
-
-
-func play_despawn(with_dust: bool) -> void:
- if _despawning:
- return
- _despawning = true
- if with_dust:
- _emit_dust()
- if _water_spurt_root != null:
- _water_spurt_root.visible = false
- queue_free()
- return
- var tween: Tween = create_tween()
- tween.set_parallel(true)
- tween.set_trans(Tween.TRANS_QUAD)
- tween.set_ease(Tween.EASE_IN)
- if _sprite != null:
- tween.tween_property(_sprite, "position:y", -0.3, 0.3)
- tween.tween_property(_sprite, "scale", Vector3(0.75, 0.75, 0.75), 0.3)
- tween.chain().tween_callback(queue_free)
-
-
-func _process(delta: float) -> void:
- if not _has_state or _despawning:
- return
- global_position = global_position.lerp(
- _target_position,
- 1.0 - exp(-10.0 * delta),
- )
- rotation.y = lerp_angle(
- rotation.y,
- _target_yaw,
- 1.0 - exp(-8.0 * delta),
- )
- if _water_spurt_root != null:
- _water_spurt_elapsed += delta
- if _water_spurt_elapsed >= WATER_SPURT_INTERVAL_SECONDS:
- _water_spurt_elapsed = fmod(
- _water_spurt_elapsed,
- WATER_SPURT_INTERVAL_SECONDS,
- )
- _emit_water_spurt()
-
-
-func _ensure_visual() -> void:
- if _sprite != null:
- return
- _sprite = Sprite3D.new()
- _sprite.name = "GatherableSprite"
- _sprite.position.y = REFERENCE_SPRITE_HEIGHT
- _sprite.shaded = false
- _sprite.double_sided = true
- _sprite.billboard = BaseMaterial3D.BILLBOARD_ENABLED
- _sprite.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST
- add_child(_sprite)
-
-
-func _ensure_water_spurt_visual() -> void:
- if _water_spurt_root != null:
- return
- _water_spurt_root = Node3D.new()
- _water_spurt_root.name = "WaterSpurt"
- add_child(_water_spurt_root)
- var hole_material := StandardMaterial3D.new()
- hole_material.albedo_color = WATER_SPURT_HOLE_COLOR
- hole_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
- hole_material.roughness = 1.0
- hole_material.metallic = 0.0
- var hole_mesh := CylinderMesh.new()
- hole_mesh.top_radius = 0.035
- hole_mesh.bottom_radius = 0.035
- hole_mesh.height = 0.004
- hole_mesh.radial_segments = 12
- hole_mesh.rings = 1
- hole_mesh.material = hole_material
- var hole := MeshInstance3D.new()
- hole.name = "BurrowMark"
- hole.position.y = 0.008
- hole.mesh = hole_mesh
- hole.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
- _water_spurt_root.add_child(hole)
-
-
-func _emit_water_spurt() -> void:
- if _water_spurt_root == null or not _water_spurt_root.visible:
- return
- var particles := CPUParticles3D.new()
- particles.name = "WaterDroplets"
- particles.position.y = 0.035
- particles.amount = 6
- particles.lifetime = 0.42
- particles.one_shot = true
- particles.local_coords = true
- particles.explosiveness = 1.0
- particles.direction = Vector3.UP
- particles.spread = 32.0
- particles.gravity = Vector3(0.0, -3.4, 0.0)
- particles.initial_velocity_min = 0.8
- particles.initial_velocity_max = 1.2
- particles.scale_amount_min = 0.75
- particles.scale_amount_max = 1.0
- var water_material := StandardMaterial3D.new()
- water_material.albedo_color = WATER_SPURT_COLOR
- water_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
- water_material.roughness = 1.0
- water_material.metallic = 0.0
- var droplet_mesh := BoxMesh.new()
- droplet_mesh.size = Vector3(0.022, 0.035, 0.022)
- droplet_mesh.material = water_material
- particles.mesh = droplet_mesh
- _water_spurt_root.add_child(particles)
- particles.emitting = true
- var cleanup := create_tween()
- cleanup.tween_interval(0.55)
- cleanup.tween_callback(particles.queue_free)
-
-
-func _emit_dust() -> void:
- var particles := CPUParticles3D.new()
- particles.name = "DustPoof"
- particles.amount = 9
- particles.lifetime = 0.55
- particles.one_shot = true
- particles.explosiveness = 1.0
- particles.direction = Vector3.UP
- particles.spread = 58.0
- particles.gravity = Vector3(0.0, -2.4, 0.0)
- particles.initial_velocity_min = 0.65
- particles.initial_velocity_max = 1.25
- particles.scale_amount_min = 0.7
- particles.scale_amount_max = 1.35
- var dust_material := StandardMaterial3D.new()
- dust_material.albedo_color = Color(0.58, 0.46, 0.31, 0.85)
- dust_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
- dust_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
- var dust_mesh := BoxMesh.new()
- dust_mesh.size = Vector3(0.1, 0.1, 0.1)
- dust_mesh.material = dust_material
- particles.mesh = dust_mesh
- add_child(particles)
- particles.emitting = true
diff --git a/gathering/world_gatherable.gd.uid b/gathering/world_gatherable.gd.uid
deleted file mode 100644
index f0de7f7..0000000
--- a/gathering/world_gatherable.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://3t8ygawpkgwj
diff --git a/homes/assets/starter_motorcoach.glb b/homes/assets/starter_motorcoach.glb
deleted file mode 100644
index 5b0d022..0000000
Binary files a/homes/assets/starter_motorcoach.glb and /dev/null differ
diff --git a/homes/assets/starter_motorcoach.glb.import b/homes/assets/starter_motorcoach.glb.import
deleted file mode 100644
index 5cba35b..0000000
--- a/homes/assets/starter_motorcoach.glb.import
+++ /dev/null
@@ -1,45 +0,0 @@
-[remap]
-
-importer="scene"
-importer_version=1
-type="PackedScene"
-uid="uid://bhph22caipcj6"
-path="res://.godot/imported/starter_motorcoach.glb-153681ef1e622a556fba4df576dc6f84.scn"
-
-[deps]
-
-source_file="res://homes/assets/starter_motorcoach.glb"
-dest_files=["res://.godot/imported/starter_motorcoach.glb-153681ef1e622a556fba4df576dc6f84.scn"]
-
-[params]
-
-nodes/root_type=""
-nodes/root_name=""
-nodes/root_script=null
-mesh_library/use_node_names_as_mesh_names=false
-array_mesh/deduplicate_surfaces=true
-nodes/apply_root_scale=true
-nodes/root_scale=1.0
-nodes/import_as_skeleton_bones=false
-nodes/use_name_suffixes=true
-nodes/use_node_type_suffixes=true
-meshes/ensure_tangents=true
-meshes/generate_lods=true
-meshes/create_shadow_meshes=true
-meshes/light_baking=1
-meshes/lightmap_texel_size=0.2
-meshes/force_disable_compression=false
-skins/use_named_skins=true
-animation/import=true
-animation/fps=30
-animation/trimming=false
-animation/remove_immutable_tracks=true
-animation/import_rest_as_RESET=false
-import_script/path=""
-materials/extract=0
-materials/extract_format=0
-materials/extract_path=""
-_subresources={}
-gltf/naming_version=2
-gltf/embedded_image_handling=1
-gltf/texture_map_mode=1
diff --git a/homes/decor_catalog.gd b/homes/decor_catalog.gd
deleted file mode 100644
index 14e5e29..0000000
--- a/homes/decor_catalog.gd
+++ /dev/null
@@ -1,59 +0,0 @@
-class_name DecorCatalog
-extends RefCounted
-
-const BASIC_CUBE_ID: StringName = &"basic_cube"
-const MAX_OWNED_PER_PRODUCT: int = 32
-
-const _PRODUCTS: Dictionary = {
- BASIC_CUBE_ID: {
- "display_name": "basic cube",
- "description": "a simple piece of decor for testing your RV layout.",
- "price": 1,
- "footprint": Vector2(1.0, 0.8),
- "height": 0.8,
- },
-}
-
-
-static func get_product_ids() -> Array[StringName]:
- var ids: Array[StringName] = []
- for value: Variant in _PRODUCTS.keys():
- ids.append(StringName(str(value)))
- ids.sort_custom(func(first: StringName, second: StringName) -> bool:
- return String(first) < String(second)
- )
- return ids
-
-
-static func has_product(product_id: StringName) -> bool:
- return _PRODUCTS.has(product_id)
-
-
-static func get_product(product_id: StringName) -> Dictionary:
- var value: Variant = _PRODUCTS.get(product_id)
- return (
- (value as Dictionary).duplicate(true)
- if typeof(value) == TYPE_DICTIONARY
- else {}
- )
-
-
-static func get_display_name(product_id: StringName) -> String:
- return str(get_product(product_id).get("display_name", product_id))
-
-
-static func get_description(product_id: StringName) -> String:
- return str(get_product(product_id).get("description", ""))
-
-
-static func get_price(product_id: StringName) -> int:
- return int(get_product(product_id).get("price", -1))
-
-
-static func get_footprint(product_id: StringName) -> Vector2:
- var value: Variant = get_product(product_id).get("footprint", Vector2.ZERO)
- return value as Vector2 if value is Vector2 else Vector2.ZERO
-
-
-static func get_height(product_id: StringName) -> float:
- return float(get_product(product_id).get("height", 0.0))
diff --git a/homes/decor_catalog.gd.uid b/homes/decor_catalog.gd.uid
deleted file mode 100644
index 10d2530..0000000
--- a/homes/decor_catalog.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bvgwmexas2gps
diff --git a/homes/home_decor_controller.gd b/homes/home_decor_controller.gd
deleted file mode 100644
index 8f9aa5f..0000000
--- a/homes/home_decor_controller.gd
+++ /dev/null
@@ -1,499 +0,0 @@
-class_name HomeDecorController
-extends Node
-
-const DecorCatalogType = preload("res://homes/decor_catalog.gd")
-const DRAG_RESPONSE: float = 22.0
-const DRAG_POSITION_EPSILON: float = 0.005
-
-var _player: Player
-var _hotbar: PlayerHotbar
-var _bag: PlayerBag
-var _home_state: PlayerHomeState
-var _network_home: NetworkHomeService
-var _home_world: HomeWorldService
-var _fishing_spot: FishingSpot
-var _toolbar: HomeDecorToolbar
-var _mode: PlayerHomeState.PlacementMode = PlayerHomeState.PlacementMode.GRID
-var _selected_instance_id: String = ""
-var _drag_instance_id: String = ""
-var _drag_owner_fingerprint: String = ""
-var _drag_pointer_offset: Vector3 = Vector3.ZERO
-var _drag_original_position: Vector3 = Vector3.ZERO
-var _drag_original_yaw: float = 0.0
-var _drag_target_position: Vector3 = Vector3.ZERO
-var _drag_target_yaw: float = 0.0
-var _drag_target_valid: bool = false
-
-
-func setup(
- player: Player,
- hotbar: PlayerHotbar,
- bag: PlayerBag,
- home_state: PlayerHomeState,
- network_home: NetworkHomeService,
- home_world: HomeWorldService,
- fishing_spot: FishingSpot,
- toolbar: HomeDecorToolbar,
-) -> void:
- _player = player
- _hotbar = hotbar
- _bag = bag
- _home_state = home_state
- _network_home = network_home
- _home_world = home_world
- _fishing_spot = fishing_spot
- _toolbar = toolbar
- if not _network_home.local_space_changed.is_connected(
- _on_local_space_changed
- ):
- _network_home.local_space_changed.connect(_on_local_space_changed)
- if not _network_home.local_manifest_rejected.is_connected(
- _on_local_manifest_rejected
- ):
- _network_home.local_manifest_rejected.connect(
- _on_local_manifest_rejected
- )
- if not _home_state.changed.is_connected(_on_home_changed):
- _home_state.changed.connect(_on_home_changed)
- if not _toolbar.placement_mode_requested.is_connected(
- _on_placement_mode_requested
- ):
- _toolbar.placement_mode_requested.connect(
- _on_placement_mode_requested
- )
- _toolbar.rotate_requested.connect(_rotate_selected)
- _toolbar.store_requested.connect(_store_selected)
- _toolbar.cancel_requested.connect(_clear_selection)
- _toolbar.set_mode(_mode)
- _refresh_toolbar()
-
-
-func begin_decorating() -> void:
- _refresh_toolbar()
- if _toolbar.visible:
- _toolbar.report_status(
- "use held decor to place it; click or drag placed decor"
- )
-
-
-func _input(event: InputEvent) -> void:
- if _drag_instance_id.is_empty():
- return
- var pointer_motion := event as InputEventMouseMotion
- if pointer_motion != null:
- _update_drag(pointer_motion.position)
- get_viewport().set_input_as_handled()
- return
- var pointer_button := event as InputEventMouseButton
- if (
- pointer_button != null
- and pointer_button.button_index == MOUSE_BUTTON_LEFT
- and not pointer_button.pressed
- ):
- # A gameplay menu can take ownership between the press and release. Do
- # not let that release finalize decor behind the menu; preserve the
- # original transform instead.
- if _can_decorate():
- _finish_drag()
- else:
- _cancel_drag()
- get_viewport().set_input_as_handled()
-
-
-func _unhandled_input(event: InputEvent) -> void:
- var pointer_button := event as InputEventMouseButton
- if (
- pointer_button != null
- and pointer_button.button_index == MOUSE_BUTTON_LEFT
- ):
- if not pointer_button.pressed or not _can_decorate():
- return
- if _begin_drag(pointer_button.position):
- get_viewport().set_input_as_handled()
- return
- var pointer_decor_id: StringName = _get_selected_decor_id()
- if (
- not pointer_decor_id.is_empty()
- and _bag.get_quantity(pointer_decor_id) > 0
- ):
- _place_carried_decor(pointer_decor_id)
- get_viewport().set_input_as_handled()
- return
- if not _can_decorate() or not event.is_action_pressed("fish_primary"):
- return
- if event is InputEventKey and event.echo:
- return
- var decor_id: StringName = _get_selected_decor_id()
- if not decor_id.is_empty() and _bag.get_quantity(decor_id) > 0:
- _place_carried_decor(decor_id)
- get_viewport().set_input_as_handled()
-
-
-func _process(delta: float) -> void:
- if _drag_instance_id.is_empty():
- return
- # Menus and other gameplay input handoffs must cancel an active drag before
- # evaluating a held/released pointer. This prevents a same-frame TAB press
- # followed by left-button release from committing a move behind the UI.
- if not _can_decorate():
- _cancel_drag()
- return
- if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
- _finish_drag()
- return
- if not _drag_target_valid:
- return
- var current: Dictionary = _home_world.get_decor_preview_transform(
- _drag_owner_fingerprint, _drag_instance_id
- )
- if current.is_empty():
- _cancel_drag()
- return
- var current_position: Vector3 = current["position"]
- var current_yaw: float = float(current["yaw"])
- var blend: float = 1.0 - exp(-DRAG_RESPONSE * delta)
- _home_world.set_decor_preview_transform(
- _drag_owner_fingerprint,
- _drag_instance_id,
- current_position.lerp(_drag_target_position, blend),
- lerp_angle(current_yaw, _drag_target_yaw, blend),
- )
-
-
-func _begin_drag(screen_position: Vector2) -> bool:
- if _network_home.has_pending_local_decor_mutation():
- _toolbar.report_status("waiting for the previous decor update")
- return false
- var owner_fingerprint: String = (
- _network_home.get_local_home_owner_fingerprint()
- )
- var camera: Camera3D = _player.get_active_gameplay_camera()
- var clicked_instance: String = _home_world.find_decor_instance_at_screen(
- owner_fingerprint, camera, screen_position
- )
- if clicked_instance.is_empty():
- return false
- var placement: Dictionary = _home_state.get_placement(clicked_instance)
- if placement.is_empty():
- return false
- var original_position: Vector3 = _placement_position(placement)
- if not original_position.is_finite():
- return false
- if not _home_world.set_decor_manipulation_active(
- owner_fingerprint, clicked_instance, true
- ):
- return false
- var pointer_position: Vector3 = _home_world.get_floor_position_from_screen(
- owner_fingerprint, camera, screen_position
- )
- _drag_instance_id = clicked_instance
- _drag_owner_fingerprint = owner_fingerprint
- _drag_original_position = original_position
- _drag_original_yaw = float(placement.get("yaw", 0.0))
- _drag_target_position = original_position
- _drag_target_yaw = _drag_original_yaw
- _drag_target_valid = true
- _drag_pointer_offset = (
- original_position - pointer_position
- if pointer_position.is_finite()
- else Vector3.ZERO
- )
- _drag_pointer_offset.y = 0.0
- _select_instance(clicked_instance)
- _toolbar.report_status("drag decor, then release to place it")
- return true
-
-
-func _update_drag(screen_position: Vector2) -> void:
- if not _can_decorate():
- _cancel_drag()
- return
- var camera: Camera3D = _player.get_active_gameplay_camera()
- var pointer_position: Vector3 = _home_world.get_floor_position_from_screen(
- _drag_owner_fingerprint, camera, screen_position
- )
- if not pointer_position.is_finite():
- # Keep the most recent valid target when the pointer briefly leaves the
- # RV floor. Releasing there should place at that target, not throw the
- # decor all the way back to where the drag began.
- _toolbar.report_status("keep the pointer over the RV floor")
- return
- var placement: Dictionary = _home_state.get_placement(_drag_instance_id)
- if placement.is_empty():
- _clear_selection()
- return
- var candidate: Vector3 = pointer_position + _drag_pointer_offset
- candidate.y = 0.0
- var normalized: Dictionary = _network_home.get_valid_local_decor_transform(
- _drag_instance_id,
- candidate,
- float(placement.get("yaw", _drag_original_yaw)),
- _mode,
- )
- if normalized.is_empty():
- # A densely furnished area may have no nearby solution. Preserve the
- # last valid preview so the interaction remains stable while the player
- # moves the pointer toward open space.
- _toolbar.report_status("holding at the nearest available position")
- return
- _drag_target_position = normalized["position"]
- _drag_target_yaw = float(normalized["yaw"])
- _drag_target_valid = true
- _toolbar.report_status("release to place decor")
-
-
-func _finish_drag() -> void:
- var instance_id: String = _drag_instance_id
- var owner_fingerprint: String = _drag_owner_fingerprint
- var original_position: Vector3 = _drag_original_position
- var original_yaw: float = _drag_original_yaw
- var target_position: Vector3 = _drag_target_position
- var target_yaw: float = _drag_target_yaw
- var target_valid: bool = _drag_target_valid
- var placement: Dictionary = _home_state.get_placement(instance_id)
- var decor_id := StringName(str(placement.get("decor_id", "")))
- if (
- not target_valid
- or decor_id.is_empty()
- or not _home_world.is_decor_placement_clear_of_players(
- owner_fingerprint, decor_id, target_position, target_yaw
- )
- ):
- _home_world.set_decor_preview_transform(
- owner_fingerprint,
- instance_id,
- original_position,
- original_yaw,
- )
- _home_world.set_decor_manipulation_active(
- owner_fingerprint, instance_id, false
- )
- _reset_drag_state()
- _toolbar.report_status(
- "a player is in the way"
- if target_valid
- else "that space is not available"
- )
- return
- _home_world.set_decor_preview_transform(
- owner_fingerprint, instance_id, target_position, target_yaw
- )
- if (
- original_position.distance_to(target_position)
- <= DRAG_POSITION_EPSILON
- and is_zero_approx(angle_difference(original_yaw, target_yaw))
- ):
- _home_world.set_decor_manipulation_active(
- owner_fingerprint, instance_id, false
- )
- _reset_drag_state()
- _select_instance(instance_id)
- return
- if _network_home.update_local_decor(
- instance_id, target_position, target_yaw, _mode
- ):
- _home_world.set_decor_manipulation_active(
- owner_fingerprint, instance_id, false
- )
- _reset_drag_state()
- _toolbar.report_status("decor moved")
- return
- _home_world.set_decor_preview_transform(
- owner_fingerprint,
- instance_id,
- original_position,
- original_yaw,
- )
- _home_world.set_decor_manipulation_active(
- owner_fingerprint, instance_id, false
- )
- _reset_drag_state()
- _toolbar.report_status("that space is not available")
-
-
-func _cancel_drag() -> void:
- if _drag_instance_id.is_empty():
- return
- _home_world.set_decor_preview_transform(
- _drag_owner_fingerprint,
- _drag_instance_id,
- _drag_original_position,
- _drag_original_yaw,
- )
- _home_world.set_decor_manipulation_active(
- _drag_owner_fingerprint, _drag_instance_id, false
- )
- _reset_drag_state()
-
-
-func _reset_drag_state() -> void:
- _drag_instance_id = ""
- _drag_owner_fingerprint = ""
- _drag_pointer_offset = Vector3.ZERO
- _drag_original_position = Vector3.ZERO
- _drag_original_yaw = 0.0
- _drag_target_position = Vector3.ZERO
- _drag_target_yaw = 0.0
- _drag_target_valid = false
-
-
-static func _placement_position(placement: Dictionary) -> Vector3:
- if placement.is_empty():
- return Vector3.INF
- var values: Array = placement.get("position", [])
- if values.size() != 3:
- return Vector3.INF
- return Vector3(
- float(values[0]), float(values[1]), float(values[2])
- )
-
-
-func _place_carried_decor(decor_id: StringName) -> void:
- if _network_home.has_pending_local_decor_mutation():
- _toolbar.report_status("waiting for the previous decor update")
- return
- var owner_fingerprint: String = (
- _network_home.get_local_home_owner_fingerprint()
- )
- # RV transitions keep the gameplay root axis-aligned so its transform can be
- # synchronized independently from the presentation. Use the visual facing
- # direction instead; otherwise carried decor is always placed along world -Z
- # instead of in front of the character the player sees.
- var forward: Vector3 = _player.get_facing_direction()
- var global_position: Vector3 = _player.global_position + forward * 1.45
- var local_position: Vector3 = _home_world.interior_global_to_local(
- owner_fingerprint, global_position
- )
- var placement_yaw: float = atan2(-forward.x, -forward.z)
- var instance_id: String = _network_home.place_local_decor(
- decor_id, local_position, placement_yaw, _mode
- )
- if instance_id.is_empty():
- _toolbar.report_status("make room in front of you to place this decor")
- return
- # A local host can reject its own optimistic manifest synchronously. Do not
- # claim success after that rollback or leave the player looking for decor that
- # was never accepted into the room.
- if _home_state.get_placement(instance_id).is_empty():
- _toolbar.report_status("decor placement was not accepted")
- return
- _select_instance(instance_id)
- _toolbar.report_status("decor placed")
-
-
-func _select_instance(instance_id: String) -> void:
- var placement: Dictionary = _home_state.get_placement(instance_id)
- if placement.is_empty():
- _clear_selection()
- return
- _selected_instance_id = instance_id
- var decor_id := StringName(str(placement.get("decor_id", "")))
- _toolbar.set_selection(
- instance_id, DecorCatalogType.get_display_name(decor_id)
- )
-
-
-func _rotate_selected() -> void:
- if not _can_decorate() or _selected_instance_id.is_empty():
- return
- if _network_home.has_pending_local_decor_mutation():
- _toolbar.report_status("waiting for the previous decor update")
- return
- var placement: Dictionary = _home_state.get_placement(
- _selected_instance_id
- )
- if placement.is_empty():
- _clear_selection()
- return
- var values: Array = placement.get("position", [])
- if values.size() != 3:
- return
- var position := Vector3(
- float(values[0]), float(values[1]), float(values[2])
- )
- if _network_home.update_local_decor(
- _selected_instance_id,
- position,
- float(placement.get("yaw", 0.0)) + PI * 0.5,
- _mode,
- ):
- _toolbar.report_status("decor rotated")
- else:
- _toolbar.report_status("not enough room to rotate here")
-
-
-func _store_selected() -> void:
- if _selected_instance_id.is_empty():
- return
- if _network_home.has_pending_local_decor_mutation():
- _toolbar.report_status("waiting for the previous decor update")
- return
- if _network_home.remove_local_decor(_selected_instance_id):
- _clear_selection()
- _toolbar.report_status("decor packed into storage")
- else:
- _toolbar.report_status("inventory and storage are full")
-
-
-func _clear_selection() -> void:
- _cancel_drag()
- _selected_instance_id = ""
- if _toolbar != null:
- _toolbar.set_selection("")
-
-
-func _on_placement_mode_requested(mode: int) -> void:
- _mode = mode as PlayerHomeState.PlacementMode
-
-
-func _on_local_space_changed(
- _space_id: StringName,
- _owner_fingerprint: String,
-) -> void:
- _clear_selection()
- _refresh_toolbar()
-
-
-func _on_local_manifest_rejected(message: String) -> void:
- _clear_selection()
- if _toolbar != null and _toolbar.visible:
- _toolbar.report_status(message)
-
-
-func _on_home_changed() -> void:
- if (
- not _selected_instance_id.is_empty()
- and _home_state.get_placement(_selected_instance_id).is_empty()
- ):
- _clear_selection()
- _refresh_toolbar()
-
-
-func _refresh_toolbar() -> void:
- if _toolbar == null:
- return
- _toolbar.set_active(_can_decorate())
- if not _toolbar.visible:
- _clear_selection()
-
-
-func _can_decorate() -> bool:
- return (
- _player != null
- and _network_home != null
- and _network_home.can_local_decorate()
- and _fishing_spot != null
- and _fishing_spot.can_open_fishing_shop()
- and _player.is_movement_enabled()
- )
-
-
-func _get_selected_decor_id() -> StringName:
- if (
- _hotbar == null
- or _hotbar.get_selected_assignment_kind()
- != PlayerHotbar.AssignmentKind.ITEM
- ):
- return StringName()
- var item_id: StringName = _hotbar.get_selected_item_id()
- return item_id if DecorCatalogType.has_product(item_id) else StringName()
diff --git a/homes/home_decor_controller.gd.uid b/homes/home_decor_controller.gd.uid
deleted file mode 100644
index 1778125..0000000
--- a/homes/home_decor_controller.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b5j0mqcvhmi54
diff --git a/homes/home_world_service.gd b/homes/home_world_service.gd
deleted file mode 100644
index fd02d93..0000000
--- a/homes/home_world_service.gd
+++ /dev/null
@@ -1,1209 +0,0 @@
-class_name HomeWorldService
-extends Node3D
-
-const NetworkHomeProtocolType = preload(
- "res://network/network_home_protocol.gd"
-)
-const PlayerHomeStateType = preload("res://homes/player_home_state.gd")
-const DecorCatalogType = preload("res://homes/decor_catalog.gd")
-const STARTER_MOTORCOACH_SCENE: PackedScene = preload(
- "res://homes/assets/starter_motorcoach.glb"
-)
-
-const EXTERIOR_BODY_SIZE := Vector3(5.6, 2.5, 2.35)
-const EXTERIOR_DOOR_LOCAL := Vector3(-0.2, 0.85, 1.48)
-const EXTERIOR_SPAWN_LOCAL := Vector3(-0.2, 0.25, 2.45)
-const EXTERIOR_INTERACTION_DISTANCE: float = 2.7
-const INTERIOR_EXIT_DISTANCE: float = 2.1
-const INTERIOR_ORIGIN := Vector3(1000.0, 300.0, 1000.0)
-const INTERIOR_SPACING: float = 14.0
-const INTERIOR_MAX_SPAWN_SLOTS: int = 64
-const INTERIOR_FLOOR_SIZE := Vector3(8.0, 0.18, 4.5)
-const INTERIOR_WALL_HEIGHT: float = 2.7
-const MIN_EXTERIOR_TRIANGLE_AREA: float = 1.0
-const MIN_EXTERIOR_SEPARATION: float = 9.0
-const EXTERIOR_FOOTPRINT_HALF_EXTENTS := Vector2(3.85, 2.55)
-const EXTERIOR_SURFACE_INDEX_CELL_SIZE: float = 5.0
-const EXTERIOR_MAX_SURFACE_HEIGHT_DELTA: float = 0.24
-const EXTERIOR_CLEARANCE_HEIGHT: float = 2.7
-const WORLD_GEOMETRY_COLLISION_LAYER: int = 1
-const DECOR_SELECTION_COLLISION_LAYER: int = 1 << 8
-# Includes the player's 0.45 m capsule radius plus enough movement margin for
-# a full low-tick-rate frame before a newly solid placement can be reached.
-const PLAYER_DECOR_CLEARANCE_RADIUS: float = 0.8
-const BASIC_CUBE_SIZE := Vector3(1.0, 0.8, 0.8)
-
-var _world: TestWorld
-var _spawn_service: PlayerSpawnService
-var _session: NetworkSession
-var _dedicated_runtime: bool = false
-var _exteriors_root: Node3D
-var _interiors_root: Node3D
-var _presentations_by_fingerprint: Dictionary[String, Dictionary] = {}
-var _local_space: StringName = NetworkHomeProtocolType.WORLD_SPACE
-var _decor_under_manipulation: Dictionary[String, bool] = {}
-var _pending_solid_decor: Dictionary[String, Dictionary] = {}
-
-
-func setup(
- world: TestWorld,
- spawn_service: PlayerSpawnService,
- session: NetworkSession,
- dedicated_runtime: bool,
-) -> void:
- _world = world
- _spawn_service = spawn_service
- _session = session
- _dedicated_runtime = dedicated_runtime
- _ensure_roots()
- set_process(true)
-
-
-func is_world_ready() -> bool:
- return _world != null and _world.is_world_ready()
-
-
-func clear_presentations() -> void:
- _presentations_by_fingerprint.clear()
- _decor_under_manipulation.clear()
- _pending_solid_decor.clear()
- if is_instance_valid(_exteriors_root):
- for child: Node in _exteriors_root.get_children():
- child.queue_free()
- if is_instance_valid(_interiors_root):
- for child: Node in _interiors_root.get_children():
- child.queue_free()
-
-
-func remove_presentation(owner_fingerprint: String) -> void:
- var presentation: Dictionary = _presentations_by_fingerprint.get(
- owner_fingerprint, {}
- )
- if presentation.is_empty():
- return
- _clear_decor_collision_tracking(owner_fingerprint)
- _free_presentation_nodes(presentation)
- _presentations_by_fingerprint.erase(owner_fingerprint)
-
-
-func apply_manifest(data: Dictionary) -> bool:
- if (
- not is_world_ready()
- or not NetworkHomeProtocolType.validate_manifest_broadcast(data)
- ):
- return false
- _ensure_roots()
- var fingerprint: String = str(data["owner_fingerprint"])
- var prior: Dictionary = _presentations_by_fingerprint.get(fingerprint, {})
- var prior_instance_ids: Dictionary[String, bool] = {}
- if not prior.is_empty():
- var prior_home: Dictionary = prior.get("home", {})
- for value: Variant in prior_home.get("placements", []):
- if typeof(value) == TYPE_DICTIONARY:
- prior_instance_ids[str(
- (value as Dictionary).get("instance_id", "")
- )] = true
- if not prior.is_empty():
- _clear_decor_collision_tracking(fingerprint)
- _free_presentation_nodes(prior)
- var exterior_transform := NetworkHomeProtocolType.array_to_transform(
- data["exterior_transform"]
- )
- var interior_index: int = int(data["interior_index"])
- var home_data: Dictionary = PlayerHomeStateType.sanitize_save_data(
- data["home"]
- )
- var exterior := _build_exterior(fingerprint, exterior_transform)
- var interior := _build_interior(fingerprint, interior_index, home_data)
- var presentation: Dictionary = {
- "owner_peer_id": int(data["owner_peer_id"]),
- "fingerprint": fingerprint,
- "home": home_data,
- "exterior_transform": exterior_transform,
- "interior_index": interior_index,
- "exterior": exterior,
- "interior": interior,
- }
- _presentations_by_fingerprint[fingerprint] = presentation
- _refresh_local_visibility()
- if not prior.is_empty():
- for value: Variant in home_data.get("placements", []):
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var instance_id: String = str(
- (value as Dictionary).get("instance_id", "")
- )
- if not instance_id.is_empty() and not prior_instance_ids.has(
- instance_id
- ):
- call_deferred(
- "_spawn_decor_poof", fingerprint, instance_id
- )
- return true
-
-
-func choose_exterior_transform(
- owner_peer_id: int,
- reserved_transforms: Array[Transform3D],
-) -> Transform3D:
- var spawn_transform: Transform3D = (
- _world.get_player_spawn_transform()
- if _world != null else Transform3D.IDENTITY
- )
- var triangles: Array[PackedVector3Array] = (
- _world.get_spawn_surface_triangles(
- [&"grass_lite", &"grass", &"sand"], -0.5
- )
- if _world != null else []
- )
- if not triangles.is_empty():
- var surface_index: Dictionary = _build_surface_index(triangles)
- var strict_result: Transform3D = _find_exterior_transform(
- owner_peer_id,
- triangles,
- surface_index,
- spawn_transform,
- reserved_transforms,
- true,
- true,
- )
- if strict_result != Transform3D.IDENTITY:
- return strict_result
- var relaxed_result: Transform3D = _find_exterior_transform(
- owner_peer_id,
- triangles,
- surface_index,
- spawn_transform,
- reserved_transforms,
- false,
- true,
- )
- if relaxed_result != Transform3D.IDENTITY:
- return relaxed_result
- var dry_result: Transform3D = _find_exterior_transform(
- owner_peer_id,
- triangles,
- surface_index,
- spawn_transform,
- reserved_transforms,
- false,
- false,
- )
- if dry_result != Transform3D.IDENTITY:
- push_warning(
- "RV exterior used a dry fallback with obstructed clearance."
- )
- return dry_result
- push_error("No dry, level RV exterior footprint was available.")
- var fallback: Transform3D = spawn_transform
- var column: int = posmod(owner_peer_id - 1, 4)
- var row: int = maxi(floori(float(owner_peer_id - 1) / 4.0), 0)
- fallback.origin += Vector3(
- -12.0 + float(column) * 8.0,
- 0.0,
- 8.0 + float(row) * 7.0,
- )
- return fallback
-
-
-func _find_exterior_transform(
- owner_peer_id: int,
- triangles: Array[PackedVector3Array],
- surface_index: Dictionary,
- spawn_transform: Transform3D,
- reserved_transforms: Array[Transform3D],
- require_spawn_clearance: bool,
- require_obstacle_clearance: bool,
-) -> Transform3D:
- var triangle_count: int = triangles.size()
- var start: int = posmod(owner_peer_id * 37, triangle_count)
- var step: int = _coprime_step(triangle_count, 29)
- for offset: int in triangle_count:
- var triangle: PackedVector3Array = triangles[
- (start + offset * step) % triangle_count
- ]
- if not _is_exterior_candidate_triangle(triangle):
- continue
- var position: Vector3 = (
- triangle[0] + triangle[1] + triangle[2]
- ) / 3.0
- if (
- require_spawn_clearance
- and position.distance_to(spawn_transform.origin) < 6.0
- ):
- continue
- if _too_close_to_reserved(position, reserved_transforms):
- continue
- var yaw: float = float(posmod(owner_peer_id + offset, 4)) * PI * 0.5
- var grounded_position: Vector3 = _ground_exterior_footprint(
- position, yaw, triangles, surface_index
- )
- if not grounded_position.is_finite():
- continue
- if (
- require_obstacle_clearance
- and not _exterior_volume_is_clear(grounded_position, yaw)
- ):
- continue
- return Transform3D(Basis(Vector3.UP, yaw), grounded_position)
- return Transform3D.IDENTITY
-
-
-func _is_exterior_candidate_triangle(triangle: PackedVector3Array) -> bool:
- if triangle.size() != 3:
- return false
- var normal: Vector3 = (triangle[1] - triangle[0]).cross(
- triangle[2] - triangle[0]
- )
- var area: float = normal.length() * 0.5
- return (
- area >= MIN_EXTERIOR_TRIANGLE_AREA
- and absf(normal.normalized().y) >= 0.94
- )
-
-
-func _ground_exterior_footprint(
- position: Vector3,
- yaw: float,
- triangles: Array[PackedVector3Array],
- surface_index: Dictionary,
-) -> Vector3:
- var heights: Array[float] = []
- for local_sample: Vector2 in _exterior_footprint_samples():
- var rotated_sample: Vector3 = Vector3(
- local_sample.x, 0.0, local_sample.y
- ).rotated(Vector3.UP, yaw)
- var sample := Vector2(
- position.x + rotated_sample.x,
- position.z + rotated_sample.z,
- )
- var height: float = _surface_height_at(
- sample, position.y, triangles, surface_index
- )
- if not is_finite(height):
- return Vector3.INF
- heights.append(height)
- var minimum_height: float = heights.min()
- var maximum_height: float = heights.max()
- if maximum_height - minimum_height > EXTERIOR_MAX_SURFACE_HEIGHT_DELTA:
- return Vector3.INF
- return Vector3(position.x, maximum_height, position.z)
-
-
-func _exterior_footprint_samples() -> Array[Vector2]:
- var samples: Array[Vector2] = []
- for x_factor: float in [-1.0, -0.5, 0.0, 0.5, 1.0]:
- for z_factor: float in [-1.0, 0.0, 1.0]:
- samples.append(Vector2(
- EXTERIOR_FOOTPRINT_HALF_EXTENTS.x * x_factor,
- EXTERIOR_FOOTPRINT_HALF_EXTENTS.y * z_factor,
- ))
- return samples
-
-
-func _build_surface_index(
- triangles: Array[PackedVector3Array],
-) -> Dictionary:
- var result: Dictionary = {}
- for triangle_index: int in triangles.size():
- var triangle: PackedVector3Array = triangles[triangle_index]
- if triangle.size() != 3:
- continue
- var minimum_x: float = minf(
- triangle[0].x, minf(triangle[1].x, triangle[2].x)
- )
- var maximum_x: float = maxf(
- triangle[0].x, maxf(triangle[1].x, triangle[2].x)
- )
- var minimum_z: float = minf(
- triangle[0].z, minf(triangle[1].z, triangle[2].z)
- )
- var maximum_z: float = maxf(
- triangle[0].z, maxf(triangle[1].z, triangle[2].z)
- )
- var minimum_cell := Vector2i(
- floori(minimum_x / EXTERIOR_SURFACE_INDEX_CELL_SIZE),
- floori(minimum_z / EXTERIOR_SURFACE_INDEX_CELL_SIZE),
- )
- var maximum_cell := Vector2i(
- floori(maximum_x / EXTERIOR_SURFACE_INDEX_CELL_SIZE),
- floori(maximum_z / EXTERIOR_SURFACE_INDEX_CELL_SIZE),
- )
- for cell_x: int in range(minimum_cell.x, maximum_cell.x + 1):
- for cell_y: int in range(minimum_cell.y, maximum_cell.y + 1):
- var cell := Vector2i(cell_x, cell_y)
- var bucket: Array = result.get(cell, [])
- bucket.append(triangle_index)
- result[cell] = bucket
- return result
-
-
-func _surface_height_at(
- point: Vector2,
- preferred_height: float,
- triangles: Array[PackedVector3Array],
- surface_index: Dictionary,
-) -> float:
- var cell := Vector2i(
- floori(point.x / EXTERIOR_SURFACE_INDEX_CELL_SIZE),
- floori(point.y / EXTERIOR_SURFACE_INDEX_CELL_SIZE),
- )
- var best_height: float = INF
- var best_delta: float = INF
- for value: Variant in surface_index.get(cell, []):
- var triangle: PackedVector3Array = triangles[int(value)]
- var height: float = _triangle_height_at(point, triangle)
- if not is_finite(height):
- continue
- var delta: float = absf(height - preferred_height)
- if delta < best_delta:
- best_delta = delta
- best_height = height
- return best_height
-
-
-func _triangle_height_at(
- point: Vector2,
- triangle: PackedVector3Array,
-) -> float:
- var a := Vector2(triangle[0].x, triangle[0].z)
- var b := Vector2(triangle[1].x, triangle[1].z)
- var c := Vector2(triangle[2].x, triangle[2].z)
- var denominator: float = (
- (b.y - c.y) * (a.x - c.x)
- + (c.x - b.x) * (a.y - c.y)
- )
- if absf(denominator) <= 0.000001:
- return INF
- var weight_a: float = (
- (b.y - c.y) * (point.x - c.x)
- + (c.x - b.x) * (point.y - c.y)
- ) / denominator
- var weight_b: float = (
- (c.y - a.y) * (point.x - c.x)
- + (a.x - c.x) * (point.y - c.y)
- ) / denominator
- var weight_c: float = 1.0 - weight_a - weight_b
- const EDGE_EPSILON: float = 0.001
- if (
- weight_a < -EDGE_EPSILON
- or weight_b < -EDGE_EPSILON
- or weight_c < -EDGE_EPSILON
- ):
- return INF
- return (
- weight_a * triangle[0].y
- + weight_b * triangle[1].y
- + weight_c * triangle[2].y
- )
-
-
-func _exterior_volume_is_clear(position: Vector3, yaw: float) -> bool:
- if not is_inside_tree() or get_world_3d() == null:
- return true
- var shape := BoxShape3D.new()
- shape.size = Vector3(
- EXTERIOR_FOOTPRINT_HALF_EXTENTS.x * 2.0,
- EXTERIOR_CLEARANCE_HEIGHT,
- EXTERIOR_FOOTPRINT_HALF_EXTENTS.y * 2.0,
- )
- var query := PhysicsShapeQueryParameters3D.new()
- query.shape = shape
- query.transform = Transform3D(
- Basis(Vector3.UP, yaw),
- position + Vector3.UP * (EXTERIOR_CLEARANCE_HEIGHT * 0.5 + 0.2),
- )
- query.collision_mask = 1
- query.collide_with_areas = false
- query.collide_with_bodies = true
- return get_world_3d().direct_space_state.intersect_shape(query, 1).is_empty()
-
-
-func _coprime_step(value: int, preferred_step: int) -> int:
- var step: int = mini(preferred_step, maxi(value - 1, 1))
- while step > 1 and _greatest_common_divisor(step, value) != 1:
- step -= 1
- return step
-
-
-func _greatest_common_divisor(first: int, second: int) -> int:
- var a: int = absi(first)
- var b: int = absi(second)
- while b != 0:
- var remainder: int = a % b
- a = b
- b = remainder
- return a
-
-
-func get_exterior_transform(owner_fingerprint: String) -> Transform3D:
- var presentation: Dictionary = _presentations_by_fingerprint.get(
- owner_fingerprint, {}
- )
- return presentation.get("exterior_transform", Transform3D.IDENTITY)
-
-
-func get_exterior_prompt_position(owner_fingerprint: String) -> Vector3:
- return get_exterior_transform(owner_fingerprint) * EXTERIOR_DOOR_LOCAL
-
-
-func get_exterior_spawn_transform(owner_fingerprint: String) -> Transform3D:
- var exterior_transform: Transform3D = get_exterior_transform(
- owner_fingerprint
- )
- return Transform3D(
- exterior_transform.basis,
- exterior_transform * EXTERIOR_SPAWN_LOCAL,
- )
-
-
-func get_interior_spawn_transform(
- owner_fingerprint: String,
- visitor_offset: int = 0,
-) -> Transform3D:
- var anchor: Vector3 = get_interior_anchor(owner_fingerprint)
- # `visitor_offset` is allocated by the host as a compact room-local slot.
- # Keep it bounded as a last line of defense if a future caller accidentally
- # supplies an engine peer ID instead of a slot.
- var visitor_slot: int = posmod(visitor_offset, INTERIOR_MAX_SPAWN_SLOTS)
- # Build the origin as a value rather than mutating Transform3D.origin.
- var spawn_origin: Vector3 = anchor + Vector3(
- -1.0 + float(posmod(visitor_slot, 4)) * 0.65,
- 0.25,
- 0.4 + float(floori(float(visitor_slot) / 4.0)) * 0.55,
- )
- return Transform3D(Basis.IDENTITY, spawn_origin)
-
-
-func get_interior_anchor(owner_fingerprint: String) -> Vector3:
- var presentation: Dictionary = _presentations_by_fingerprint.get(
- owner_fingerprint, {}
- )
- var index: int = int(presentation.get("interior_index", 0))
- return INTERIOR_ORIGIN + Vector3(float(index) * INTERIOR_SPACING, 0.0, 0.0)
-
-
-func is_avatar_near_exterior(avatar: Player, owner_fingerprint: String) -> bool:
- if avatar == null or not _presentations_by_fingerprint.has(owner_fingerprint):
- return false
- var transform: Transform3D = get_exterior_transform(owner_fingerprint)
- var door_position: Vector3 = transform * EXTERIOR_DOOR_LOCAL
- return avatar.global_position.distance_to(door_position) <= (
- EXTERIOR_INTERACTION_DISTANCE
- )
-
-
-func is_avatar_near_interior_exit(
- avatar: Player,
- owner_fingerprint: String,
-) -> bool:
- if avatar == null:
- return false
- return avatar.global_position.distance_to(
- get_interior_exit_position(owner_fingerprint)
- ) <= INTERIOR_EXIT_DISTANCE
-
-
-func get_interior_exit_position(owner_fingerprint: String) -> Vector3:
- return get_interior_anchor(owner_fingerprint) + Vector3(
- -INTERIOR_FLOOR_SIZE.x * 0.5 + 0.6,
- 0.35,
- 0.0,
- )
-
-
-func get_interior_prompt_position(owner_fingerprint: String) -> Vector3:
- return get_interior_exit_position(owner_fingerprint) + Vector3.UP * 1.2
-
-
-func interior_local_to_global(
- owner_fingerprint: String,
- local_position: Vector3,
-) -> Vector3:
- return get_interior_anchor(owner_fingerprint) + local_position
-
-
-func interior_global_to_local(
- owner_fingerprint: String,
- global_position: Vector3,
-) -> Vector3:
- return global_position - get_interior_anchor(owner_fingerprint)
-
-
-func get_floor_position_from_screen(
- owner_fingerprint: String,
- camera: Camera3D,
- screen_position: Vector2,
-) -> Vector3:
- if camera == null or owner_fingerprint.is_empty():
- return Vector3.INF
- var ray_origin: Vector3 = camera.project_ray_origin(screen_position)
- var ray_direction: Vector3 = camera.project_ray_normal(screen_position)
- if absf(ray_direction.y) <= 0.0001:
- return Vector3.INF
- var floor_y: float = get_interior_anchor(owner_fingerprint).y + 0.1
- var distance: float = (floor_y - ray_origin.y) / ray_direction.y
- if distance <= 0.0:
- return Vector3.INF
- return interior_global_to_local(
- owner_fingerprint,
- ray_origin + ray_direction * distance,
- )
-
-
-func find_decor_instance_at_screen(
- owner_fingerprint: String,
- camera: Camera3D,
- screen_position: Vector2,
-) -> String:
- if camera == null or owner_fingerprint.is_empty():
- return ""
- var ray_origin: Vector3 = camera.project_ray_origin(screen_position)
- var ray_end: Vector3 = ray_origin + camera.project_ray_normal(
- screen_position
- ) * 50.0
- var query := PhysicsRayQueryParameters3D.create(ray_origin, ray_end)
- query.collision_mask = DECOR_SELECTION_COLLISION_LAYER
- query.collide_with_areas = false
- var result: Dictionary = camera.get_world_3d().direct_space_state.intersect_ray(
- query
- )
- var collider: Object = result.get("collider")
- if collider is Node and (collider as Node).has_meta(&"decor_instance_id"):
- if str((collider as Node).get_meta(&"decor_owner_fingerprint")) == (
- owner_fingerprint
- ):
- return str((collider as Node).get_meta(&"decor_instance_id"))
- return ""
-
-
-func get_decor_preview_transform(
- owner_fingerprint: String,
- instance_id: String,
-) -> Dictionary:
- var decor: Node3D = _get_decor_presentation(
- owner_fingerprint, instance_id
- )
- if decor == null:
- return {}
- return {
- "position": Vector3(decor.position.x, 0.0, decor.position.z),
- "yaw": decor.rotation.y,
- }
-
-
-func set_decor_preview_transform(
- owner_fingerprint: String,
- instance_id: String,
- position: Vector3,
- yaw_radians: float,
-) -> bool:
- var decor: Node3D = _get_decor_presentation(
- owner_fingerprint, instance_id
- )
- if decor == null or not position.is_finite() or not is_finite(yaw_radians):
- return false
- decor.position = Vector3(position.x, 0.18, position.z)
- decor.rotation.y = yaw_radians
- return true
-
-
-func set_decor_manipulation_active(
- owner_fingerprint: String,
- instance_id: String,
- active: bool,
-) -> bool:
- var decor: Node3D = _get_decor_presentation(
- owner_fingerprint, instance_id
- )
- if decor == null:
- return false
- var body: StaticBody3D = decor.get_node_or_null(
- "BasicCubeCollision"
- ) as StaticBody3D
- if body == null:
- return false
- var key: String = _decor_collision_key(
- owner_fingerprint, instance_id
- )
- if active:
- _decor_under_manipulation[key] = true
- _pending_solid_decor.erase(key)
- body.collision_layer = DECOR_SELECTION_COLLISION_LAYER
- return true
- _decor_under_manipulation.erase(key)
- _queue_decor_world_collision(
- owner_fingerprint,
- instance_id,
- StringName(str(decor.get_meta(&"decor_id", ""))),
- body,
- )
- return true
-
-
-func is_decor_placement_clear_of_players(
- owner_fingerprint: String,
- decor_id: StringName,
- local_position: Vector3,
- yaw_radians: float,
-) -> bool:
- if (
- owner_fingerprint.is_empty()
- or not DecorCatalogType.has_product(decor_id)
- or not local_position.is_finite()
- or not is_finite(yaw_radians)
- ):
- return false
- if _spawn_service == null:
- return true
- var global_center: Vector3 = interior_local_to_global(
- owner_fingerprint, local_position
- )
- var half_size: Vector2 = DecorCatalogType.get_footprint(decor_id) * 0.5
- for peer_id: int in _spawn_service.get_peer_ids():
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- if avatar == null or not is_instance_valid(avatar):
- continue
- var offset := Vector2(
- avatar.global_position.x - global_center.x,
- avatar.global_position.z - global_center.z,
- ).rotated(-yaw_radians)
- var nearest := Vector2(
- clampf(offset.x, -half_size.x, half_size.x),
- clampf(offset.y, -half_size.y, half_size.y),
- )
- if offset.distance_squared_to(nearest) < (
- PLAYER_DECOR_CLEARANCE_RADIUS
- * PLAYER_DECOR_CLEARANCE_RADIUS
- ):
- return false
- return true
-
-
-func _get_decor_presentation(
- owner_fingerprint: String,
- instance_id: String,
-) -> Node3D:
- if owner_fingerprint.is_empty() or instance_id.is_empty():
- return null
- var presentation: Dictionary = _presentations_by_fingerprint.get(
- owner_fingerprint, {}
- )
- var interior: Node3D = presentation.get("interior")
- if not is_instance_valid(interior):
- return null
- return interior.get_node_or_null(
- "Decor/%s" % instance_id
- ) as Node3D
-
-
-func find_nearby_exterior_owner(avatar: Player) -> String:
- if avatar == null:
- return ""
- var closest: String = ""
- var closest_distance: float = EXTERIOR_INTERACTION_DISTANCE
- for fingerprint: String in _presentations_by_fingerprint:
- var transform: Transform3D = get_exterior_transform(fingerprint)
- var distance: float = avatar.global_position.distance_to(
- transform * EXTERIOR_DOOR_LOCAL
- )
- if distance <= closest_distance:
- closest = fingerprint
- closest_distance = distance
- return closest
-
-
-func set_local_space(space_id: StringName) -> void:
- _local_space = space_id
- _refresh_local_visibility()
-
-
-func get_manifest(owner_fingerprint: String) -> Dictionary:
- var presentation: Dictionary = _presentations_by_fingerprint.get(
- owner_fingerprint, {}
- )
- return (presentation.get("home", {}) as Dictionary).duplicate(true)
-
-
-func _process(_delta: float) -> void:
- _refresh_pending_decor_collisions()
- if not _dedicated_runtime:
- _refresh_nearest_wall()
-
-
-func _queue_decor_world_collision(
- owner_fingerprint: String,
- instance_id: String,
- decor_id: StringName,
- body: StaticBody3D,
-) -> void:
- if (
- owner_fingerprint.is_empty()
- or instance_id.is_empty()
- or not DecorCatalogType.has_product(decor_id)
- or body == null
- ):
- return
- var key: String = _decor_collision_key(
- owner_fingerprint, instance_id
- )
- body.collision_layer = DECOR_SELECTION_COLLISION_LAYER
- _pending_solid_decor[key] = {
- "owner_fingerprint": owner_fingerprint,
- "instance_id": instance_id,
- "decor_id": decor_id,
- "body": body,
- }
-
-
-func _refresh_pending_decor_collisions() -> void:
- if _pending_solid_decor.is_empty():
- return
- var keys: Array[String] = []
- keys.assign(_pending_solid_decor.keys())
- for key: String in keys:
- if _decor_under_manipulation.has(key):
- continue
- var pending: Dictionary = _pending_solid_decor.get(key, {})
- var body: StaticBody3D = pending.get("body") as StaticBody3D
- var owner_fingerprint: String = str(
- pending.get("owner_fingerprint", "")
- )
- var instance_id: String = str(pending.get("instance_id", ""))
- var decor_id := StringName(str(pending.get("decor_id", "")))
- var decor: Node3D = _get_decor_presentation(
- owner_fingerprint, instance_id
- )
- if body == null or decor == null or not is_instance_valid(body):
- _pending_solid_decor.erase(key)
- continue
- if not is_decor_placement_clear_of_players(
- owner_fingerprint,
- decor_id,
- Vector3(decor.position.x, 0.0, decor.position.z),
- decor.rotation.y,
- ):
- continue
- body.collision_layer = (
- WORLD_GEOMETRY_COLLISION_LAYER
- | DECOR_SELECTION_COLLISION_LAYER
- )
- _pending_solid_decor.erase(key)
-
-
-func _clear_decor_collision_tracking(owner_fingerprint: String) -> void:
- var prefix: String = "%s/" % owner_fingerprint
- for key: String in _decor_under_manipulation.keys():
- if key.begins_with(prefix):
- _decor_under_manipulation.erase(key)
- for key: String in _pending_solid_decor.keys():
- if key.begins_with(prefix):
- _pending_solid_decor.erase(key)
-
-
-static func _decor_collision_key(
- owner_fingerprint: String,
- instance_id: String,
-) -> String:
- return "%s/%s" % [owner_fingerprint, instance_id]
-
-
-func _ensure_roots() -> void:
- if not is_instance_valid(_exteriors_root):
- _exteriors_root = Node3D.new()
- _exteriors_root.name = "RVExteriors"
- add_child(_exteriors_root)
- if not is_instance_valid(_interiors_root):
- _interiors_root = Node3D.new()
- _interiors_root.name = "RVInteriors"
- add_child(_interiors_root)
-
-
-func _build_exterior(
- fingerprint: String,
- world_transform: Transform3D,
-) -> Node3D:
- var root := Node3D.new()
- root.name = "RV_%s" % fingerprint.left(8)
- _exteriors_root.add_child(root)
- root.global_transform = world_transform
- if not _dedicated_runtime:
- var motorcoach: Node3D = STARTER_MOTORCOACH_SCENE.instantiate() as Node3D
- if motorcoach != null:
- motorcoach.name = "StarterMotorcoach"
- root.add_child(motorcoach)
- else:
- push_error("The starter motorcoach exterior could not be instantiated.")
- _add_box_collision(
- root,
- "Body",
- EXTERIOR_BODY_SIZE,
- Vector3(0.0, 1.35, 0.0),
- )
- root.visible = not _dedicated_runtime
- return root
-
-
-func _build_interior(
- fingerprint: String,
- interior_index: int,
- home_data: Dictionary,
-) -> Node3D:
- var root := Node3D.new()
- root.name = "RVInterior_%s" % fingerprint.left(8)
- root.position = INTERIOR_ORIGIN + Vector3(
- float(interior_index) * INTERIOR_SPACING, 0.0, 0.0
- )
- var half_width: float = INTERIOR_FLOOR_SIZE.x * 0.5
- var half_depth: float = INTERIOR_FLOOR_SIZE.z * 0.5
- var wall_center_y: float = INTERIOR_WALL_HEIGHT * 0.5
- _interiors_root.add_child(root)
- _add_box(
- root, "Floor", INTERIOR_FLOOR_SIZE, Vector3.ZERO,
- Color("a6754e"), true,
- )
- _add_wall(
- root, "WallNorth",
- Vector3(INTERIOR_FLOOR_SIZE.x, INTERIOR_WALL_HEIGHT, 0.18),
- Vector3(0.0, wall_center_y, -half_depth),
- )
- _add_wall(
- root, "WallSouth",
- Vector3(INTERIOR_FLOOR_SIZE.x, INTERIOR_WALL_HEIGHT, 0.18),
- Vector3(0.0, wall_center_y, half_depth),
- )
- _add_wall(
- root, "WallWest",
- Vector3(0.18, INTERIOR_WALL_HEIGHT, INTERIOR_FLOOR_SIZE.z),
- Vector3(-half_width, wall_center_y, 0.0),
- )
- _add_wall(
- root, "WallEast",
- Vector3(0.18, INTERIOR_WALL_HEIGHT, INTERIOR_FLOOR_SIZE.z),
- Vector3(half_width, wall_center_y, 0.0),
- )
- for x_position: float in [-1.75, 1.75]:
- _add_box(
- root, "WallNorthWindow%s" % ("Left" if x_position < 0.0 else "Right"),
- Vector3(1.65, 0.95, 0.04),
- Vector3(x_position, 1.55, -half_depth + 0.11),
- Color("153640"), false,
- )
- _add_box(
- root, "WallSouthWindow%s" % ("Left" if x_position < 0.0 else "Right"),
- Vector3(1.65, 0.95, 0.04),
- Vector3(x_position, 1.55, half_depth - 0.11),
- Color("153640"), false,
- )
- _add_box(
- root, "WallEastWindow", Vector3(0.04, 0.95, 1.55),
- Vector3(half_width - 0.11, 1.55, 0.0), Color("153640"), false,
- )
- _add_box(
- root, "WallWestDoor", Vector3(0.04, 1.9, 1.25),
- Vector3(-half_width + 0.11, 1.0, 0.0), Color("2897a8"), false,
- )
- var decor := Node3D.new()
- decor.name = "Decor"
- root.add_child(decor)
- _rebuild_decor(decor, home_data, fingerprint)
- root.visible = false
- if _dedicated_runtime:
- _set_geometry_visible(root, false)
- return root
-
-
-func _rebuild_decor(
- parent: Node3D,
- home_data: Dictionary,
- owner_fingerprint: String,
-) -> void:
- for value: Variant in home_data.get("placements", []):
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var placement: Dictionary = value
- var decor_id := StringName(str(placement["decor_id"]))
- if decor_id != DecorCatalogType.BASIC_CUBE_ID:
- continue
- var position_values: Array = placement["position"]
- var placement_root := Node3D.new()
- placement_root.name = str(placement["instance_id"])
- placement_root.position = Vector3(
- float(position_values[0]), 0.18, float(position_values[2])
- )
- placement_root.rotation.y = float(placement["yaw"])
- placement_root.set_meta(
- &"decor_instance_id", str(placement["instance_id"])
- )
- placement_root.set_meta(
- &"decor_owner_fingerprint", owner_fingerprint
- )
- placement_root.set_meta(&"decor_id", String(decor_id))
- parent.add_child(placement_root)
- var body := _add_box(
- placement_root, "BasicCube", BASIC_CUBE_SIZE,
- Vector3(0.0, BASIC_CUBE_SIZE.y * 0.5, 0.0),
- Color("b77a55"), true,
- )
- if body != null:
- body.collision_layer = DECOR_SELECTION_COLLISION_LAYER
- body.set_meta(
- &"decor_instance_id", str(placement["instance_id"])
- )
- body.set_meta(
- &"decor_owner_fingerprint", owner_fingerprint
- )
- body.set_meta(&"decor_id", String(decor_id))
- _queue_decor_world_collision(
- owner_fingerprint,
- str(placement["instance_id"]),
- decor_id,
- body,
- )
- _add_box(
- placement_root,
- "BasicCubeFront",
- Vector3(BASIC_CUBE_SIZE.x * 0.82, BASIC_CUBE_SIZE.y * 0.68, 0.025),
- Vector3(
- 0.0,
- BASIC_CUBE_SIZE.y * 0.5,
- -BASIC_CUBE_SIZE.z * 0.5 - 0.013,
- ),
- Color("2897a8"),
- false,
- )
-
-
-func _add_wall(
- parent: Node3D,
- node_name: String,
- size: Vector3,
- position: Vector3,
-) -> void:
- _add_box(parent, node_name, size, position, Color("d6caaa"), true)
-
-
-func _add_box(
- parent: Node3D,
- node_name: String,
- size: Vector3,
- position: Vector3,
- color: Color,
- with_collision: bool,
- invert_faces: bool = false,
-) -> StaticBody3D:
- var mesh_instance := MeshInstance3D.new()
- mesh_instance.name = node_name
- var mesh := BoxMesh.new()
- mesh.size = size
- mesh.material = _material(color)
- if invert_faces:
- mesh.flip_faces = true
- mesh_instance.mesh = mesh
- mesh_instance.position = position
- parent.add_child(mesh_instance)
- if with_collision:
- return _add_box_collision(parent, node_name, size, position)
- return null
-
-
-func _add_box_collision(
- parent: Node3D,
- node_name: String,
- size: Vector3,
- position: Vector3,
-) -> StaticBody3D:
- var body := StaticBody3D.new()
- body.name = "%sCollision" % node_name
- body.position = position
- var collision := CollisionShape3D.new()
- var shape := BoxShape3D.new()
- shape.size = size
- collision.shape = shape
- body.add_child(collision)
- parent.add_child(body)
- return body
-
-
-func _spawn_decor_poof(
- owner_fingerprint: String,
- instance_id: String,
-) -> void:
- var presentation: Dictionary = _presentations_by_fingerprint.get(
- owner_fingerprint, {}
- )
- var interior: Node3D = presentation.get("interior")
- if not is_instance_valid(interior):
- return
- var placement_root: Node3D = interior.get_node_or_null(
- "Decor/%s" % instance_id
- )
- if placement_root == null:
- return
- var poof := CPUParticles3D.new()
- poof.name = "DecorPoof"
- poof.amount = 12
- poof.lifetime = 0.45
- poof.one_shot = true
- poof.explosiveness = 0.9
- poof.direction = Vector3.UP
- poof.spread = 80.0
- poof.initial_velocity_min = 0.7
- poof.initial_velocity_max = 1.5
- poof.scale_amount_min = 0.08
- poof.scale_amount_max = 0.16
- var particle_mesh := SphereMesh.new()
- particle_mesh.radius = 0.06
- particle_mesh.height = 0.12
- particle_mesh.material = _material(Color("d9c5a3"))
- poof.mesh = particle_mesh
- placement_root.add_child(poof)
- poof.emitting = true
- get_tree().create_timer(poof.lifetime + 0.1).timeout.connect(
- poof.queue_free
- )
-
-
-func _material(color: Color) -> StandardMaterial3D:
- var material := StandardMaterial3D.new()
- material.albedo_color = color
- material.roughness = 1.0
- material.shading_mode = BaseMaterial3D.SHADING_MODE_PER_VERTEX
- return material
-
-
-func _free_presentation_nodes(presentation: Dictionary) -> void:
- for key: String in ["exterior", "interior"]:
- var node: Node = presentation.get(key)
- if is_instance_valid(node):
- node.queue_free()
-
-
-func _refresh_local_visibility() -> void:
- var active_fingerprint: String = ""
- if String(_local_space).begins_with("rv:"):
- active_fingerprint = String(_local_space).trim_prefix("rv:")
- if _world != null:
- _world.set_local_home_interior_presentation_active(
- not active_fingerprint.is_empty()
- )
- for fingerprint: String in _presentations_by_fingerprint:
- var presentation: Dictionary = _presentations_by_fingerprint[fingerprint]
- var interior: Node3D = presentation.get("interior")
- if is_instance_valid(interior):
- interior.visible = (
- not _dedicated_runtime and fingerprint == active_fingerprint
- )
- var exterior: Node3D = presentation.get("exterior")
- if is_instance_valid(exterior):
- exterior.visible = (
- not _dedicated_runtime and active_fingerprint.is_empty()
- )
- var local_player: Player = (
- _spawn_service.get_local_player()
- if _spawn_service != null else null
- )
- if local_player == null:
- return
- var excluded_wall_rids: Array[RID] = []
- if not active_fingerprint.is_empty():
- var active: Dictionary = _presentations_by_fingerprint.get(
- active_fingerprint, {}
- )
- var active_interior: Node3D = active.get("interior")
- if is_instance_valid(active_interior):
- for wall_name: String in [
- "WallNorth", "WallSouth", "WallWest", "WallEast",
- ]:
- var body: StaticBody3D = active_interior.get_node_or_null(
- "%sCollision" % wall_name
- )
- if body != null:
- excluded_wall_rids.append(body.get_rid())
- local_player.set_rv_interior_camera_mode(
- not active_fingerprint.is_empty(), excluded_wall_rids
- )
-
-
-func _refresh_nearest_wall() -> void:
- if not String(_local_space).begins_with("rv:") or _spawn_service == null:
- return
- var fingerprint: String = String(_local_space).trim_prefix("rv:")
- var presentation: Dictionary = _presentations_by_fingerprint.get(
- fingerprint, {}
- )
- var interior: Node3D = presentation.get("interior")
- var player: Player = _spawn_service.get_local_player()
- if not is_instance_valid(interior) or player == null:
- return
- var camera: Camera3D = player.get_active_gameplay_camera()
- if camera == null:
- return
- var local_camera: Vector3 = interior.to_local(camera.global_position)
- var local_view_direction: Vector3 = (
- interior.global_basis.inverse() * -camera.global_basis.z
- ).normalized()
- var horizontal_view_weight: float = absf(local_view_direction.x)
- var depth_view_weight: float = absf(local_view_direction.z)
- var hidden_walls: Dictionary[String, bool] = {}
- var outside_horizontal_wall: bool = (
- absf(local_camera.x) >= INTERIOR_FLOOR_SIZE.x * 0.5 - 0.05
- )
- var outside_depth_wall: bool = (
- absf(local_camera.z) >= INTERIOR_FLOOR_SIZE.z * 0.5 - 0.05
- )
- # At a diagonal, both near walls occupy the view. Use the camera's viewing
- # direction to hide both instead of choosing only the dominant position
- # axis and leaving the adjacent wall across the screen.
- if (
- outside_horizontal_wall
- and horizontal_view_weight >= depth_view_weight * 0.35
- ):
- hidden_walls[
- "WallEast" if local_camera.x >= 0.0 else "WallWest"
- ] = true
- if (
- outside_depth_wall
- and depth_view_weight >= horizontal_view_weight * 0.35
- ):
- hidden_walls[
- "WallSouth" if local_camera.z >= 0.0 else "WallNorth"
- ] = true
- if hidden_walls.is_empty():
- var fallback_wall: String = (
- "WallEast" if local_camera.x >= 0.0 else "WallWest"
- )
- if depth_view_weight > horizontal_view_weight:
- fallback_wall = (
- "WallSouth" if local_camera.z >= 0.0 else "WallNorth"
- )
- hidden_walls[fallback_wall] = true
- for wall_name: String in [
- "WallNorth", "WallSouth", "WallWest", "WallEast",
- ]:
- var wall: MeshInstance3D = interior.get_node_or_null(wall_name)
- if wall != null:
- wall.visible = not hidden_walls.has(wall_name)
- for child: Node in interior.get_children():
- if (
- child is GeometryInstance3D
- and String(child.name).begins_with(wall_name)
- ):
- (child as GeometryInstance3D).visible = (
- not hidden_walls.has(wall_name)
- )
-
-
-func _too_close_to_reserved(
- position: Vector3,
- reserved_transforms: Array[Transform3D],
-) -> bool:
- for transform: Transform3D in reserved_transforms:
- if position.distance_to(transform.origin) < MIN_EXTERIOR_SEPARATION:
- return true
- return false
-
-
-func _set_geometry_visible(root: Node, visible: bool) -> void:
- for node: Node in root.find_children("*", "GeometryInstance3D", true, false):
- (node as GeometryInstance3D).visible = visible
diff --git a/homes/home_world_service.gd.uid b/homes/home_world_service.gd.uid
deleted file mode 100644
index 5505d06..0000000
--- a/homes/home_world_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cy2kjoxhwxujd
diff --git a/homes/player_home_state.gd b/homes/player_home_state.gd
deleted file mode 100644
index decd34a..0000000
--- a/homes/player_home_state.gd
+++ /dev/null
@@ -1,786 +0,0 @@
-class_name PlayerHomeState
-extends Node
-
-signal changed
-
-const DecorCatalogType = preload("res://homes/decor_catalog.gd")
-
-const HOME_SCHEMA_VERSION: int = 1
-const STARTER_TIER: int = 0
-const STARTER_EXTERIOR_ID: StringName = &"starter_motorcoach"
-const MAX_TIER: int = 8
-const MAX_PLACEMENTS: int = 128
-const MAX_INSTANCE_ID_LENGTH: int = 96
-const MAX_REVISION: int = 2147483647
-const GRID_SIZE: float = 0.5
-const FREE_PLACEMENT_SEARCH_STEP: float = 0.1
-const FREE_PLACEMENT_SEARCH_ANGLES: int = 24
-const NEAREST_PLACEMENT_SEARCH_DISTANCE: float = 2.0
-# The starter RV is intentionally cramped. Later RV tiers can replace these
-# bounds with larger layouts when tiered interiors are introduced.
-# The starter interior is 8.0 m by 4.5 m with 0.18 m walls. These bounds
-# follow the walls' inner faces, with a small visual gap to avoid z-fighting.
-const INTERIOR_HALF_EXTENTS := Vector2(3.91, 2.16)
-const PLACEMENT_EDGE_PADDING: float = 0.02
-
-enum Privacy {
- OPEN,
- CLOSED,
-}
-
-enum PlacementMode {
- GRID,
- FREE,
-}
-
-var _tier: int = STARTER_TIER
-var _exterior_id: StringName = STARTER_EXTERIOR_ID
-var _privacy: Privacy = Privacy.OPEN
-var _owned_decor: Dictionary[StringName, int] = {}
-var _placements: Array[Dictionary] = []
-var _revision: int = 0
-var _next_instance_sequence: int = 1
-
-
-func get_tier() -> int:
- return _tier
-
-
-func get_exterior_id() -> StringName:
- return _exterior_id
-
-
-func get_privacy() -> Privacy:
- return _privacy
-
-
-func get_revision() -> int:
- return _revision
-
-
-func get_owned_count(decor_id: StringName) -> int:
- return int(_owned_decor.get(decor_id, 0))
-
-
-func get_placed_count(decor_id: StringName) -> int:
- var count: int = 0
- for placement: Dictionary in _placements:
- if StringName(str(placement.get("decor_id", ""))) == decor_id:
- count += 1
- return count
-
-
-func get_available_count(decor_id: StringName) -> int:
- return maxi(
- get_owned_count(decor_id) - get_placed_count(decor_id),
- 0,
- )
-
-
-func get_owned_decor() -> Dictionary[StringName, int]:
- return _owned_decor.duplicate()
-
-
-func get_placements() -> Array[Dictionary]:
- var result: Array[Dictionary] = []
- for placement: Dictionary in _placements:
- result.append(placement.duplicate(true))
- return result
-
-
-func get_placement(instance_id: String) -> Dictionary:
- var index: int = _placement_index(instance_id)
- return _placements[index].duplicate(true) if index >= 0 else {}
-
-
-func can_place_decor(
- decor_id: StringName,
- position: Vector3,
- yaw_radians: float,
- mode: PlacementMode,
-) -> bool:
- if (
- get_available_count(decor_id) <= 0
- or _placements.size() >= MAX_PLACEMENTS
- or not _valid_mode(mode)
- ):
- return false
- return not get_nearest_valid_placement_transform(
- decor_id,
- position,
- yaw_radians,
- mode,
- "",
- ).is_empty()
-
-
-func set_privacy(value: Privacy) -> bool:
- if value < Privacy.OPEN or value > Privacy.CLOSED:
- return false
- if _privacy == value:
- return true
- _privacy = value
- _touch()
- return true
-
-
-func add_owned_decor(
- decor_id: StringName,
- quantity: int = 1,
-) -> bool:
- if (
- not DecorCatalogType.has_product(decor_id)
- or quantity <= 0
- ):
- return false
- var current: int = get_owned_count(decor_id)
- if current + quantity > DecorCatalogType.MAX_OWNED_PER_PRODUCT:
- return false
- _owned_decor[decor_id] = current + quantity
- _touch()
- return true
-
-
-func remove_owned_decor(
- decor_id: StringName,
- quantity: int = 1,
-) -> bool:
- if quantity <= 0:
- return false
- var current: int = get_owned_count(decor_id)
- if current - quantity < get_placed_count(decor_id):
- return false
- var remaining: int = current - quantity
- if remaining < 0:
- return false
- if remaining == 0:
- _owned_decor.erase(decor_id)
- else:
- _owned_decor[decor_id] = remaining
- _touch()
- return true
-
-
-func place_decor(
- decor_id: StringName,
- position: Vector3,
- yaw_radians: float,
- mode: PlacementMode,
-) -> String:
- if (
- get_available_count(decor_id) <= 0
- or _placements.size() >= MAX_PLACEMENTS
- or not _valid_mode(mode)
- ):
- return ""
- var normalized := get_nearest_valid_placement_transform(
- decor_id,
- position,
- yaw_radians,
- mode,
- "",
- )
- if normalized.is_empty():
- return ""
- var instance_id := _make_instance_id()
- var placement: Dictionary = {
- "instance_id": instance_id,
- "decor_id": String(decor_id),
- "mode": int(mode),
- "position": _vector3_to_array(normalized["position"]),
- "yaw": float(normalized["yaw"]),
- }
- _placements.append(placement)
- _touch()
- return instance_id
-
-
-func update_placement(
- instance_id: String,
- position: Vector3,
- yaw_radians: float,
- mode: PlacementMode,
-) -> bool:
- var index: int = _placement_index(instance_id)
- var normalized: Dictionary = get_valid_update_transform(
- instance_id, position, yaw_radians, mode
- )
- if index < 0 or normalized.is_empty():
- return false
- var placement: Dictionary = _placements[index].duplicate(true)
- placement["mode"] = int(mode)
- placement["position"] = _vector3_to_array(normalized["position"])
- placement["yaw"] = float(normalized["yaw"])
- _placements[index] = placement
- _touch()
- return true
-
-
-func get_valid_update_transform(
- instance_id: String,
- position: Vector3,
- yaw_radians: float,
- mode: PlacementMode,
- candidate_validator: Callable = Callable(),
-) -> Dictionary:
- var index: int = _placement_index(instance_id)
- if index < 0 or not _valid_mode(mode):
- return {}
- var decor_id := StringName(
- str(_placements[index].get("decor_id", ""))
- )
- return get_nearest_valid_placement_transform(
- decor_id,
- position,
- yaw_radians,
- mode,
- instance_id,
- candidate_validator,
- )
-
-
-func get_nearest_valid_placement_transform(
- decor_id: StringName,
- position: Vector3,
- yaw_radians: float,
- mode: PlacementMode,
- ignored_instance_id: String = "",
- candidate_validator: Callable = Callable(),
-) -> Dictionary:
- var normalized: Dictionary = normalized_placement_transform(
- decor_id, position, yaw_radians, mode
- )
- if normalized.is_empty():
- return {}
- if (
- not _overlaps_existing(
- decor_id, normalized, ignored_instance_id
- )
- and _candidate_is_accepted(normalized, candidate_validator)
- ):
- return normalized
- return (
- _find_nearest_grid_transform(
- decor_id,
- normalized,
- ignored_instance_id,
- candidate_validator,
- )
- if mode == PlacementMode.GRID
- else _find_nearest_free_transform(
- decor_id,
- normalized,
- ignored_instance_id,
- candidate_validator,
- )
- )
-
-
-func remove_placement(instance_id: String) -> bool:
- var index: int = _placement_index(instance_id)
- if index < 0:
- return false
- _placements.remove_at(index)
- _touch()
- return true
-
-
-func reset_to_defaults() -> void:
- _tier = STARTER_TIER
- _exterior_id = STARTER_EXTERIOR_ID
- _privacy = Privacy.OPEN
- _owned_decor.clear()
- _placements.clear()
- _revision = 0
- _next_instance_sequence = 1
- changed.emit()
-
-
-func to_save_data() -> Dictionary:
- var owned: Dictionary = {}
- var ids: Array[StringName] = []
- ids.assign(_owned_decor.keys())
- ids.sort_custom(func(first: StringName, second: StringName) -> bool:
- return String(first) < String(second)
- )
- for decor_id: StringName in ids:
- owned[String(decor_id)] = _owned_decor[decor_id]
- return {
- "schema_version": HOME_SCHEMA_VERSION,
- "tier": _tier,
- "exterior_id": String(_exterior_id),
- "privacy": int(_privacy),
- "owned_decor": owned,
- "placements": get_placements(),
- "revision": _revision,
- "next_instance_sequence": _next_instance_sequence,
- }
-
-
-func restore_from_save_data(data: Dictionary) -> bool:
- var sanitized: Dictionary = sanitize_save_data(data)
- if sanitized.is_empty():
- return false
- _tier = int(sanitized["tier"])
- _exterior_id = StringName(str(sanitized["exterior_id"]))
- _privacy = int(sanitized["privacy"])
- _owned_decor.clear()
- for key: Variant in (sanitized["owned_decor"] as Dictionary):
- _owned_decor[StringName(str(key))] = int(
- (sanitized["owned_decor"] as Dictionary)[key]
- )
- _placements.clear()
- for value: Variant in sanitized["placements"] as Array:
- _placements.append((value as Dictionary).duplicate(true))
- _revision = int(sanitized["revision"])
- _next_instance_sequence = int(sanitized["next_instance_sequence"])
- changed.emit()
- return true
-
-
-static func default_save_data() -> Dictionary:
- return {
- "schema_version": HOME_SCHEMA_VERSION,
- "tier": STARTER_TIER,
- "exterior_id": String(STARTER_EXTERIOR_ID),
- "privacy": int(Privacy.OPEN),
- "owned_decor": {},
- "placements": [],
- "revision": 0,
- "next_instance_sequence": 1,
- }
-
-
-static func sanitize_save_data(data: Dictionary) -> Dictionary:
- if (
- not _is_integer_value(data.get("schema_version"))
- or int(data.get("schema_version", -1)) != HOME_SCHEMA_VERSION
- or not _is_integer_value(data.get("tier"))
- or typeof(data.get("exterior_id")) not in [
- TYPE_STRING, TYPE_STRING_NAME,
- ]
- or not _is_integer_value(data.get("privacy"))
- or typeof(data.get("owned_decor")) != TYPE_DICTIONARY
- or typeof(data.get("placements")) != TYPE_ARRAY
- or not _is_integer_value(data.get("revision"))
- or not _is_integer_value(data.get("next_instance_sequence"))
- ):
- return {}
- var tier: int = int(data["tier"])
- var exterior_id: String = str(data["exterior_id"])
- var privacy: int = int(data["privacy"])
- var revision: int = int(data["revision"])
- var next_sequence: int = int(data["next_instance_sequence"])
- if (
- tier < STARTER_TIER
- or tier > MAX_TIER
- or exterior_id.is_empty()
- or exterior_id.length() > 96
- or privacy < Privacy.OPEN
- or privacy > Privacy.CLOSED
- or revision < 0
- or revision > MAX_REVISION
- or next_sequence < 1
- or next_sequence > MAX_REVISION
- ):
- return {}
- var sanitized_owned: Dictionary = {}
- var owned: Dictionary = data["owned_decor"]
- for key: Variant in owned:
- if typeof(key) not in [TYPE_STRING, TYPE_STRING_NAME]:
- return {}
- var decor_id := StringName(str(key))
- var quantity_value: Variant = owned[key]
- if (
- not DecorCatalogType.has_product(decor_id)
- or not _is_integer_value(quantity_value)
- or int(quantity_value) <= 0
- or int(quantity_value) > DecorCatalogType.MAX_OWNED_PER_PRODUCT
- ):
- return {}
- sanitized_owned[String(decor_id)] = int(quantity_value)
- var values: Array = data["placements"]
- if values.size() > MAX_PLACEMENTS:
- return {}
- var sanitized_placements: Array[Dictionary] = []
- var seen_instance_ids: Dictionary[String, bool] = {}
- var placed_counts: Dictionary[StringName, int] = {}
- for value: Variant in values:
- if typeof(value) != TYPE_DICTIONARY:
- return {}
- var sanitized: Dictionary = sanitize_placement(value)
- if sanitized.is_empty():
- return {}
- var instance_id: String = str(sanitized["instance_id"])
- var decor_id := StringName(str(sanitized["decor_id"]))
- if seen_instance_ids.has(instance_id):
- return {}
- seen_instance_ids[instance_id] = true
- placed_counts[decor_id] = int(
- placed_counts.get(decor_id, 0)
- ) + 1
- if (
- int(placed_counts[decor_id])
- > int(sanitized_owned.get(String(decor_id), 0))
- ):
- return {}
- for previous: Dictionary in sanitized_placements:
- if placements_overlap(previous, sanitized):
- return {}
- sanitized_placements.append(sanitized)
- return {
- "schema_version": HOME_SCHEMA_VERSION,
- "tier": tier,
- "exterior_id": exterior_id,
- "privacy": privacy,
- "owned_decor": sanitized_owned,
- "placements": sanitized_placements,
- "revision": revision,
- "next_instance_sequence": next_sequence,
- }
-
-
-static func sanitize_placement(value: Dictionary) -> Dictionary:
- for key: String in [
- "instance_id", "decor_id", "mode", "position", "yaw",
- ]:
- if not value.has(key):
- return {}
- if (
- typeof(value["instance_id"]) != TYPE_STRING
- or typeof(value["decor_id"]) not in [
- TYPE_STRING, TYPE_STRING_NAME,
- ]
- or not _is_integer_value(value["mode"])
- or typeof(value["position"]) != TYPE_ARRAY
- or typeof(value["yaw"]) not in [TYPE_FLOAT, TYPE_INT]
- ):
- return {}
- var instance_id: String = value["instance_id"]
- var decor_id := StringName(str(value["decor_id"]))
- var mode: int = int(value["mode"])
- var position_values: Array = value["position"]
- var yaw: float = float(value["yaw"])
- if (
- instance_id.is_empty()
- or instance_id.length() > MAX_INSTANCE_ID_LENGTH
- or not DecorCatalogType.has_product(decor_id)
- or not _valid_mode(mode)
- or position_values.size() != 3
- or not is_finite(yaw)
- ):
- return {}
- for component: Variant in position_values:
- if typeof(component) not in [TYPE_FLOAT, TYPE_INT]:
- return {}
- var position := Vector3(
- float(position_values[0]),
- float(position_values[1]),
- float(position_values[2]),
- )
- var normalized := normalized_placement_transform(
- decor_id,
- position,
- yaw,
- mode as PlacementMode,
- )
- if normalized.is_empty():
- return {}
- return {
- "instance_id": instance_id,
- "decor_id": String(decor_id),
- "mode": mode,
- "position": _vector3_to_array(normalized["position"]),
- "yaw": float(normalized["yaw"]),
- }
-
-
-static func _is_integer_value(value: Variant) -> bool:
- if typeof(value) == TYPE_INT:
- return true
- if typeof(value) != TYPE_FLOAT:
- return false
- var number: float = float(value)
- return is_finite(number) and is_equal_approx(number, roundf(number))
-
-
-static func normalized_placement_transform(
- decor_id: StringName,
- position: Vector3,
- yaw_radians: float,
- mode: PlacementMode,
-) -> Dictionary:
- if (
- not DecorCatalogType.has_product(decor_id)
- or not position.is_finite()
- or not is_finite(yaw_radians)
- or not _valid_mode(mode)
- ):
- return {}
- var normalized_position := Vector3(position.x, 0.0, position.z)
- var normalized_yaw: float = wrapf(yaw_radians, -PI, PI)
- if mode == PlacementMode.GRID:
- normalized_position.x = snappedf(normalized_position.x, GRID_SIZE)
- normalized_position.z = snappedf(normalized_position.z, GRID_SIZE)
- normalized_yaw = snappedf(normalized_yaw, PI * 0.5)
- var footprint: Vector2 = DecorCatalogType.get_footprint(decor_id)
- var cosine: float = absf(cos(normalized_yaw))
- var sine: float = absf(sin(normalized_yaw))
- var half_size := Vector2(
- (footprint.x * cosine + footprint.y * sine) * 0.5,
- (footprint.x * sine + footprint.y * cosine) * 0.5,
- )
- var maximum_x: float = (
- INTERIOR_HALF_EXTENTS.x - half_size.x - PLACEMENT_EDGE_PADDING
- )
- var maximum_z: float = (
- INTERIOR_HALF_EXTENTS.y - half_size.y - PLACEMENT_EDGE_PADDING
- )
- if maximum_x < 0.0 or maximum_z < 0.0:
- return {}
- # Clamp after grid snapping so a grid-placed item can still sit flush at a
- # wall even when the wall itself is not on an exact grid interval.
- normalized_position.x = clampf(
- normalized_position.x, -maximum_x, maximum_x
- )
- normalized_position.z = clampf(
- normalized_position.z, -maximum_z, maximum_z
- )
- return {
- "position": normalized_position,
- "yaw": normalized_yaw,
- }
-
-
-func _find_nearest_grid_transform(
- decor_id: StringName,
- origin: Dictionary,
- ignored_instance_id: String,
- candidate_validator: Callable,
-) -> Dictionary:
- var origin_position: Vector3 = origin["position"]
- var yaw: float = float(origin["yaw"])
- var maximum_ring: int = ceili(
- NEAREST_PLACEMENT_SEARCH_DISTANCE / GRID_SIZE
- )
- var seen: Dictionary[String, bool] = {}
- for ring: int in range(1, maximum_ring + 1):
- var nearest: Dictionary = {}
- var nearest_distance_squared: float = INF
- for x_offset: int in range(-ring, ring + 1):
- for z_offset: int in range(-ring, ring + 1):
- if maxi(absi(x_offset), absi(z_offset)) != ring:
- continue
- var candidate: Dictionary = normalized_placement_transform(
- decor_id,
- origin_position + Vector3(
- float(x_offset) * GRID_SIZE,
- 0.0,
- float(z_offset) * GRID_SIZE,
- ),
- yaw,
- PlacementMode.GRID,
- )
- if candidate.is_empty() or not _mark_candidate_unseen(
- candidate, seen
- ):
- continue
- if _overlaps_existing(
- decor_id, candidate, ignored_instance_id
- ) or not _candidate_is_accepted(
- candidate, candidate_validator
- ):
- continue
- var candidate_position: Vector3 = candidate["position"]
- var distance_squared: float = origin_position.distance_squared_to(
- candidate_position
- )
- if distance_squared < nearest_distance_squared:
- nearest = candidate
- nearest_distance_squared = distance_squared
- if not nearest.is_empty():
- return nearest
- return {}
-
-
-func _find_nearest_free_transform(
- decor_id: StringName,
- origin: Dictionary,
- ignored_instance_id: String,
- candidate_validator: Callable,
-) -> Dictionary:
- var origin_position: Vector3 = origin["position"]
- var yaw: float = float(origin["yaw"])
- var maximum_ring: int = ceili(
- NEAREST_PLACEMENT_SEARCH_DISTANCE / FREE_PLACEMENT_SEARCH_STEP
- )
- var seen: Dictionary[String, bool] = {}
- for ring: int in range(1, maximum_ring + 1):
- var radius: float = float(ring) * FREE_PLACEMENT_SEARCH_STEP
- var nearest: Dictionary = {}
- var nearest_distance_squared: float = INF
- for angle_index: int in FREE_PLACEMENT_SEARCH_ANGLES:
- var angle: float = (
- TAU * float(angle_index) / float(FREE_PLACEMENT_SEARCH_ANGLES)
- )
- var candidate: Dictionary = normalized_placement_transform(
- decor_id,
- origin_position + Vector3(
- cos(angle) * radius, 0.0, sin(angle) * radius
- ),
- yaw,
- PlacementMode.FREE,
- )
- if candidate.is_empty() or not _mark_candidate_unseen(
- candidate, seen
- ):
- continue
- if _overlaps_existing(
- decor_id, candidate, ignored_instance_id
- ) or not _candidate_is_accepted(
- candidate, candidate_validator
- ):
- continue
- var candidate_position: Vector3 = candidate["position"]
- var distance_squared: float = origin_position.distance_squared_to(
- candidate_position
- )
- if distance_squared < nearest_distance_squared:
- nearest = candidate
- nearest_distance_squared = distance_squared
- if not nearest.is_empty():
- return nearest
- return {}
-
-
-static func _mark_candidate_unseen(
- candidate: Dictionary,
- seen: Dictionary[String, bool],
-) -> bool:
- var position: Vector3 = candidate["position"]
- var key := "%d:%d" % [
- roundi(position.x * 10000.0),
- roundi(position.z * 10000.0),
- ]
- if seen.has(key):
- return false
- seen[key] = true
- return true
-
-
-static func _candidate_is_accepted(
- candidate: Dictionary,
- validator: Callable,
-) -> bool:
- return not validator.is_valid() or bool(validator.call(candidate))
-
-
-static func placements_overlap(first: Dictionary, second: Dictionary) -> bool:
- var first_id := StringName(str(first.get("decor_id", "")))
- var second_id := StringName(str(second.get("decor_id", "")))
- if (
- not DecorCatalogType.has_product(first_id)
- or not DecorCatalogType.has_product(second_id)
- ):
- return true
- var first_position: Vector3 = _array_to_vector3(first.get("position", []))
- var second_position: Vector3 = _array_to_vector3(second.get("position", []))
- var first_yaw: float = float(first.get("yaw", 0.0))
- var second_yaw: float = float(second.get("yaw", 0.0))
- var first_half: Vector2 = (
- DecorCatalogType.get_footprint(first_id) * 0.5
- )
- var second_half: Vector2 = (
- DecorCatalogType.get_footprint(second_id) * 0.5
- )
- var first_axes: Array[Vector2] = _placement_axes(first_yaw)
- var second_axes: Array[Vector2] = _placement_axes(second_yaw)
- var delta := Vector2(
- second_position.x - first_position.x,
- second_position.z - first_position.z,
- )
- for axis: Vector2 in [
- first_axes[0], first_axes[1], second_axes[0], second_axes[1],
- ]:
- var first_radius: float = (
- absf(axis.dot(first_axes[0])) * first_half.x
- + absf(axis.dot(first_axes[1])) * first_half.y
- )
- var second_radius: float = (
- absf(axis.dot(second_axes[0])) * second_half.x
- + absf(axis.dot(second_axes[1])) * second_half.y
- )
- if absf(delta.dot(axis)) >= (
- first_radius + second_radius - 0.001
- ):
- return false
- return true
-
-
-func _make_instance_id() -> String:
- var result := "decor-%d-%d" % [
- Time.get_ticks_usec(),
- _next_instance_sequence,
- ]
- _next_instance_sequence = mini(
- _next_instance_sequence + 1,
- MAX_REVISION,
- )
- return result
-
-
-func _placement_index(instance_id: String) -> int:
- if instance_id.is_empty():
- return -1
- for index: int in _placements.size():
- if str(_placements[index].get("instance_id", "")) == instance_id:
- return index
- return -1
-
-
-func _overlaps_existing(
- decor_id: StringName,
- normalized: Dictionary,
- ignored_instance_id: String,
-) -> bool:
- var candidate: Dictionary = {
- "decor_id": String(decor_id),
- "position": _vector3_to_array(normalized["position"]),
- "yaw": float(normalized["yaw"]),
- }
- for placement: Dictionary in _placements:
- if str(placement.get("instance_id", "")) == ignored_instance_id:
- continue
- if placements_overlap(candidate, placement):
- return true
- return false
-
-
-func _touch() -> void:
- _revision = mini(_revision + 1, MAX_REVISION)
- changed.emit()
-
-
-static func _valid_mode(value: int) -> bool:
- return value >= PlacementMode.GRID and value <= PlacementMode.FREE
-
-
-static func _vector3_to_array(value: Vector3) -> Array[float]:
- return [value.x, value.y, value.z]
-
-
-static func _array_to_vector3(value: Variant) -> Vector3:
- if typeof(value) != TYPE_ARRAY or (value as Array).size() != 3:
- return Vector3.INF
- var components: Array = value
- return Vector3(
- float(components[0]),
- float(components[1]),
- float(components[2]),
- )
-
-
-static func _placement_axes(yaw: float) -> Array[Vector2]:
- var cosine: float = cos(yaw)
- var sine: float = sin(yaw)
- return [Vector2(cosine, sine), Vector2(-sine, cosine)]
diff --git a/homes/player_home_state.gd.uid b/homes/player_home_state.gd.uid
deleted file mode 100644
index 37836c6..0000000
--- a/homes/player_home_state.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ggfdoak2ixxx
diff --git a/icon.svg b/icon.svg
new file mode 100644
index 0000000..c6bbb7d
--- /dev/null
+++ b/icon.svg
@@ -0,0 +1 @@
+
diff --git a/art/exported/characters/patterns/bengal/body_arms_bengal.png.import b/icon.svg.import
similarity index 69%
rename from art/exported/characters/patterns/bengal/body_arms_bengal.png.import
rename to icon.svg.import
index 8e98690..1b9da13 100644
--- a/art/exported/characters/patterns/bengal/body_arms_bengal.png.import
+++ b/icon.svg.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://b2e0uxfn187gu"
-path="res://.godot/imported/body_arms_bengal.png-350ed15f218f0efa6ae7573ad27d3c65.ctex"
+uid="uid://cxpiqjwgff0xr"
+path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://art/exported/characters/patterns/bengal/body_arms_bengal.png"
-dest_files=["res://.godot/imported/body_arms_bengal.png-350ed15f218f0efa6ae7573ad27d3c65.ctex"]
+source_file="res://icon.svg"
+dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
@@ -38,3 +38,6 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/inventory/fish_inventory.gd b/inventory/fish_inventory.gd
index 9318457..89e192d 100644
--- a/inventory/fish_inventory.gd
+++ b/inventory/fish_inventory.gd
@@ -8,34 +8,13 @@ signal catches_changed
var _catches: Array[FishCatchType] = []
var _next_catch_sequence: int = 1
-var _reservation_service: PlayerAssetReservationService
-var _inventory_layout: PlayerInventoryLayout
-func set_reservation_service(
- reservation_service: PlayerAssetReservationService,
-) -> void:
- _reservation_service = reservation_service
-
-
-func set_inventory_layout(layout: PlayerInventoryLayout) -> void:
- _inventory_layout = layout
-
-
-func can_accept_catch(catch_id: StringName = StringName()) -> bool:
- return (
- _inventory_layout == null
- or _inventory_layout.can_accept_catch(catch_id)
- )
-
-
-func add_catch(fish_catch: FishCatchType) -> bool:
+func add_catch(fish_catch: FishCatchType) -> void:
if fish_catch == null or not fish_catch.is_valid():
- return false
+ return
if get_catch_by_id(fish_catch.catch_id) != null:
- return false
- if not can_accept_catch(fish_catch.catch_id):
- return false
+ return
if fish_catch.catch_sequence <= 0:
fish_catch.catch_sequence = _next_catch_sequence
_next_catch_sequence = maxi(
@@ -48,7 +27,6 @@ func add_catch(fish_catch: FishCatchType) -> bool:
get_count(fish_catch.fish_id)
)
catches_changed.emit()
- return true
func get_count(fish_id: StringName) -> int:
@@ -123,25 +101,6 @@ func remove_catch_by_id(catch_id: StringName) -> FishCatchType:
func remove_catches_by_ids(
catch_ids: Array[StringName],
-) -> Array[FishCatchType]:
- return _remove_catches_by_ids(catch_ids, "", "")
-
-
-func remove_reserved_catch_for_mail_transfer(
- catch_id: StringName,
- reservation_id: String,
- transfer_id: String,
-) -> FishCatchType:
- var removed := _remove_catches_by_ids(
- [catch_id], reservation_id, transfer_id
- )
- return removed.front() if removed.size() == 1 else null
-
-
-func _remove_catches_by_ids(
- catch_ids: Array[StringName],
- mail_reservation_id: String,
- mail_transfer_id: String,
) -> Array[FishCatchType]:
var removed: Array[FishCatchType] = []
if catch_ids.is_empty():
@@ -156,17 +115,6 @@ func _remove_catches_by_ids(
if fish_catch == null:
removed.clear()
return removed
- if (
- _reservation_service != null
- and _reservation_service.is_fish_reserved(catch_id)
- and not _reservation_service.authorize_mail_fish_removal(
- mail_reservation_id,
- catch_id,
- mail_transfer_id,
- )
- ):
- removed.clear()
- return removed
removed.append(fish_catch)
var remaining: Array[FishCatchType] = []
diff --git a/inventory/player_bag.gd b/inventory/player_bag.gd
index d94845d..1e885ee 100644
--- a/inventory/player_bag.gd
+++ b/inventory/player_bag.gd
@@ -5,85 +5,34 @@ const ItemCatalogType = preload("res://items/item_catalog.gd")
const ItemDataType = preload("res://items/item_data.gd")
const OwnedItemType = preload("res://items/owned_item.gd")
-const DEFAULT_UNLOCKED_BAIT_IDS: Array[StringName] = [&"worms"]
-const MIN_STORAGE_SLOT_COUNT: int = 15
-const MAX_STORAGE_SLOT_INDEX: int = 254
-
-enum StorageGroup {
- NONE,
- EQUIPMENT,
- ITEMS,
- BAIT,
- LURES,
-}
-
signal contents_changed
var _catalog: ItemCatalogType
-var _inventory_layout: PlayerInventoryLayout
var _items: Array[OwnedItemType] = []
-var _unlocked_bait_ids: Array[StringName] = (
- DEFAULT_UNLOCKED_BAIT_IDS.duplicate()
-)
func setup(catalog: ItemCatalogType) -> void:
_catalog = catalog
-func set_inventory_layout(layout: PlayerInventoryLayout) -> void:
- _inventory_layout = layout
-
-
func add_item(item_id: StringName, quantity: int = 1) -> bool:
if not can_add_item(item_id, quantity):
return false
var item: ItemDataType = _resolve_valid_item(item_id)
var existing: OwnedItemType = get_owned_item(item_id)
- if (
- existing == null
- and _inventory_layout != null
- and not _inventory_layout.can_accept_item(item_id)
- ):
- return false
if existing != null:
existing.quantity += quantity
else:
var owned := OwnedItemType.new()
owned.item_id = item_id
owned.quantity = quantity
- owned.storage_slot = _first_available_storage_slot(item, _items)
_items.append(owned)
- if item.is_bait() and not _unlocked_bait_ids.has(item_id):
- _unlocked_bait_ids.append(item_id)
- contents_changed.emit()
- return true
-
-
-func add_item_to_storage(item_id: StringName, quantity: int = 1) -> bool:
- var existing: OwnedItemType = get_owned_item(item_id)
- if existing != null:
- return add_item(item_id, quantity)
- var item: ItemDataType = _resolve_available_item(item_id)
- if (
- item == null
- or quantity <= 0
- or quantity > (item.max_stack if item.stackable else 1)
- or _inventory_layout == null
- or not _inventory_layout.can_accept_item_in_storage(item_id)
- ):
- return false
- var owned := OwnedItemType.new()
- owned.item_id = item_id
- owned.quantity = quantity
- owned.storage_slot = _first_available_storage_slot(item, _items)
- _items.append(owned)
contents_changed.emit()
return true
func can_add_item(item_id: StringName, quantity: int = 1) -> bool:
- var item: ItemDataType = _resolve_available_item(item_id)
+ var item: ItemDataType = _resolve_valid_item(item_id)
if item == null or quantity <= 0:
return false
var existing: OwnedItemType = get_owned_item(item_id)
@@ -92,11 +41,6 @@ func can_add_item(item_id: StringName, quantity: int = 1) -> bool:
item.stackable
and existing.quantity + quantity <= item.max_stack
)
- if (
- _inventory_layout != null
- and not _inventory_layout.can_accept_item(item_id)
- ):
- return false
return quantity <= (item.max_stack if item.stackable else 1)
@@ -141,82 +85,6 @@ func get_all_items() -> Array[OwnedItemType]:
return result
-func get_storage_slot(item_id: StringName) -> int:
- var owned: OwnedItemType = get_owned_item(item_id)
- return owned.storage_slot if owned != null else -1
-
-
-func move_item_to_storage_slot(item_id: StringName, target_slot: int) -> bool:
- if target_slot < 0 or target_slot > MAX_STORAGE_SLOT_INDEX:
- return false
- var owned: OwnedItemType = get_owned_item(item_id)
- var item: ItemDataType = _resolve_valid_item(item_id)
- var group: StorageGroup = _storage_group_for_item(item)
- if owned == null or group == StorageGroup.NONE:
- return false
- if owned.storage_slot == target_slot:
- return true
- var displaced: OwnedItemType
- for candidate: OwnedItemType in _items:
- if candidate == null or candidate == owned:
- continue
- var candidate_item: ItemDataType = _resolve_valid_item(
- candidate.item_id
- )
- if (
- _storage_group_for_item(candidate_item) == group
- and candidate.storage_slot == target_slot
- ):
- displaced = candidate
- break
- var previous_slot: int = owned.storage_slot
- owned.storage_slot = target_slot
- if displaced != null:
- displaced.storage_slot = previous_slot
- contents_changed.emit()
- return true
-
-
-func get_unlocked_bait_ids() -> Array[StringName]:
- return _unlocked_bait_ids.duplicate()
-
-
-func is_bait_unlocked(item_id: StringName) -> bool:
- return _unlocked_bait_ids.has(item_id)
-
-
-func get_unlocked_bait_items() -> Array[OwnedItemType]:
- var result: Array[OwnedItemType] = []
- for item_id: StringName in _unlocked_bait_ids:
- var owned: OwnedItemType = get_owned_item(item_id)
- if owned != null:
- result.append(owned.duplicate_record())
- continue
- var empty_record := OwnedItemType.new()
- empty_record.item_id = item_id
- empty_record.quantity = 0
- result.append(empty_record)
- return result
-
-
-func replace_unlocked_bait_ids(item_ids: Array[StringName]) -> bool:
- var validated: Array[StringName] = DEFAULT_UNLOCKED_BAIT_IDS.duplicate()
- var seen: Dictionary[StringName, bool] = {}
- for item_id: StringName in item_ids:
- if item_id.is_empty() or seen.has(item_id):
- return false
- seen[item_id] = true
- if validated.has(item_id):
- continue
- var item: ItemDataType = _resolve_valid_item(item_id)
- if item == null or not item.is_bait():
- return false
- validated.append(item_id)
- _unlocked_bait_ids = validated
- contents_changed.emit()
- return true
-
-
func replace_all_items(items: Array[OwnedItemType]) -> bool:
var validated: Array[OwnedItemType] = []
var seen: Dictionary[StringName, bool] = {}
@@ -231,91 +99,13 @@ func replace_all_items(items: Array[OwnedItemType]) -> bool:
return false
seen[owned.item_id] = true
validated.append(owned.duplicate_record())
- _normalize_storage_slots(validated)
_items = validated
contents_changed.emit()
return true
-func _normalize_storage_slots(items: Array[OwnedItemType]) -> void:
- var occupied: Dictionary[String, bool] = {}
- for owned: OwnedItemType in items:
- var item: ItemDataType = _resolve_valid_item(owned.item_id)
- var group: StorageGroup = _storage_group_for_item(item)
- if group == StorageGroup.NONE:
- owned.storage_slot = -1
- continue
- var key := _storage_slot_key(group, owned.storage_slot)
- if (
- owned.storage_slot < 0
- or owned.storage_slot > MAX_STORAGE_SLOT_INDEX
- or occupied.has(key)
- ):
- owned.storage_slot = -1
- continue
- occupied[key] = true
- for owned: OwnedItemType in items:
- if owned.storage_slot >= 0:
- continue
- var item: ItemDataType = _resolve_valid_item(owned.item_id)
- var group: StorageGroup = _storage_group_for_item(item)
- if group == StorageGroup.NONE:
- continue
- for slot_index: int in range(MAX_STORAGE_SLOT_INDEX + 1):
- var key := _storage_slot_key(group, slot_index)
- if occupied.has(key):
- continue
- owned.storage_slot = slot_index
- occupied[key] = true
- break
-
-
-func _first_available_storage_slot(
- item: ItemDataType,
- items: Array[OwnedItemType],
-) -> int:
- var group: StorageGroup = _storage_group_for_item(item)
- if group == StorageGroup.NONE:
- return -1
- var occupied: Dictionary[int, bool] = {}
- for owned: OwnedItemType in items:
- var owned_item: ItemDataType = _resolve_valid_item(owned.item_id)
- if (
- _storage_group_for_item(owned_item) == group
- and owned.storage_slot >= 0
- ):
- occupied[owned.storage_slot] = true
- for slot_index: int in range(MAX_STORAGE_SLOT_INDEX + 1):
- if not occupied.has(slot_index):
- return slot_index
- return -1
-
-
-func _storage_group_for_item(item: ItemDataType) -> StorageGroup:
- if item == null:
- return StorageGroup.NONE
- if item.is_bait():
- return StorageGroup.BAIT
- if item.is_lure():
- return StorageGroup.LURES
- return (
- StorageGroup.ITEMS
- if item.category == ItemDataType.Category.CONSUMABLE
- else StorageGroup.EQUIPMENT
- )
-
-
-func _storage_slot_key(group: StorageGroup, slot_index: int) -> String:
- return "%d:%d" % [int(group), slot_index]
-
-
func _resolve_valid_item(item_id: StringName) -> ItemDataType:
if _catalog == null:
return null
var item: ItemDataType = _catalog.get_item_by_id(item_id)
return item if item != null and item.is_valid() else null
-
-
-func _resolve_available_item(item_id: StringName) -> ItemDataType:
- var item: ItemDataType = _resolve_valid_item(item_id)
- return item if item != null and item.active else null
diff --git a/inventory/player_hotbar.gd b/inventory/player_hotbar.gd
index 36068ea..94cbb79 100644
--- a/inventory/player_hotbar.gd
+++ b/inventory/player_hotbar.gd
@@ -5,64 +5,28 @@ const SLOT_COUNT: int = 9
const ItemCatalogType = preload("res://items/item_catalog.gd")
const ItemDataType = preload("res://items/item_data.gd")
const PlayerBagType = preload("res://inventory/player_bag.gd")
-const FishInventoryType = preload("res://inventory/fish_inventory.gd")
-
-enum AssignmentKind {
- EMPTY,
- ITEM,
- FISH,
-}
signal slots_changed
signal selected_slot_changed(slot_index: int, item_id: StringName)
-signal selected_assignment_changed(
- slot_index: int,
- kind: AssignmentKind,
- identity: StringName,
-)
var _bag: PlayerBagType
var _catalog: ItemCatalogType
-var _fish_inventory: FishInventoryType
-var _inventory_layout: PlayerInventoryLayout
var _slots: Array[StringName] = []
-var _fish_slots: Array[StringName] = []
var _selected_slot: int = 0
func _init() -> void:
_slots.resize(SLOT_COUNT)
_slots.fill(StringName())
- _fish_slots.resize(SLOT_COUNT)
- _fish_slots.fill(StringName())
-func setup(
- bag: PlayerBagType,
- catalog: ItemCatalogType,
- fish_inventory: FishInventoryType = null,
- inventory_layout: PlayerInventoryLayout = null,
-) -> void:
+func setup(bag: PlayerBagType, catalog: ItemCatalogType) -> void:
_bag = bag
_catalog = catalog
- _fish_inventory = fish_inventory
- _inventory_layout = inventory_layout
if _bag != null and not _bag.contents_changed.is_connected(
_on_bag_contents_changed
):
_bag.contents_changed.connect(_on_bag_contents_changed)
- if (
- _fish_inventory != null
- and not _fish_inventory.catches_changed.is_connected(
- _on_fish_inventory_changed
- )
- ):
- _fish_inventory.catches_changed.connect(_on_fish_inventory_changed)
- if (
- _inventory_layout != null
- and not _inventory_layout.layout_changed.is_connected(_validate_assignments)
- ):
- _inventory_layout.layout_changed.connect(_validate_assignments)
_validate_assignments()
@@ -71,86 +35,23 @@ func assign_item(slot_index: int, item_id: StringName) -> bool:
return false
if _slots[slot_index] == item_id:
return true
- var source_index: int = _slots.find(item_id)
- var displaced_item: StringName = _slots[slot_index]
- var displaced_fish: StringName = _fish_slots[slot_index]
- if (
- _inventory_layout != null
- and not _inventory_layout.move_entry(
- PlayerInventoryLayout.EntryKind.ITEM,
- item_id,
- PlayerInventoryLayout.InventoryContainer.HOTBAR,
- slot_index,
- )
- ):
- return false
- var selected_slot_was_affected: bool = slot_index == _selected_slot
- if source_index >= 0 and source_index != slot_index:
- _slots[source_index] = displaced_item
- _fish_slots[source_index] = displaced_fish
- selected_slot_was_affected = (
- selected_slot_was_affected or source_index == _selected_slot
- )
+ for index: int in range(SLOT_COUNT):
+ if index != slot_index and _slots[index] == item_id:
+ _slots[index] = StringName()
_slots[slot_index] = item_id
- _fish_slots[slot_index] = StringName()
slots_changed.emit()
- if selected_slot_was_affected:
- _emit_selected_assignment()
- return true
-
-
-func assign_fish(slot_index: int, catch_id: StringName) -> bool:
- if not _is_slot_valid(slot_index) or not _can_assign_fish(catch_id):
- return false
- if _fish_slots[slot_index] == catch_id:
- return true
- var source_index: int = _fish_slots.find(catch_id)
- var displaced_item: StringName = _slots[slot_index]
- var displaced_fish: StringName = _fish_slots[slot_index]
- if (
- _inventory_layout != null
- and not _inventory_layout.move_entry(
- PlayerInventoryLayout.EntryKind.CATCH,
- catch_id,
- PlayerInventoryLayout.InventoryContainer.HOTBAR,
- slot_index,
- )
- ):
- return false
- var selected_slot_was_affected: bool = slot_index == _selected_slot
- if source_index >= 0 and source_index != slot_index:
- _slots[source_index] = displaced_item
- _fish_slots[source_index] = displaced_fish
- selected_slot_was_affected = (
- selected_slot_was_affected or source_index == _selected_slot
- )
- _slots[slot_index] = StringName()
- _fish_slots[slot_index] = catch_id
- slots_changed.emit()
- if selected_slot_was_affected:
- _emit_selected_assignment()
+ if slot_index == _selected_slot:
+ selected_slot_changed.emit(_selected_slot, item_id)
return true
func clear_slot(slot_index: int) -> bool:
- if (
- not _is_slot_valid(slot_index)
- or (
- _slots[slot_index].is_empty()
- and _fish_slots[slot_index].is_empty()
- )
- ):
- return false
- if (
- _inventory_layout != null
- and not _inventory_layout.return_hotbar_entry_to_inventory(slot_index)
- ):
+ if not _is_slot_valid(slot_index) or _slots[slot_index].is_empty():
return false
_slots[slot_index] = StringName()
- _fish_slots[slot_index] = StringName()
slots_changed.emit()
if slot_index == _selected_slot:
- _emit_selected_assignment()
+ selected_slot_changed.emit(_selected_slot, StringName())
return true
@@ -161,92 +62,15 @@ func swap_slots(from_index: int, to_index: int) -> bool:
or from_index == to_index
):
return false
- if _inventory_layout != null:
- var kind: PlayerInventoryLayout.EntryKind
- var identity: StringName
- if not _fish_slots[from_index].is_empty():
- kind = PlayerInventoryLayout.EntryKind.CATCH
- identity = _fish_slots[from_index]
- elif not _slots[from_index].is_empty():
- kind = PlayerInventoryLayout.EntryKind.ITEM
- identity = _slots[from_index]
- else:
- kind = (
- PlayerInventoryLayout.EntryKind.CATCH
- if not _fish_slots[to_index].is_empty()
- else PlayerInventoryLayout.EntryKind.ITEM
- )
- identity = (
- _fish_slots[to_index]
- if not _fish_slots[to_index].is_empty()
- else _slots[to_index]
- )
- var temporary_index: int = from_index
- from_index = to_index
- to_index = temporary_index
- if (
- identity.is_empty()
- or not _inventory_layout.move_entry(
- kind,
- identity,
- PlayerInventoryLayout.InventoryContainer.HOTBAR,
- to_index,
- )
- ):
- return false
var temporary: StringName = _slots[from_index]
_slots[from_index] = _slots[to_index]
_slots[to_index] = temporary
- temporary = _fish_slots[from_index]
- _fish_slots[from_index] = _fish_slots[to_index]
- _fish_slots[to_index] = temporary
slots_changed.emit()
if from_index == _selected_slot or to_index == _selected_slot:
- _emit_selected_assignment()
- return true
-
-
-func move_slot_to_container(
- slot_index: int,
- target_container: int,
- target_slot: int,
-) -> bool:
- if not _is_slot_valid(slot_index) or _inventory_layout == null:
- return false
- var kind: PlayerInventoryLayout.EntryKind
- var identity: StringName
- if not _fish_slots[slot_index].is_empty():
- kind = PlayerInventoryLayout.EntryKind.CATCH
- identity = _fish_slots[slot_index]
- elif not _slots[slot_index].is_empty():
- kind = PlayerInventoryLayout.EntryKind.ITEM
- identity = _slots[slot_index]
- else:
- return false
- if not _inventory_layout.move_entry(
- kind,
- identity,
- target_container,
- target_slot,
- ):
- return false
- _slots[slot_index] = StringName()
- _fish_slots[slot_index] = StringName()
- var replacement := _inventory_layout.get_entry_at(
- PlayerInventoryLayout.InventoryContainer.HOTBAR,
- slot_index,
- )
- if not replacement.is_empty():
- var replacement_identity := StringName(
- str(replacement.get("identity", ""))
+ selected_slot_changed.emit(
+ _selected_slot,
+ _slots[_selected_slot]
)
- if int(replacement.get("kind", -1)) == PlayerInventoryLayout.EntryKind.CATCH:
- _fish_slots[slot_index] = replacement_identity
- else:
- _slots[slot_index] = replacement_identity
- slots_changed.emit()
- if slot_index == _selected_slot:
- _emit_selected_assignment()
return true
@@ -256,7 +80,7 @@ func select_slot(slot_index: int) -> bool:
if _selected_slot == slot_index:
return true
_selected_slot = slot_index
- _emit_selected_assignment()
+ selected_slot_changed.emit(_selected_slot, _slots[_selected_slot])
return true
@@ -276,80 +100,21 @@ func get_selected_item_id() -> StringName:
return _slots[_selected_slot]
-func get_selected_fish_catch_id() -> StringName:
- return _fish_slots[_selected_slot]
-
-
-func get_selected_assignment_kind() -> AssignmentKind:
- return get_assignment_kind(_selected_slot)
-
-
func get_item_id(slot_index: int) -> StringName:
return _slots[slot_index] if _is_slot_valid(slot_index) else StringName()
-func get_fish_catch_id(slot_index: int) -> StringName:
- return (
- _fish_slots[slot_index]
- if _is_slot_valid(slot_index)
- else StringName()
- )
-
-
-func get_assignment_kind(slot_index: int) -> AssignmentKind:
- if not _is_slot_valid(slot_index):
- return AssignmentKind.EMPTY
- if not _fish_slots[slot_index].is_empty():
- return AssignmentKind.FISH
- if not _slots[slot_index].is_empty():
- return AssignmentKind.ITEM
- return AssignmentKind.EMPTY
-
-
func get_slots() -> Array[StringName]:
return _slots.duplicate()
-func get_fish_slots() -> Array[StringName]:
- return _fish_slots.duplicate()
-
-
-func find_item_slot(item_id: StringName) -> int:
- return _slots.find(item_id)
-
-
-func find_first_empty_slot() -> int:
- for slot_index: int in SLOT_COUNT:
- if get_assignment_kind(slot_index) != AssignmentKind.EMPTY:
- continue
- if (
- _inventory_layout == null
- or _inventory_layout.get_key_at(
- PlayerInventoryLayout.InventoryContainer.HOTBAR,
- slot_index,
- ).is_empty()
- ):
- return slot_index
- return -1
-
-
func replace_state(
slots: Array[StringName],
selected_slot: int,
- fish_slots: Array[StringName] = [],
- synchronize_layout: bool = true,
) -> bool:
- if (
- synchronize_layout and _inventory_layout != null
- and not _inventory_layout.return_all_hotbar_entries_to_inventory()
- ):
- return false
var normalized: Array[StringName] = []
normalized.resize(SLOT_COUNT)
normalized.fill(StringName())
- var normalized_fish: Array[StringName] = []
- normalized_fish.resize(SLOT_COUNT)
- normalized_fish.fill(StringName())
var seen_assignments: Dictionary[StringName, bool] = {}
for index: int in range(mini(slots.size(), SLOT_COUNT)):
var item_id: StringName = slots[index]
@@ -358,44 +123,19 @@ func replace_state(
if _can_assign(item_id) and not seen_assignments.has(item_id):
normalized[index] = item_id
seen_assignments[item_id] = true
- var seen_fish: Dictionary[StringName, bool] = {}
- for index: int in range(mini(fish_slots.size(), SLOT_COUNT)):
- if not normalized[index].is_empty():
- continue
- var catch_id: StringName = fish_slots[index]
- if _can_assign_fish(catch_id) and not seen_fish.has(catch_id):
- normalized_fish[index] = catch_id
- seen_fish[catch_id] = true
_slots = normalized
- _fish_slots = normalized_fish
- if _inventory_layout != null and synchronize_layout:
- for index: int in SLOT_COUNT:
- if not _slots[index].is_empty():
- _inventory_layout.move_entry(
- PlayerInventoryLayout.EntryKind.ITEM,
- _slots[index],
- PlayerInventoryLayout.InventoryContainer.HOTBAR,
- index,
- )
- elif not _fish_slots[index].is_empty():
- _inventory_layout.move_entry(
- PlayerInventoryLayout.EntryKind.CATCH,
- _fish_slots[index],
- PlayerInventoryLayout.InventoryContainer.HOTBAR,
- index,
- )
_selected_slot = clampi(selected_slot, 0, SLOT_COUNT - 1)
slots_changed.emit()
- _emit_selected_assignment()
+ selected_slot_changed.emit(
+ _selected_slot,
+ _slots[_selected_slot]
+ )
return true
func has_any_assignment() -> bool:
- for index: int in range(SLOT_COUNT):
- if (
- not _slots[index].is_empty()
- or not _fish_slots[index].is_empty()
- ):
+ for item_id: StringName in _slots:
+ if not item_id.is_empty():
return true
return false
@@ -406,31 +146,7 @@ func _can_assign(item_id: StringName) -> bool:
if _catalog == null:
return false
var item: ItemDataType = _catalog.get_item_by_id(item_id)
- return (
- item != null
- and item.is_available()
- and item.hotbar_allowed
- and (
- _inventory_layout == null
- or _inventory_layout.get_container(
- PlayerInventoryLayout.EntryKind.ITEM, item_id
- ) >= 0
- )
- )
-
-
-func _can_assign_fish(catch_id: StringName) -> bool:
- return (
- not catch_id.is_empty()
- and _fish_inventory != null
- and _fish_inventory.contains_catch_id(catch_id)
- and (
- _inventory_layout == null
- or _inventory_layout.get_container(
- PlayerInventoryLayout.EntryKind.CATCH, catch_id
- ) >= 0
- )
- )
+ return item != null and item.is_valid() and item.hotbar_allowed
func _validate_assignments() -> void:
@@ -439,35 +155,17 @@ func _validate_assignments() -> void:
if not _slots[index].is_empty() and not _can_assign(_slots[index]):
_slots[index] = StringName()
changed = true
- if (
- not _fish_slots[index].is_empty()
- and not _can_assign_fish(_fish_slots[index])
- ):
- _fish_slots[index] = StringName()
- changed = true
if changed:
slots_changed.emit()
- _emit_selected_assignment()
+ selected_slot_changed.emit(
+ _selected_slot,
+ _slots[_selected_slot]
+ )
func _on_bag_contents_changed() -> void:
_validate_assignments()
-func _on_fish_inventory_changed() -> void:
- _validate_assignments()
-
-
-func _emit_selected_assignment() -> void:
- var kind: AssignmentKind = get_selected_assignment_kind()
- var identity: StringName = StringName()
- if kind == AssignmentKind.ITEM:
- identity = _slots[_selected_slot]
- elif kind == AssignmentKind.FISH:
- identity = _fish_slots[_selected_slot]
- selected_slot_changed.emit(_selected_slot, _slots[_selected_slot])
- selected_assignment_changed.emit(_selected_slot, kind, identity)
-
-
func _is_slot_valid(slot_index: int) -> bool:
return slot_index >= 0 and slot_index < SLOT_COUNT
diff --git a/inventory/player_inventory_layout.gd b/inventory/player_inventory_layout.gd
deleted file mode 100644
index bce726e..0000000
--- a/inventory/player_inventory_layout.gd
+++ /dev/null
@@ -1,599 +0,0 @@
-class_name PlayerInventoryLayout
-extends Node
-
-const ItemCatalogType = preload("res://items/item_catalog.gd")
-const ItemDataType = preload("res://items/item_data.gd")
-const PlayerBagType = preload("res://inventory/player_bag.gd")
-const FishInventoryType = preload("res://inventory/fish_inventory.gd")
-const PlayerCoolerCapacityType = preload(
- "res://progression/player_cooler_capacity.gd"
-)
-const PlayerWalletType = preload("res://economy/player_wallet.gd")
-
-const INVENTORY_COLUMNS: int = 9
-const INVENTORY_CAPACITIES: Array[int] = [9, 18, 27, 36]
-const BACKPACK_EXPANSION_COSTS: Array[int] = [1500, 4500, 9000]
-const MAX_BACKPACK_LEVEL: int = 3
-const MAX_INVENTORY_SLOT_COUNT: int = 36
-const MAX_STORAGE_SLOT_COUNT: int = PlayerCoolerCapacityType.MAX_CAPACITY
-const HOTBAR_SLOT_COUNT: int = 9
-
-enum EntryKind {
- ITEM,
- CATCH,
-}
-
-enum InventoryContainer {
- INVENTORY,
- STORAGE,
- HOTBAR,
-}
-
-signal layout_changed
-signal backpack_capacity_changed(level: int, capacity: int)
-
-var _bag: PlayerBagType
-var _fish_inventory: FishInventoryType
-var _item_catalog: ItemCatalogType
-var _storage_capacity: PlayerCoolerCapacityType
-var _placements: Dictionary[String, Dictionary] = {}
-var _prepared_item_placements: Dictionary[String, Dictionary] = {}
-var _reconciling: bool = false
-var _backpack_level: int = 0
-
-
-func setup(
- bag: PlayerBagType,
- fish_inventory: FishInventoryType,
- item_catalog: ItemCatalogType,
- storage_capacity: PlayerCoolerCapacityType,
-) -> void:
- _bag = bag
- _fish_inventory = fish_inventory
- _item_catalog = item_catalog
- _storage_capacity = storage_capacity
- if _bag != null and not _bag.contents_changed.is_connected(_on_contents_changed):
- _bag.contents_changed.connect(_on_contents_changed)
- if (
- _fish_inventory != null
- and not _fish_inventory.catches_changed.is_connected(_on_contents_changed)
- ):
- _fish_inventory.catches_changed.connect(_on_contents_changed)
- if (
- _storage_capacity != null
- and not _storage_capacity.capacity_changed.is_connected(
- _on_storage_capacity_changed
- )
- ):
- _storage_capacity.capacity_changed.connect(_on_storage_capacity_changed)
- _reconcile(true)
-
-
-func can_accept_item(item_id: StringName) -> bool:
- if not is_managed_item(item_id):
- return true
- var key := item_key(item_id)
- return (
- _placements.has(key)
- or _is_prepared_item_placement_available(key)
- or _first_free_slot(InventoryContainer.INVENTORY) >= 0
- )
-
-
-func prepare_new_item_placement(
- item_id: StringName,
- target_container: InventoryContainer,
- target_slot: int,
-) -> bool:
- if not is_managed_item(item_id):
- return false
- var key := item_key(item_id)
- if (
- _placements.has(key)
- or not _is_slot_valid(target_container, target_slot)
- or not _key_at(target_container, target_slot).is_empty()
- or _prepared_slot_is_occupied(target_container, target_slot)
- ):
- return false
- _prepared_item_placements[key] = {
- "container": int(target_container),
- "slot": target_slot,
- }
- return true
-
-
-func cancel_prepared_item_placement(item_id: StringName) -> void:
- _prepared_item_placements.erase(item_key(item_id))
-
-
-func can_accept_item_in_storage(item_id: StringName) -> bool:
- if not is_managed_item(item_id):
- return true
- var key := item_key(item_id)
- return _placements.has(key) or _first_free_slot(InventoryContainer.STORAGE) >= 0
-
-
-func can_accept_catch(catch_id: StringName = StringName()) -> bool:
- if not catch_id.is_empty() and _placements.has(catch_key(catch_id)):
- return true
- return _first_free_slot(InventoryContainer.INVENTORY) >= 0
-
-
-func is_managed_item(item_id: StringName) -> bool:
- var item: ItemDataType = (
- _item_catalog.get_item_by_id(item_id)
- if _item_catalog != null else null
- )
- return item != null and not item.is_bait() and not item.is_lure()
-
-
-func is_item_in_inventory(item_id: StringName) -> bool:
- return _is_entry_in_container(item_key(item_id), InventoryContainer.INVENTORY)
-
-
-func is_catch_in_inventory(catch_id: StringName) -> bool:
- return _is_entry_in_container(catch_key(catch_id), InventoryContainer.INVENTORY)
-
-
-func is_item_carried(item_id: StringName) -> bool:
- return _is_entry_carried(item_key(item_id))
-
-
-func is_catch_carried(catch_id: StringName) -> bool:
- return _is_entry_carried(catch_key(catch_id))
-
-
-func get_inventory_count() -> int:
- return get_entries(InventoryContainer.INVENTORY).size()
-
-
-func get_inventory_capacity() -> int:
- return INVENTORY_CAPACITIES[_backpack_level]
-
-
-func get_backpack_level() -> int:
- return _backpack_level
-
-
-func get_next_inventory_capacity() -> int:
- if _backpack_level >= MAX_BACKPACK_LEVEL:
- return -1
- return INVENTORY_CAPACITIES[_backpack_level + 1]
-
-
-func get_next_backpack_cost() -> int:
- if _backpack_level >= MAX_BACKPACK_LEVEL:
- return -1
- return BACKPACK_EXPANSION_COSTS[_backpack_level]
-
-
-func can_purchase_backpack(wallet: PlayerWalletType) -> bool:
- var cost := get_next_backpack_cost()
- return wallet != null and cost >= 0 and wallet.can_afford(cost)
-
-
-func purchase_backpack(wallet: PlayerWalletType) -> bool:
- if not can_purchase_backpack(wallet):
- return false
- var cost := get_next_backpack_cost()
- if not wallet.debit(cost):
- return false
- _backpack_level += 1
- backpack_capacity_changed.emit(_backpack_level, get_inventory_capacity())
- layout_changed.emit()
- return true
-
-
-func restore_backpack_level(level: int) -> bool:
- var validated := clampi(level, 0, MAX_BACKPACK_LEVEL)
- var changed := _backpack_level != validated
- _backpack_level = validated
- if changed:
- backpack_capacity_changed.emit(_backpack_level, get_inventory_capacity())
- _reconcile(changed)
- return true
-
-
-func get_storage_count() -> int:
- return get_entries(InventoryContainer.STORAGE).size()
-
-
-func get_hotbar_count() -> int:
- return get_entries(InventoryContainer.HOTBAR).size()
-
-
-func get_storage_capacity() -> int:
- return _storage_capacity.get_capacity() if _storage_capacity != null else 0
-
-
-func get_entries(container: InventoryContainer) -> Array[Dictionary]:
- var entries: Array[Dictionary] = []
- for key: String in _placements:
- var placement: Dictionary = _placements[key]
- if int(placement.get("container", -1)) != int(container):
- continue
- var entry: Dictionary = placement.duplicate(true)
- entry["key"] = key
- entries.append(entry)
- entries.sort_custom(
- func(left: Dictionary, right: Dictionary) -> bool:
- return int(left.get("slot", -1)) < int(right.get("slot", -1))
- )
- return entries
-
-
-func get_entry(kind: EntryKind, identity: StringName) -> Dictionary:
- var key := _entry_key(kind, identity)
- return _placements.get(key, {}).duplicate(true)
-
-
-func get_container(kind: EntryKind, identity: StringName) -> int:
- return int(
- get_entry(kind, identity).get("container", -1)
- )
-
-
-func get_key_at(container: InventoryContainer, slot: int) -> String:
- return _key_at(container, slot)
-
-
-func get_entry_at(container: InventoryContainer, slot: int) -> Dictionary:
- var key := _key_at(container, slot)
- if key.is_empty():
- return {}
- var entry: Dictionary = _placements[key].duplicate(true)
- entry["key"] = key
- return entry
-
-
-func move_entry(
- kind: EntryKind,
- identity: StringName,
- target_container: InventoryContainer,
- target_slot: int,
-) -> bool:
- var key := _entry_key(kind, identity)
- if not _placements.has(key) or not _is_slot_valid(target_container, target_slot):
- return false
- var source: Dictionary = _placements[key]
- if (
- int(source.get("container", -1)) == int(target_container)
- and int(source.get("slot", -1)) == target_slot
- ):
- return true
- var displaced_key := _key_at(target_container, target_slot)
- var source_container: int = int(source.get("container", InventoryContainer.INVENTORY))
- var source_slot: int = int(source.get("slot", -1))
- source["container"] = int(target_container)
- source["slot"] = target_slot
- _placements[key] = source
- if not displaced_key.is_empty():
- var displaced: Dictionary = _placements[displaced_key]
- displaced["container"] = source_container
- displaced["slot"] = source_slot
- _placements[displaced_key] = displaced
- layout_changed.emit()
- return true
-
-
-func move_entry_to_first_free(
- kind: EntryKind,
- identity: StringName,
- target_container: InventoryContainer,
-) -> bool:
- var slot := _first_free_slot(target_container)
- return slot >= 0 and move_entry(kind, identity, target_container, slot)
-
-
-func return_hotbar_entry_to_inventory(slot: int) -> bool:
- var key := _key_at(InventoryContainer.HOTBAR, slot)
- if key.is_empty():
- return true
- var placement: Dictionary = _placements[key]
- return move_entry_to_first_free(
- int(placement.get("kind", -1)) as EntryKind,
- StringName(str(placement.get("identity", ""))),
- InventoryContainer.INVENTORY,
- )
-
-
-func return_all_hotbar_entries_to_inventory() -> bool:
- var entries := get_entries(InventoryContainer.HOTBAR)
- if get_inventory_count() + entries.size() > get_inventory_capacity():
- return false
- for entry: Dictionary in entries:
- if not move_entry_to_first_free(
- int(entry.get("kind", -1)) as EntryKind,
- StringName(str(entry.get("identity", ""))),
- InventoryContainer.INVENTORY,
- ):
- return false
- return true
-
-
-func to_save_data() -> Dictionary:
- var serialized: Array[Dictionary] = []
- var keys: Array[String] = []
- keys.assign(_placements.keys())
- keys.sort()
- for key: String in keys:
- var placement: Dictionary = _placements[key]
- serialized.append({
- "kind": int(placement.get("kind", EntryKind.ITEM)),
- "identity": str(placement.get("identity", "")),
- "container": int(placement.get("container", InventoryContainer.INVENTORY)),
- "slot": int(placement.get("slot", -1)),
- })
- return {
- "backpack_level": _backpack_level,
- "placements": serialized,
- }
-
-
-func restore_from_save_data(data: Dictionary) -> bool:
- var level_value: Variant = data.get("backpack_level", 0)
- if (
- typeof(level_value) not in [TYPE_INT, TYPE_FLOAT]
- or not is_equal_approx(float(level_value), floorf(float(level_value)))
- ):
- return false
- _backpack_level = clampi(int(level_value), 0, MAX_BACKPACK_LEVEL)
- var restored: Dictionary[String, Dictionary] = {}
- var values: Variant = data.get("placements", [])
- if typeof(values) != TYPE_ARRAY:
- return false
- for value: Variant in values as Array:
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var record := value as Dictionary
- var kind: int = int(record.get("kind", -1))
- var identity := StringName(str(record.get("identity", "")))
- var container: int = int(record.get("container", -1))
- var slot: int = int(record.get("slot", -1))
- if (
- kind not in [EntryKind.ITEM, EntryKind.CATCH]
- or identity.is_empty()
- or container not in [
- InventoryContainer.INVENTORY,
- InventoryContainer.STORAGE,
- InventoryContainer.HOTBAR,
- ]
- or not _is_slot_valid(container as InventoryContainer, slot)
- ):
- continue
- var key := _entry_key(kind as EntryKind, identity)
- if restored.has(key) or _placement_slot_occupied(restored, container, slot):
- continue
- restored[key] = {
- "kind": kind,
- "identity": identity,
- "container": container,
- "slot": slot,
- }
- _prepared_item_placements.clear()
- _placements = restored
- _reconcile(true)
- return true
-
-
-func reset_to_defaults() -> void:
- var capacity_changed := _backpack_level != 0
- _backpack_level = 0
- _prepared_item_placements.clear()
- _placements.clear()
- _reconcile(true)
- if capacity_changed:
- backpack_capacity_changed.emit(_backpack_level, get_inventory_capacity())
-
-
-static func item_key(item_id: StringName) -> String:
- return "item:%s" % String(item_id)
-
-
-static func catch_key(catch_id: StringName) -> String:
- return "catch:%s" % String(catch_id)
-
-
-func _entry_key(kind: EntryKind, identity: StringName) -> String:
- return item_key(identity) if kind == EntryKind.ITEM else catch_key(identity)
-
-
-func _is_entry_in_container(key: String, container: InventoryContainer) -> bool:
- var placement: Dictionary = _placements.get(key, {})
- return int(placement.get("container", -1)) == int(container)
-
-
-func _is_entry_carried(key: String) -> bool:
- var placement: Dictionary = _placements.get(key, {})
- var container: int = int(placement.get("container", -1))
- return container in [InventoryContainer.INVENTORY, InventoryContainer.HOTBAR]
-
-
-func _on_contents_changed() -> void:
- _reconcile(false)
-
-
-func _on_storage_capacity_changed(_level: int, _capacity: int) -> void:
- _reconcile(true)
-
-
-func _reconcile(force_signal: bool) -> void:
- if _reconciling or _bag == null or _fish_inventory == null:
- return
- _reconciling = true
- var expected: Dictionary[String, Dictionary] = {}
- for owned in _bag.get_all_items():
- if owned == null or not is_managed_item(owned.item_id):
- continue
- var key := item_key(owned.item_id)
- expected[key] = {"kind": EntryKind.ITEM, "identity": owned.item_id}
- for fish_catch in _fish_inventory.get_all_catches():
- if fish_catch == null or not fish_catch.is_valid():
- continue
- var key := catch_key(fish_catch.catch_id)
- expected[key] = {"kind": EntryKind.CATCH, "identity": fish_catch.catch_id}
- var changed: bool = false
- for key: String in _placements.keys():
- if expected.has(key):
- continue
- _placements.erase(key)
- changed = true
- _normalize_existing_placements()
- for key: String in expected:
- if _placements.has(key):
- continue
- var prepared: Dictionary = _prepared_item_placements.get(key, {})
- var target_container := InventoryContainer.INVENTORY
- var target_slot: int = -1
- if _is_prepared_item_placement_available(key):
- target_container = int(
- prepared["container"]
- ) as InventoryContainer
- target_slot = int(prepared["slot"])
- _prepared_item_placements.erase(key)
- if target_slot < 0:
- target_slot = _first_free_slot(target_container)
- if target_slot < 0:
- target_container = InventoryContainer.STORAGE
- target_slot = _first_free_slot(target_container)
- if target_slot < 0:
- push_error("Inventory and storage cannot fit owned entry '%s'." % key)
- continue
- var placement: Dictionary = expected[key].duplicate(true)
- placement["container"] = int(target_container)
- placement["slot"] = target_slot
- _placements[key] = placement
- changed = true
- _reconciling = false
- if changed or force_signal:
- layout_changed.emit()
-
-
-func _normalize_existing_placements() -> void:
- var occupied_inventory: Dictionary[int, bool] = {}
- var occupied_storage: Dictionary[int, bool] = {}
- var occupied_hotbar: Dictionary[int, bool] = {}
- var invalid_keys: Array[String] = []
- var keys: Array[String] = []
- keys.assign(_placements.keys())
- keys.sort()
- for key: String in keys:
- var placement: Dictionary = _placements[key]
- var container: int = int(placement.get("container", -1))
- var slot: int = int(placement.get("slot", -1))
- var occupied: Dictionary[int, bool]
- match container:
- InventoryContainer.INVENTORY:
- occupied = occupied_inventory
- InventoryContainer.STORAGE:
- occupied = occupied_storage
- InventoryContainer.HOTBAR:
- occupied = occupied_hotbar
- _:
- occupied = {}
- if (
- container not in [
- InventoryContainer.INVENTORY,
- InventoryContainer.STORAGE,
- InventoryContainer.HOTBAR,
- ]
- or not _is_slot_valid(container as InventoryContainer, slot)
- or occupied.has(slot)
- ):
- invalid_keys.append(key)
- continue
- occupied[slot] = true
- for key: String in invalid_keys:
- # Invalid records must stop occupying their old slot before free-space
- # lookup. Otherwise a duplicate can incorrectly reserve a valid slot.
- var placement: Dictionary = _placements[key]
- _placements.erase(key)
- var target := InventoryContainer.INVENTORY
- var slot := _first_free_slot(target)
- if slot < 0:
- target = InventoryContainer.STORAGE
- slot = _first_free_slot(target)
- if slot < 0:
- _placements.erase(key)
- continue
- placement["container"] = int(target)
- placement["slot"] = slot
- _placements[key] = placement
-
-
-func _first_free_slot(container: InventoryContainer) -> int:
- var capacity := (
- get_inventory_capacity() if container == InventoryContainer.INVENTORY
- else HOTBAR_SLOT_COUNT if container == InventoryContainer.HOTBAR
- else get_storage_capacity()
- )
- for slot: int in capacity:
- if _key_at(container, slot).is_empty():
- return slot
- return -1
-
-
-func _is_prepared_item_placement_available(key: String) -> bool:
- var placement: Dictionary = _prepared_item_placements.get(key, {})
- if placement.is_empty():
- return false
- var container: int = int(placement.get("container", -1))
- var slot: int = int(placement.get("slot", -1))
- if (
- container not in [
- InventoryContainer.INVENTORY,
- InventoryContainer.STORAGE,
- InventoryContainer.HOTBAR,
- ]
- or not _is_slot_valid(container as InventoryContainer, slot)
- ):
- return false
- return _key_at(container as InventoryContainer, slot).is_empty()
-
-
-func _prepared_slot_is_occupied(
- container: InventoryContainer,
- slot: int,
-) -> bool:
- for placement: Dictionary in _prepared_item_placements.values():
- if (
- int(placement.get("container", -1)) == int(container)
- and int(placement.get("slot", -1)) == slot
- ):
- return true
- return false
-
-
-func _key_at(container: InventoryContainer, slot: int) -> String:
- for key: String in _placements:
- var placement: Dictionary = _placements[key]
- if (
- int(placement.get("container", -1)) == int(container)
- and int(placement.get("slot", -1)) == slot
- ):
- return key
- return ""
-
-
-func _is_slot_valid(container: InventoryContainer, slot: int) -> bool:
- if slot < 0:
- return false
- return slot < (
- get_inventory_capacity() if container == InventoryContainer.INVENTORY
- else HOTBAR_SLOT_COUNT if container == InventoryContainer.HOTBAR
- else get_storage_capacity()
- )
-
-
-func _placement_slot_occupied(
- placements: Dictionary[String, Dictionary],
- container: int,
- slot: int,
-) -> bool:
- for placement: Dictionary in placements.values():
- if (
- int(placement.get("container", -1)) == container
- and int(placement.get("slot", -1)) == slot
- ):
- return true
- return false
diff --git a/inventory/player_inventory_layout.gd.uid b/inventory/player_inventory_layout.gd.uid
deleted file mode 100644
index debd549..0000000
--- a/inventory/player_inventory_layout.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c5cku54c6qgmw
diff --git a/items/catalog/art_kit.tres b/items/catalog/art_kit.tres
deleted file mode 100644
index e59705d..0000000
--- a/items/catalog/art_kit.tres
+++ /dev/null
@@ -1,17 +0,0 @@
-[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"]
-[ext_resource type="Texture2D" path="res://items/icons/art/art_kit.png" id="2_icon"]
-
-[resource]
-script = ExtResource("1_item")
-item_id = &"art_kit"
-display_name = "Art Kit"
-description = "Select the Art Kit in the Hotbar to paint."
-category = 1
-icon = ExtResource("2_icon")
-stackable = false
-max_stack = 1
-usable = false
-equippable = true
-hotbar_allowed = true
diff --git a/items/catalog/basic_cube.tres b/items/catalog/basic_cube.tres
deleted file mode 100644
index 86a2cf9..0000000
--- a/items/catalog/basic_cube.tres
+++ /dev/null
@@ -1,17 +0,0 @@
-[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"]
-[ext_resource type="Texture2D" path="res://ui/icons/pictograms/x_light.png" id="2_icon"]
-
-[resource]
-script = ExtResource("1_item")
-item_id = &"basic_cube"
-display_name = "basic cube"
-description = "a simple piece of decor for testing your RV layout."
-category = 6
-icon = ExtResource("2_icon")
-stackable = true
-max_stack = 32
-usable = false
-equippable = true
-hotbar_allowed = true
diff --git a/items/catalog/basic_fishing_rod.tres b/items/catalog/basic_fishing_rod.tres
index 9849b89..4530abc 100644
--- a/items/catalog/basic_fishing_rod.tres
+++ b/items/catalog/basic_fishing_rod.tres
@@ -1,14 +1,13 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=3 format=3]
+[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-[ext_resource type="Texture2D" path="res://items/icons/equipment/64_rod_base_thick.png" id="2_icon"]
+[ext_resource type="Script" path="res://items/item_data.gd" id="1_script"]
+[ext_resource type="Texture2D" path="res://items/icons/placeholder/basic_fishing_rod.svg" id="2_icon"]
[resource]
script = ExtResource("1_script")
item_id = &"basic_fishing_rod"
display_name = "basic fishing rod"
description = "a dependable starter rod. select it to cast."
-active = true
category = 0
icon = ExtResource("2_icon")
stackable = false
@@ -16,8 +15,3 @@ max_stack = 1
usable = true
equippable = true
hotbar_allowed = true
-shop_order = 1
-shop_price = 0
-unlock_level = 1
-effect_summary = "neutral baseline with no catch-selection or handling modifiers."
-tradeoff = "none."
diff --git a/items/catalog/batteries.tres b/items/catalog/batteries.tres
deleted file mode 100644
index 9f26650..0000000
--- a/items/catalog/batteries.tres
+++ /dev/null
@@ -1,15 +0,0 @@
-[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"]
-
-[resource]
-script = ExtResource("1_item")
-item_id = &"batteries"
-display_name = "batteries"
-description = "replacement batteries for the fish finder."
-category = 3
-stackable = true
-max_stack = 99
-usable = false
-equippable = false
-hotbar_allowed = false
diff --git a/items/catalog/coffee.tres b/items/catalog/coffee.tres
index 00d5206..755b671 100644
--- a/items/catalog/coffee.tres
+++ b/items/catalog/coffee.tres
@@ -1,7 +1,7 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
-[ext_resource type="Texture2D" path="res://items/icons/consumables/64_consumable_coffee.png" id="2"]
+[ext_resource type="Texture2D" path="res://items/icons/placeholder/coffee.svg" id="2"]
[resource]
script = ExtResource("1")
diff --git a/items/catalog/cooler_expansion.tres b/items/catalog/cooler_expansion.tres
new file mode 100644
index 0000000..2e9ed29
--- /dev/null
+++ b/items/catalog/cooler_expansion.tres
@@ -0,0 +1,17 @@
+[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
+
+[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
+[ext_resource type="Texture2D" path="res://items/icons/placeholder/cooler_expansion.svg" id="2"]
+
+[resource]
+script = ExtResource("1")
+item_id = &"cooler_expansion"
+display_name = "cooler expansion"
+description = "permanently increases the number of fish the cooler can hold."
+category = 4
+icon = ExtResource("2")
+stackable = false
+max_stack = 1
+usable = false
+equippable = false
+hotbar_allowed = false
diff --git a/items/catalog/crab_net.tres b/items/catalog/crab_net.tres
deleted file mode 100644
index a6e26b0..0000000
--- a/items/catalog/crab_net.tres
+++ /dev/null
@@ -1,18 +0,0 @@
-[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"]
-[ext_resource type="Texture2D" path="res://items/icons/equipment/net.png" id="2_icon"]
-
-[resource]
-script = ExtResource("1_item")
-item_id = &"crab_net"
-display_name = "crab net"
-description = "a hand net for catching crabs and other small shore animals."
-active = true
-category = 1
-icon = ExtResource("2_icon")
-stackable = false
-max_stack = 1
-usable = false
-equippable = true
-hotbar_allowed = true
diff --git a/items/catalog/energy_drink.tres b/items/catalog/energy_drink.tres
index d3fcd93..813230e 100644
--- a/items/catalog/energy_drink.tres
+++ b/items/catalog/energy_drink.tres
@@ -1,7 +1,7 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
-[ext_resource type="Texture2D" path="res://items/icons/consumables/64_consumable_energydrink_redfish.png" id="2"]
+[ext_resource type="Texture2D" path="res://items/icons/placeholder/energy_drink.svg" id="2"]
[resource]
script = ExtResource("1")
diff --git a/items/catalog/fish_finder.tres b/items/catalog/fish_finder.tres
index a3fb6c4..a37fb4a 100644
--- a/items/catalog/fish_finder.tres
+++ b/items/catalog/fish_finder.tres
@@ -1,17 +1,16 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
-[ext_resource type="Texture2D" path="res://items/icons/consumables/64_consumable_fishfinder.png" id="2"]
+[ext_resource type="Texture2D" path="res://items/icons/placeholder/fish_finder.svg" id="2"]
[resource]
script = ExtResource("1")
item_id = &"fish_finder"
display_name = "fish finder"
-description = "uses one battery to temporarily shorten bite waits and improve rarity weights."
-category = 1
+description = "temporarily shortens bite waits and improves rarity weights."
+category = 3
icon = ExtResource("2")
-stackable = false
-max_stack = 1
+stackable = true
+max_stack = 99
usable = true
-equippable = true
hotbar_allowed = true
diff --git a/items/catalog/fishing_net.tres b/items/catalog/fishing_net.tres
deleted file mode 100644
index 04b1919..0000000
--- a/items/catalog/fishing_net.tres
+++ /dev/null
@@ -1,16 +0,0 @@
-[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"]
-
-[resource]
-script = ExtResource("1_item")
-item_id = &"fishing_net"
-display_name = "fishing net"
-description = "a placeable water net reserved for a future passive fishing system."
-active = true
-category = 1
-stackable = false
-max_stack = 1
-usable = false
-equippable = false
-hotbar_allowed = false
diff --git a/items/catalog/item_catalog.tres b/items/catalog/item_catalog.tres
index 2f15e0a..3cccb42 100644
--- a/items/catalog/item_catalog.tres
+++ b/items/catalog/item_catalog.tres
@@ -1,4 +1,4 @@
-[gd_resource type="Resource" script_class="ItemCatalog" load_steps=42 format=3]
+[gd_resource type="Resource" script_class="ItemCatalog" load_steps=8 format=3]
[ext_resource type="Script" path="res://items/item_catalog.gd" id="1_script"]
[ext_resource type="Resource" path="res://items/catalog/basic_fishing_rod.tres" id="2_rod"]
@@ -6,42 +6,8 @@
[ext_resource type="Resource" path="res://items/catalog/energy_drink.tres" id="4_energy"]
[ext_resource type="Resource" path="res://items/catalog/snack.tres" id="5_snack"]
[ext_resource type="Resource" path="res://items/catalog/fish_finder.tres" id="6_finder"]
-[ext_resource type="Resource" path="res://items/catalog/art_kit.tres" id="8_art_kit"]
-[ext_resource type="Resource" path="res://items/catalog/worms.tres" id="9_worms"]
-[ext_resource type="Resource" path="res://items/catalog/snails.tres" id="10_snails"]
-[ext_resource type="Resource" path="res://items/catalog/shrimp.tres" id="11_shrimp"]
-[ext_resource type="Resource" path="res://items/catalog/squid_chunks.tres" id="12_squid"]
-[ext_resource type="Resource" path="res://items/catalog/whole_anchovy.tres" id="13_anchovy"]
-[ext_resource type="Resource" path="res://items/catalog/whole_sardine.tres" id="14_sardine"]
-[ext_resource type="Resource" path="res://items/catalog/luminous_roe.tres" id="15_roe"]
-[ext_resource type="Resource" path="res://items/catalog/the_standby.tres" id="16_standby"]
-[ext_resource type="Resource" path="res://items/catalog/batteries.tres" id="17_batteries"]
-[ext_resource type="Resource" path="res://items/catalog/rods/cardboard_classic_rod.tres" id="18_cardboard"]
-[ext_resource type="Resource" path="res://items/catalog/rods/pond_skipper_rod.tres" id="19_pond"]
-[ext_resource type="Resource" path="res://items/catalog/rods/river_runner_rod.tres" id="20_river"]
-[ext_resource type="Resource" path="res://items/catalog/rods/lake_lunker_rod.tres" id="21_lake"]
-[ext_resource type="Resource" path="res://items/catalog/rods/salt_spray_rod.tres" id="22_salt"]
-[ext_resource type="Resource" path="res://items/catalog/rods/whisker_stick_rod.tres" id="23_whisker"]
-[ext_resource type="Resource" path="res://items/catalog/rods/reef_raker_rod.tres" id="24_reef"]
-[ext_resource type="Resource" path="res://items/catalog/rods/moonbeam_rod.tres" id="25_moonbeam"]
-[ext_resource type="Resource" path="res://items/catalog/rods/sun_chaser_rod.tres" id="26_sun"]
-[ext_resource type="Resource" path="res://items/catalog/rods/rainmaker_rod.tres" id="27_rain"]
-[ext_resource type="Resource" path="res://items/catalog/rods/fogcatcher_rod.tres" id="28_fog"]
-[ext_resource type="Resource" path="res://items/catalog/rods/field_guide_rod.tres" id="29_guide"]
-[ext_resource type="Resource" path="res://items/catalog/rods/small_fry_rod.tres" id="30_small"]
-[ext_resource type="Resource" path="res://items/catalog/rods/heavy_hauler_rod.tres" id="31_heavy"]
-[ext_resource type="Resource" path="res://items/catalog/rods/pocket_rocket_rod.tres" id="32_rocket"]
-[ext_resource type="Resource" path="res://items/catalog/rods/lucky_twig_rod.tres" id="33_lucky"]
-[ext_resource type="Resource" path="res://items/catalog/rods/showboat_rod.tres" id="34_showboat"]
-[ext_resource type="Resource" path="res://items/catalog/rods/deep_blue_rod.tres" id="35_deep"]
-[ext_resource type="Resource" path="res://items/catalog/rods/oddity_string_rod.tres" id="36_oddity"]
-[ext_resource type="Resource" path="res://items/catalog/rods/aurora_rod.tres" id="37_aurora"]
-[ext_resource type="Resource" path="res://items/catalog/magnet.tres" id="38_magnet"]
-[ext_resource type="Resource" path="res://items/catalog/crab_net.tres" id="39_crab_net"]
-[ext_resource type="Resource" path="res://items/catalog/standard_shovel.tres" id="40_shovel"]
-[ext_resource type="Resource" path="res://items/catalog/fishing_net.tres" id="41_fishing_net"]
-[ext_resource type="Resource" path="res://items/catalog/basic_cube.tres" id="42_basic_cube"]
+[ext_resource type="Resource" path="res://items/catalog/cooler_expansion.tres" id="7_cooler"]
[resource]
script = ExtResource("1_script")
-items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("8_art_kit"), ExtResource("9_worms"), ExtResource("10_snails"), ExtResource("11_shrimp"), ExtResource("12_squid"), ExtResource("14_sardine"), ExtResource("13_anchovy"), ExtResource("15_roe"), ExtResource("16_standby"), ExtResource("17_batteries"), ExtResource("18_cardboard"), ExtResource("19_pond"), ExtResource("20_river"), ExtResource("21_lake"), ExtResource("22_salt"), ExtResource("23_whisker"), ExtResource("24_reef"), ExtResource("25_moonbeam"), ExtResource("26_sun"), ExtResource("27_rain"), ExtResource("28_fog"), ExtResource("29_guide"), ExtResource("30_small"), ExtResource("31_heavy"), ExtResource("32_rocket"), ExtResource("33_lucky"), ExtResource("34_showboat"), ExtResource("35_deep"), ExtResource("36_oddity"), ExtResource("37_aurora"), ExtResource("38_magnet"), ExtResource("39_crab_net"), ExtResource("40_shovel"), ExtResource("41_fishing_net"), ExtResource("42_basic_cube")]
+items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("7_cooler")]
diff --git a/items/catalog/luminous_roe.tres b/items/catalog/luminous_roe.tres
deleted file mode 100644
index 677563e..0000000
--- a/items/catalog/luminous_roe.tres
+++ /dev/null
@@ -1,16 +0,0 @@
-[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
-[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_roe_luminous.png" id="2_icon"]
-
-[resource]
-script = ExtResource("1")
-item_id = &"luminous_roe"
-display_name = "luminous roe"
-description = "rare glowing roe with the strongest rarity and quality odds."
-icon = ExtResource("2_icon")
-category = 2
-bait_tags = Array[StringName]([&"bait_rarity_6"])
-stackable = true
-max_stack = 10
-hotbar_allowed = false
diff --git a/items/catalog/magnet.tres b/items/catalog/magnet.tres
deleted file mode 100644
index eb2e25c..0000000
--- a/items/catalog/magnet.tres
+++ /dev/null
@@ -1,16 +0,0 @@
-[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
-
-[resource]
-script = ExtResource("1")
-item_id = &"magnet"
-display_name = "magnet"
-description = "a heavy magnet ready for future magnet fishing."
-active = true
-category = 1
-stackable = false
-max_stack = 1
-usable = false
-equippable = false
-hotbar_allowed = false
diff --git a/items/catalog/rods/aurora_rod.tres b/items/catalog/rods/aurora_rod.tres
deleted file mode 100644
index 303ce95..0000000
--- a/items/catalog/rods/aurora_rod.tres
+++ /dev/null
@@ -1,25 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"aurora_rod"
-display_name = "aurora rod"
-description = "a shifting ribbon of color for anglers who have tried nearly everything else."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 21
-shop_price = 5000
-unlock_level = 15
-bite_time_multiplier = 1.35
-reel_speed_multiplier = 0.9
-rarity_weight_multipliers = Array[float]([0.85, 1.00, 1.15, 1.30, 1.45])
-quality_weight_multipliers = Array[float]([0.80, 0.95, 1.15, 1.30, 1.45])
-effect_summary = "modestly improves both species rarity and catch quality across all eligible fish."
-tradeoff = "the slowest bite cycle in the set; intended as an expensive late-game sidegrade."
diff --git a/items/catalog/rods/cardboard_classic_rod.tres b/items/catalog/rods/cardboard_classic_rod.tres
deleted file mode 100644
index 97a304f..0000000
--- a/items/catalog/rods/cardboard_classic_rod.tres
+++ /dev/null
@@ -1,26 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"cardboard_classic_rod"
-display_name = "cardboard classic"
-description = "a cheerful folded rod that gets bites quickly, if not impressively."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 2
-shop_price = 40
-unlock_level = 1
-bite_time_multiplier = 0.85
-reel_speed_multiplier = 0.95
-barrier_power_multiplier = 0.85
-rarity_weight_multipliers = Array[float]([1.20, 1.00, 0.85, 0.70, 0.55])
-quality_weight_multipliers = Array[float]([1.20, 1.05, 0.85, 0.70, 0.55])
-effect_summary = "shorter bite waits and a low-cost early sidegrade."
-tradeoff = "leans toward common, lower-quality fish and has weaker handling."
diff --git a/items/catalog/rods/deep_blue_rod.tres b/items/catalog/rods/deep_blue_rod.tres
deleted file mode 100644
index 2d9b03f..0000000
--- a/items/catalog/rods/deep_blue_rod.tres
+++ /dev/null
@@ -1,27 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"deep_blue_rod"
-display_name = "deep blue"
-description = "a heavy navy rod for enormous shapes moving far offshore."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 19
-shop_price = 2500
-unlock_level = 12
-preferred_collection_groups = Array[StringName]([&"Coastal sharks", &"Large coastal predators", &"Oceanic billfish", &"Oceanic pelagic fish", &"Oceanic tuna", &"Open-ocean giants"])
-affinity_weight_multiplier = 1.75
-weight_roll_bias = 0.32
-bite_time_multiplier = 1.28
-reel_speed_multiplier = 0.82
-barrier_power_multiplier = 1.25
-effect_summary = "favors large ocean predator and pelagic collection groups and biases weight upward."
-tradeoff = "very long bite waits and slower reeling."
diff --git a/items/catalog/rods/field_guide_rod.tres b/items/catalog/rods/field_guide_rod.tres
deleted file mode 100644
index 2b19f30..0000000
--- a/items/catalog/rods/field_guide_rod.tres
+++ /dev/null
@@ -1,23 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"field_guide_rod"
-display_name = "field guide rod"
-description = "a note-covered rod that keeps tugging toward something new."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 13
-shop_price = 1350
-unlock_level = 8
-undiscovered_weight_multiplier = 1.9
-bite_time_multiplier = 1.15
-effect_summary = "substantially increases the relative weight of eligible undiscovered species."
-tradeoff = "longer bite waits and no bonus after a species is discovered."
diff --git a/items/catalog/rods/fogcatcher_rod.tres b/items/catalog/rods/fogcatcher_rod.tres
deleted file mode 100644
index c0fdd25..0000000
--- a/items/catalog/rods/fogcatcher_rod.tres
+++ /dev/null
@@ -1,23 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"fogcatcher_rod"
-display_name = "fogcatcher"
-description = "a hazy blue rod for fish that prefer not to be seen."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 12
-shop_price = 1100
-unlock_level = 7
-weather_affinity = 2
-affinity_weight_multiplier = 1.65
-effect_summary = "strongly favors eligible fog-active fish."
-tradeoff = "its affinity provides no benefit outside foggy weather."
diff --git a/items/catalog/rods/heavy_hauler_rod.tres b/items/catalog/rods/heavy_hauler_rod.tres
deleted file mode 100644
index bc68c78..0000000
--- a/items/catalog/rods/heavy_hauler_rod.tres
+++ /dev/null
@@ -1,25 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"heavy_hauler_rod"
-display_name = "heavy hauler"
-description = "a thick rod that would rather bring home one enormous fish."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 15
-shop_price = 1500
-unlock_level = 9
-weight_roll_bias = 0.28
-bite_time_multiplier = 1.18
-reel_speed_multiplier = 0.88
-barrier_power_multiplier = 1.2
-effect_summary = "biases fish weight upward and adds barrier power."
-tradeoff = "longer bite waits and slower reeling."
diff --git a/items/catalog/rods/lake_lunker_rod.tres b/items/catalog/rods/lake_lunker_rod.tres
deleted file mode 100644
index 4d30f56..0000000
--- a/items/catalog/rods/lake_lunker_rod.tres
+++ /dev/null
@@ -1,25 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"lake_lunker_rod"
-display_name = "lake lunker"
-description = "a broad, patient rod that favors hefty catches from open lakes."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 5
-shop_price = 400
-unlock_level = 4
-preferred_location_tags = Array[StringName]([&"lake"])
-affinity_weight_multiplier = 1.45
-weight_roll_bias = 0.12
-bite_time_multiplier = 1.05
-effect_summary = "favors eligible lake fish and gently biases weight rolls upward."
-tradeoff = "slightly longer bite waits."
diff --git a/items/catalog/rods/lucky_twig_rod.tres b/items/catalog/rods/lucky_twig_rod.tres
deleted file mode 100644
index 8c26b19..0000000
--- a/items/catalog/rods/lucky_twig_rod.tres
+++ /dev/null
@@ -1,25 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"lucky_twig_rod"
-display_name = "lucky twig"
-description = "an ordinary-looking branch that insists it is extremely lucky."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 17
-shop_price = 1800
-unlock_level = 10
-bite_time_multiplier = 1.2
-reel_speed_multiplier = 0.9
-barrier_power_multiplier = 0.85
-rarity_weight_multipliers = Array[float]([0.65, 1.00, 1.35, 1.65, 2.00])
-effect_summary = "strongly shifts species rarity toward rare, epic, and legendary fish."
-tradeoff = "longer bite waits, slower reeling, and weaker barrier power."
diff --git a/items/catalog/rods/moonbeam_rod.tres b/items/catalog/rods/moonbeam_rod.tres
deleted file mode 100644
index df83634..0000000
--- a/items/catalog/rods/moonbeam_rod.tres
+++ /dev/null
@@ -1,25 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"moonbeam_rod"
-display_name = "moonbeam rod"
-description = "a pale rod that wakes up when the island goes quiet."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 9
-shop_price = 950
-unlock_level = 6
-time_affinity = 2
-affinity_weight_multiplier = 1.5
-bite_time_multiplier = 1.08
-quality_weight_multipliers = Array[float]([0.95, 0.98, 1.00, 1.08, 1.25])
-effect_summary = "favors eligible night fish and gives shiny quality a modest lift."
-tradeoff = "slightly longer bite waits."
diff --git a/items/catalog/rods/oddity_string_rod.tres b/items/catalog/rods/oddity_string_rod.tres
deleted file mode 100644
index 8c51739..0000000
--- a/items/catalog/rods/oddity_string_rod.tres
+++ /dev/null
@@ -1,25 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"oddity_string_rod"
-display_name = "oddity string"
-description = "a crooked spool of string that attracts fish with questionable silhouettes."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 20
-shop_price = 2600
-unlock_level = 12
-preferred_collection_groups = Array[StringName]([&"Brackish oddities", &"Coastal oddities", &"Deep-sea oddities", &"Freshwater oddities", &"Reef oddities"])
-affinity_weight_multiplier = 1.8
-bite_time_multiplier = 1.18
-quality_weight_multipliers = Array[float]([1.10, 1.05, 0.95, 0.90, 0.90])
-effect_summary = "favors eligible fish from authored oddity collection groups."
-tradeoff = "longer bite waits and a slight average-quality penalty."
diff --git a/items/catalog/rods/pocket_rocket_rod.tres b/items/catalog/rods/pocket_rocket_rod.tres
deleted file mode 100644
index b93d348..0000000
--- a/items/catalog/rods/pocket_rocket_rod.tres
+++ /dev/null
@@ -1,25 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"pocket_rocket_rod"
-display_name = "pocket rocket"
-description = "a buzzing pocket-sized rod for anglers in a hurry."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 16
-shop_price = 1400
-unlock_level = 9
-bite_time_multiplier = 0.7
-reel_speed_multiplier = 1.25
-barrier_power_multiplier = 0.8
-rarity_weight_multipliers = Array[float]([1.15, 1.00, 0.85, 0.72, 0.60])
-effect_summary = "greatly shortens bite waits and reels faster."
-tradeoff = "reduces higher-rarity odds and barrier power."
diff --git a/items/catalog/rods/pond_skipper_rod.tres b/items/catalog/rods/pond_skipper_rod.tres
deleted file mode 100644
index adb18e4..0000000
--- a/items/catalog/rods/pond_skipper_rod.tres
+++ /dev/null
@@ -1,24 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"pond_skipper_rod"
-display_name = "pond skipper"
-description = "a springy little rod that feels at home beside quiet ponds."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 3
-shop_price = 150
-unlock_level = 2
-preferred_location_tags = Array[StringName]([&"starter_pond", &"pond"])
-affinity_weight_multiplier = 1.5
-bite_time_multiplier = 0.95
-effect_summary = "increases the relative selection weight of eligible pond fish."
-tradeoff = "none; a focused sidegrade rather than a direct upgrade."
diff --git a/items/catalog/rods/rainmaker_rod.tres b/items/catalog/rods/rainmaker_rod.tres
deleted file mode 100644
index 3a407fd..0000000
--- a/items/catalog/rods/rainmaker_rod.tres
+++ /dev/null
@@ -1,23 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"rainmaker_rod"
-display_name = "rainmaker"
-description = "a drip-shaped rod that becomes unusually lucky in the rain."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 11
-shop_price = 1100
-unlock_level = 7
-weather_affinity = 1
-affinity_weight_multiplier = 1.65
-effect_summary = "strongly favors eligible rain-active fish."
-tradeoff = "its affinity provides no benefit outside rainy weather."
diff --git a/items/catalog/rods/reef_raker_rod.tres b/items/catalog/rods/reef_raker_rod.tres
deleted file mode 100644
index 2b3ff91..0000000
--- a/items/catalog/rods/reef_raker_rod.tres
+++ /dev/null
@@ -1,24 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"reef_raker_rod"
-display_name = "reef raker"
-description = "a bright hooked rod tuned for the crowded life around reefs."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 8
-shop_price = 900
-unlock_level = 6
-preferred_collection_groups = Array[StringName]([&"Cold reef fish", &"Reef ambush fish", &"Reef eels", &"Reef flatfish", &"Reef grazers", &"Reef oddities", &"Reef ornamentals", &"Reef predators", &"Reef schooling fish", &"Reef snapper", &"Reef wrasses", &"Temperate reef fish", &"Temperate reef predators"])
-affinity_weight_multiplier = 1.45
-quality_weight_multipliers = Array[float]([0.95, 1.00, 1.12, 1.05, 1.00])
-effect_summary = "favors eligible reef collection groups and modestly favors impressive quality."
-tradeoff = "no advantage away from reef-associated fish."
diff --git a/items/catalog/rods/river_runner_rod.tres b/items/catalog/rods/river_runner_rod.tres
deleted file mode 100644
index 7f0a464..0000000
--- a/items/catalog/rods/river_runner_rod.tres
+++ /dev/null
@@ -1,24 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"river_runner_rod"
-display_name = "river runner"
-description = "a lively rod made to keep pace with moving river water."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 4
-shop_price = 250
-unlock_level = 3
-preferred_location_tags = Array[StringName]([&"river"])
-affinity_weight_multiplier = 1.5
-reel_speed_multiplier = 1.05
-effect_summary = "increases the relative selection weight of eligible river fish."
-tradeoff = "none; a focused sidegrade rather than a direct upgrade."
diff --git a/items/catalog/rods/salt_spray_rod.tres b/items/catalog/rods/salt_spray_rod.tres
deleted file mode 100644
index f3e6102..0000000
--- a/items/catalog/rods/salt_spray_rod.tres
+++ /dev/null
@@ -1,24 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"salt_spray_rod"
-display_name = "salt spray"
-description = "a sea-green rod that seems happiest with salt on the line."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 6
-shop_price = 600
-unlock_level = 5
-preferred_location_tags = Array[StringName]([&"ocean"])
-affinity_weight_multiplier = 1.5
-rarity_weight_multipliers = Array[float]([0.95, 1.00, 1.05, 1.05, 1.05])
-effect_summary = "increases the relative selection weight of eligible ocean fish."
-tradeoff = "none; a focused sidegrade rather than a direct upgrade."
diff --git a/items/catalog/rods/showboat_rod.tres b/items/catalog/rods/showboat_rod.tres
deleted file mode 100644
index 99bef41..0000000
--- a/items/catalog/rods/showboat_rod.tres
+++ /dev/null
@@ -1,24 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"showboat_rod"
-display_name = "showboat"
-description = "a theatrical rod that refuses to land an unimpressive fish quietly."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 18
-shop_price = 2000
-unlock_level = 11
-bite_time_multiplier = 1.18
-reel_speed_multiplier = 0.9
-quality_weight_multipliers = Array[float]([0.50, 0.75, 1.35, 1.70, 2.10])
-effect_summary = "strongly shifts catch quality toward impressive, exceptional, and shiny."
-tradeoff = "longer bite waits and slower reeling; species rarity is unchanged."
diff --git a/items/catalog/rods/small_fry_rod.tres b/items/catalog/rods/small_fry_rod.tres
deleted file mode 100644
index a1631f0..0000000
--- a/items/catalog/rods/small_fry_rod.tres
+++ /dev/null
@@ -1,23 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"small_fry_rod"
-display_name = "small fry"
-description = "a tiny rod for anglers who value character over size."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 14
-shop_price = 1200
-unlock_level = 8
-weight_roll_bias = -0.25
-quality_weight_multipliers = Array[float]([0.75, 0.90, 1.25, 1.40, 1.55])
-effect_summary = "biases fish weight downward while improving high-quality outcomes."
-tradeoff = "produces smaller catches on average."
diff --git a/items/catalog/rods/sun_chaser_rod.tres b/items/catalog/rods/sun_chaser_rod.tres
deleted file mode 100644
index 22c0803..0000000
--- a/items/catalog/rods/sun_chaser_rod.tres
+++ /dev/null
@@ -1,24 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"sun_chaser_rod"
-display_name = "sun chaser"
-description = "a warm cream rod that likes bright mornings and busy afternoons."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 10
-shop_price = 950
-unlock_level = 6
-time_affinity = 1
-affinity_weight_multiplier = 1.4
-rarity_weight_multipliers = Array[float]([0.95, 1.00, 1.12, 1.08, 1.00])
-effect_summary = "favors eligible daytime fish and modestly improves rare-species odds."
-tradeoff = "none; a focused sidegrade rather than a direct upgrade."
diff --git a/items/catalog/rods/whisker_stick_rod.tres b/items/catalog/rods/whisker_stick_rod.tres
deleted file mode 100644
index a49ff63..0000000
--- a/items/catalog/rods/whisker_stick_rod.tres
+++ /dev/null
@@ -1,24 +0,0 @@
-[gd_resource type="Resource" script_class="FishingRodData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/fishing_rod_data.gd" id="1_script"]
-
-[resource]
-script = ExtResource("1_script")
-item_id = &"whisker_stick_rod"
-display_name = "whisker stick"
-description = "a bendy rod with a suspicious talent for finding catfish."
-active = false
-category = 0
-stackable = false
-max_stack = 1
-usable = true
-equippable = true
-hotbar_allowed = true
-shop_order = 7
-shop_price = 750
-unlock_level = 5
-preferred_collection_groups = Array[StringName]([&"Freshwater catfish"])
-affinity_weight_multiplier = 1.7
-bite_time_multiplier = 1.08
-effect_summary = "strongly favors eligible fish in the Freshwater catfish collection group."
-tradeoff = "slightly longer bite waits while it searches for whiskers."
diff --git a/items/catalog/shrimp.tres b/items/catalog/shrimp.tres
deleted file mode 100644
index 6864265..0000000
--- a/items/catalog/shrimp.tres
+++ /dev/null
@@ -1,16 +0,0 @@
-[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
-[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_shrimp.png" id="2_icon"]
-
-[resource]
-script = ExtResource("1")
-item_id = &"shrimp"
-display_name = "shrimp"
-description = "lively bait that noticeably improves rarity and quality odds."
-icon = ExtResource("2_icon")
-category = 2
-bait_tags = Array[StringName]([&"bait_rarity_2"])
-stackable = true
-max_stack = 10
-hotbar_allowed = false
diff --git a/items/catalog/snack.tres b/items/catalog/snack.tres
index 4215595..9d7c89b 100644
--- a/items/catalog/snack.tres
+++ b/items/catalog/snack.tres
@@ -1,12 +1,12 @@
[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
-[ext_resource type="Texture2D" path="res://items/icons/consumables/64_consumable_energybar.png" id="2"]
+[ext_resource type="Texture2D" path="res://items/icons/placeholder/snack.svg" id="2"]
[resource]
script = ExtResource("1")
item_id = &"snack"
-display_name = "Energy Bar"
+display_name = "snack"
description = "temporarily adds one barrier damage."
category = 3
icon = ExtResource("2")
diff --git a/items/catalog/snails.tres b/items/catalog/snails.tres
deleted file mode 100644
index adddefb..0000000
--- a/items/catalog/snails.tres
+++ /dev/null
@@ -1,16 +0,0 @@
-[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
-[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_snail.png" id="2_icon"]
-
-[resource]
-script = ExtResource("1")
-item_id = &"snails"
-display_name = "snails"
-description = "slow-moving bait with better rarity and quality odds."
-icon = ExtResource("2_icon")
-category = 2
-bait_tags = Array[StringName]([&"bait_rarity_1"])
-stackable = true
-max_stack = 10
-hotbar_allowed = false
diff --git a/items/catalog/squid_chunks.tres b/items/catalog/squid_chunks.tres
deleted file mode 100644
index bf0da93..0000000
--- a/items/catalog/squid_chunks.tres
+++ /dev/null
@@ -1,16 +0,0 @@
-[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
-[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_squid.png" id="2_icon"]
-
-[resource]
-script = ExtResource("1")
-item_id = &"squid_chunks"
-display_name = "squid chunks"
-description = "rich cut bait that strongly favors rarer, higher-quality catches."
-icon = ExtResource("2_icon")
-category = 2
-bait_tags = Array[StringName]([&"bait_rarity_3"])
-stackable = true
-max_stack = 10
-hotbar_allowed = false
diff --git a/items/catalog/standard_shovel.tres b/items/catalog/standard_shovel.tres
deleted file mode 100644
index b30589c..0000000
--- a/items/catalog/standard_shovel.tres
+++ /dev/null
@@ -1,18 +0,0 @@
-[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"]
-[ext_resource type="Texture2D" path="res://items/icons/equipment/shovel.png" id="2_icon"]
-
-[resource]
-script = ExtResource("1_item")
-item_id = &"standard_shovel"
-display_name = "standard shovel"
-description = "a sturdy shovel for digging up buried finds on the beach."
-active = true
-category = 1
-icon = ExtResource("2_icon")
-stackable = false
-max_stack = 1
-usable = false
-equippable = true
-hotbar_allowed = true
diff --git a/items/catalog/the_standby.tres b/items/catalog/the_standby.tres
deleted file mode 100644
index 1fcc92e..0000000
--- a/items/catalog/the_standby.tres
+++ /dev/null
@@ -1,16 +0,0 @@
-[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
-
-[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"]
-
-[resource]
-script = ExtResource("1_item")
-item_id = &"the_standby"
-display_name = "The Standby"
-description = "holds a bite until you're ready to start the fight."
-category = 7
-lure_effects = Array[StringName]([&"deferred_fight"])
-stackable = false
-max_stack = 1
-usable = false
-equippable = true
-hotbar_allowed = false
diff --git a/items/catalog/whole_anchovy.tres b/items/catalog/whole_anchovy.tres
deleted file mode 100644
index b52018b..0000000
--- a/items/catalog/whole_anchovy.tres
+++ /dev/null
@@ -1,16 +0,0 @@
-[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
-[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_sardine.png" id="2_icon"]
-
-[resource]
-script = ExtResource("1")
-item_id = &"whole_anchovy"
-display_name = "sardine"
-description = "valuable oily schooling bait tuned for exceptional rarity and quality odds."
-icon = ExtResource("2_icon")
-category = 2
-bait_tags = Array[StringName]([&"bait_rarity_5"])
-stackable = true
-max_stack = 10
-hotbar_allowed = false
diff --git a/items/catalog/whole_sardine.tres b/items/catalog/whole_sardine.tres
deleted file mode 100644
index 78a0e0e..0000000
--- a/items/catalog/whole_sardine.tres
+++ /dev/null
@@ -1,16 +0,0 @@
-[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
-[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_minnow.png" id="2_icon"]
-
-[resource]
-script = ExtResource("1")
-item_id = &"whole_sardine"
-display_name = "minnow"
-description = "small live bait with strong rarity and quality potential."
-icon = ExtResource("2_icon")
-category = 2
-bait_tags = Array[StringName]([&"bait_rarity_4"])
-stackable = true
-max_stack = 10
-hotbar_allowed = false
diff --git a/items/catalog/worms.tres b/items/catalog/worms.tres
deleted file mode 100644
index 71bd744..0000000
--- a/items/catalog/worms.tres
+++ /dev/null
@@ -1,16 +0,0 @@
-[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://items/item_data.gd" id="1"]
-[ext_resource type="Texture2D" path="res://items/icons/bait/64_bait_worms.png" id="2_icon"]
-
-[resource]
-script = ExtResource("1")
-item_id = &"worms"
-display_name = "worms"
-description = "basic bait that opens a small chance for rarer, higher-quality catches."
-icon = ExtResource("2_icon")
-category = 2
-bait_tags = Array[StringName]([&"worm", &"bait_rarity_0"])
-stackable = true
-max_stack = 10
-hotbar_allowed = false
diff --git a/items/fishing_rod_data.gd b/items/fishing_rod_data.gd
deleted file mode 100644
index d1c8e7c..0000000
--- a/items/fishing_rod_data.gd
+++ /dev/null
@@ -1,151 +0,0 @@
-class_name FishingRodData
-extends ItemData
-
-const FishDataType = preload("res://fish/fish_data.gd")
-const FishingContextType = preload("res://fishing/fishing_context.gd")
-
-enum TimeAffinity {
- ANY,
- DAY,
- NIGHT,
-}
-
-enum WeatherAffinity {
- ANY,
- RAIN,
- FOG,
-}
-
-const MULTIPLIER_COUNT: int = 5
-
-@export_category("Rod Shop")
-@export var shop_enabled: bool = true
-@export_range(1, 999, 1) var shop_order: int = 1
-@export_range(0, 1000000, 1) var shop_price: int = 0
-@export_range(1, 100000, 1) var unlock_level: int = 1
-
-@export_category("Rod Presentation")
-## Optional authored visual. A null scene deliberately uses the current
-## temporary rod mesh so new rod data never requires a model to function.
-@export var rod_model_scene: PackedScene
-@export var rod_model_transform: Transform3D = Transform3D.IDENTITY
-@export var rod_tip_position := Vector3(0.0, 1.1, 0.0)
-
-@export_category("Rod Affinity")
-@export var preferred_location_tags: Array[StringName] = []
-@export var preferred_collection_groups: Array[StringName] = []
-@export var time_affinity: TimeAffinity = TimeAffinity.ANY
-@export var weather_affinity: WeatherAffinity = WeatherAffinity.ANY
-@export_range(0.0, 10.0, 0.01) var affinity_weight_multiplier: float = 1.0
-@export_range(0.0, 10.0, 0.01) var undiscovered_weight_multiplier: float = 1.0
-
-@export_category("Rod Catch Modifiers")
-@export_range(-0.5, 0.5, 0.01) var weight_roll_bias: float = 0.0
-@export_range(0.05, 5.0, 0.01) var bite_time_multiplier: float = 1.0
-@export_range(0.05, 5.0, 0.01) var reel_speed_multiplier: float = 1.0
-@export_range(0.05, 5.0, 0.01) var barrier_power_multiplier: float = 1.0
-@export var rarity_weight_multipliers: Array[float] = [
- 1.0, 1.0, 1.0, 1.0, 1.0,
-]
-@export var quality_weight_multipliers: Array[float] = [
- 1.0, 1.0, 1.0, 1.0, 1.0,
-]
-@export_multiline var effect_summary: String
-@export_multiline var tradeoff: String
-
-
-func is_valid() -> bool:
- return (
- super.is_valid()
- and category == Category.ROD
- and shop_order >= 1
- and shop_price >= 0
- and unlock_level >= 1
- and rarity_weight_multipliers.size() == MULTIPLIER_COUNT
- and quality_weight_multipliers.size() == MULTIPLIER_COUNT
- and _multipliers_are_non_negative(rarity_weight_multipliers)
- and _multipliers_are_non_negative(quality_weight_multipliers)
- and affinity_weight_multiplier >= 0.0
- and undiscovered_weight_multiplier >= 0.0
- and bite_time_multiplier > 0.0
- and reel_speed_multiplier > 0.0
- and barrier_power_multiplier > 0.0
- )
-
-
-func is_shop_available() -> bool:
- return is_available() and shop_enabled and icon != null
-
-
-func get_affinity_multiplier(
- fish: FishDataType,
- context: FishingContextType,
-) -> float:
- if fish == null or context == null or not _has_authored_affinity():
- return 1.0
- if (
- not preferred_location_tags.is_empty()
- and not _has_any_match(
- preferred_location_tags,
- context.location_tags,
- )
- ):
- return 1.0
- if (
- not preferred_collection_groups.is_empty()
- and fish.collection_group not in preferred_collection_groups
- ):
- return 1.0
- match time_affinity:
- TimeAffinity.DAY:
- if context.is_night or context.is_day_night_transition:
- return 1.0
- TimeAffinity.NIGHT:
- if not context.is_night or context.is_day_night_transition:
- return 1.0
- match weather_affinity:
- WeatherAffinity.RAIN:
- if not context.is_raining:
- return 1.0
- WeatherAffinity.FOG:
- if not context.is_foggy:
- return 1.0
- return maxf(affinity_weight_multiplier, 0.0)
-
-
-func get_rarity_multiplier(rarity: int) -> float:
- if rarity < 0 or rarity >= rarity_weight_multipliers.size():
- return 1.0
- return maxf(rarity_weight_multipliers[rarity], 0.0)
-
-
-func get_quality_multiplier(quality: int) -> float:
- if quality < 0 or quality >= quality_weight_multipliers.size():
- return 1.0
- return maxf(quality_weight_multipliers[quality], 0.0)
-
-
-func _has_authored_affinity() -> bool:
- return (
- not preferred_location_tags.is_empty()
- or not preferred_collection_groups.is_empty()
- or time_affinity != TimeAffinity.ANY
- or weather_affinity != WeatherAffinity.ANY
- )
-
-
-func _has_any_match(
- expected: Array[StringName],
- actual: Array[StringName],
-) -> bool:
- for value: StringName in expected:
- if value in actual:
- return true
- return false
-
-
-func _multipliers_are_non_negative(values: Array[float]) -> bool:
- for value: float in values:
- if not is_finite(value) or value < 0.0:
- return false
- return true
diff --git a/items/fishing_rod_data.gd.uid b/items/fishing_rod_data.gd.uid
deleted file mode 100644
index 324cfd7..0000000
--- a/items/fishing_rod_data.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://car3ne3q6dhln
diff --git a/items/icons/art/art_kit.png b/items/icons/art/art_kit.png
deleted file mode 100644
index 92adfa7..0000000
Binary files a/items/icons/art/art_kit.png and /dev/null differ
diff --git a/items/icons/art/art_kit.png.import b/items/icons/art/art_kit.png.import
deleted file mode 100644
index 053fd1f..0000000
--- a/items/icons/art/art_kit.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://datu7j7tq2k3e"
-path="res://.godot/imported/art_kit.png-b54a66a446203c844f97c37241065423.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit.png"
-dest_files=["res://.godot/imported/art_kit.png-b54a66a446203c844f97c37241065423.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_eraser.png b/items/icons/art/art_kit_eraser.png
deleted file mode 100644
index 08e7691..0000000
Binary files a/items/icons/art/art_kit_eraser.png and /dev/null differ
diff --git a/items/icons/art/art_kit_eraser.png.import b/items/icons/art/art_kit_eraser.png.import
deleted file mode 100644
index a25a8d1..0000000
--- a/items/icons/art/art_kit_eraser.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cdbba3bvhuear"
-path="res://.godot/imported/art_kit_eraser.png-4df8445ba3139b8236508fc61a024158.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_eraser.png"
-dest_files=["res://.godot/imported/art_kit_eraser.png-4df8445ba3139b8236508fc61a024158.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_dark.png b/items/icons/art/art_kit_grid_dark.png
deleted file mode 100644
index ba2ec42..0000000
Binary files a/items/icons/art/art_kit_grid_dark.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_dark.png.import b/items/icons/art/art_kit_grid_dark.png.import
deleted file mode 100644
index 2e573da..0000000
--- a/items/icons/art/art_kit_grid_dark.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://ti3njllquh6r"
-path="res://.godot/imported/art_kit_grid_dark.png-a2b68e7c7a0946c1d4bbb0762a0fe05e.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_dark.png"
-dest_files=["res://.godot/imported/art_kit_grid_dark.png-a2b68e7c7a0946c1d4bbb0762a0fe05e.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_finish_dark.png b/items/icons/art/art_kit_grid_finish_dark.png
deleted file mode 100644
index 0fe0922..0000000
Binary files a/items/icons/art/art_kit_grid_finish_dark.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_finish_dark.png.import b/items/icons/art/art_kit_grid_finish_dark.png.import
deleted file mode 100644
index 1d91f6e..0000000
--- a/items/icons/art/art_kit_grid_finish_dark.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://br0d1nm0xof6g"
-path="res://.godot/imported/art_kit_grid_finish_dark.png-1658b5ecf5ce8df3dfa03b5f0e4a299c.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_finish_dark.png"
-dest_files=["res://.godot/imported/art_kit_grid_finish_dark.png-1658b5ecf5ce8df3dfa03b5f0e4a299c.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_finish_light.png b/items/icons/art/art_kit_grid_finish_light.png
deleted file mode 100644
index 20cafce..0000000
Binary files a/items/icons/art/art_kit_grid_finish_light.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_finish_light.png.import b/items/icons/art/art_kit_grid_finish_light.png.import
deleted file mode 100644
index 63d8261..0000000
--- a/items/icons/art/art_kit_grid_finish_light.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dm24dy3ebvqmv"
-path="res://.godot/imported/art_kit_grid_finish_light.png-fe16d574640fb174cb8b6ff3cfb95e5e.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_finish_light.png"
-dest_files=["res://.godot/imported/art_kit_grid_finish_light.png-fe16d574640fb174cb8b6ff3cfb95e5e.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_finish_mid.png b/items/icons/art/art_kit_grid_finish_mid.png
deleted file mode 100644
index b2ff80b..0000000
Binary files a/items/icons/art/art_kit_grid_finish_mid.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_finish_mid.png.import b/items/icons/art/art_kit_grid_finish_mid.png.import
deleted file mode 100644
index 40fd76d..0000000
--- a/items/icons/art/art_kit_grid_finish_mid.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bdfey7xm6rp7k"
-path="res://.godot/imported/art_kit_grid_finish_mid.png-9ea2ca237871e37c0ecf862375f58498.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_finish_mid.png"
-dest_files=["res://.godot/imported/art_kit_grid_finish_mid.png-9ea2ca237871e37c0ecf862375f58498.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_hidden_dark.png b/items/icons/art/art_kit_grid_hidden_dark.png
deleted file mode 100644
index e1e9e1e..0000000
Binary files a/items/icons/art/art_kit_grid_hidden_dark.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_hidden_dark.png.import b/items/icons/art/art_kit_grid_hidden_dark.png.import
deleted file mode 100644
index 06ef0d9..0000000
--- a/items/icons/art/art_kit_grid_hidden_dark.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cr1mpyrrykxhs"
-path="res://.godot/imported/art_kit_grid_hidden_dark.png-d8995a8a7bfc3f9bcd1ce726ac962ed5.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_hidden_dark.png"
-dest_files=["res://.godot/imported/art_kit_grid_hidden_dark.png-d8995a8a7bfc3f9bcd1ce726ac962ed5.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_hidden_light.png b/items/icons/art/art_kit_grid_hidden_light.png
deleted file mode 100644
index 191e852..0000000
Binary files a/items/icons/art/art_kit_grid_hidden_light.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_hidden_light.png.import b/items/icons/art/art_kit_grid_hidden_light.png.import
deleted file mode 100644
index 95d7ac3..0000000
--- a/items/icons/art/art_kit_grid_hidden_light.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cv6yicrysb0vs"
-path="res://.godot/imported/art_kit_grid_hidden_light.png-e92350d8a3e2c3e3bf286658f1ae0c41.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_hidden_light.png"
-dest_files=["res://.godot/imported/art_kit_grid_hidden_light.png-e92350d8a3e2c3e3bf286658f1ae0c41.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_hidden_mid.png b/items/icons/art/art_kit_grid_hidden_mid.png
deleted file mode 100644
index f9cbfe4..0000000
Binary files a/items/icons/art/art_kit_grid_hidden_mid.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_hidden_mid.png.import b/items/icons/art/art_kit_grid_hidden_mid.png.import
deleted file mode 100644
index 7080836..0000000
--- a/items/icons/art/art_kit_grid_hidden_mid.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://d3dcmab3w4ycy"
-path="res://.godot/imported/art_kit_grid_hidden_mid.png-ed65b27edb04579b9bd1b1eda42667db.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_hidden_mid.png"
-dest_files=["res://.godot/imported/art_kit_grid_hidden_mid.png-ed65b27edb04579b9bd1b1eda42667db.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_large_dark.png b/items/icons/art/art_kit_grid_large_dark.png
deleted file mode 100644
index 1a26888..0000000
Binary files a/items/icons/art/art_kit_grid_large_dark.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_large_dark.png.import b/items/icons/art/art_kit_grid_large_dark.png.import
deleted file mode 100644
index 799918e..0000000
--- a/items/icons/art/art_kit_grid_large_dark.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://6vqwncasappw"
-path="res://.godot/imported/art_kit_grid_large_dark.png-2327b60999e01476872d201de06f9f50.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_large_dark.png"
-dest_files=["res://.godot/imported/art_kit_grid_large_dark.png-2327b60999e01476872d201de06f9f50.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_large_light.png b/items/icons/art/art_kit_grid_large_light.png
deleted file mode 100644
index 53ed358..0000000
Binary files a/items/icons/art/art_kit_grid_large_light.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_large_light.png.import b/items/icons/art/art_kit_grid_large_light.png.import
deleted file mode 100644
index 1f8447c..0000000
--- a/items/icons/art/art_kit_grid_large_light.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b75kbv3pib0qj"
-path="res://.godot/imported/art_kit_grid_large_light.png-54205faa1c3601a058feb891cd59af61.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_large_light.png"
-dest_files=["res://.godot/imported/art_kit_grid_large_light.png-54205faa1c3601a058feb891cd59af61.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_large_mid.png b/items/icons/art/art_kit_grid_large_mid.png
deleted file mode 100644
index 8f30fce..0000000
Binary files a/items/icons/art/art_kit_grid_large_mid.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_large_mid.png.import b/items/icons/art/art_kit_grid_large_mid.png.import
deleted file mode 100644
index 436fbd3..0000000
--- a/items/icons/art/art_kit_grid_large_mid.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://eicec6lsxldy"
-path="res://.godot/imported/art_kit_grid_large_mid.png-a1bee2fda05cd0af5076e2d10d5d155d.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_large_mid.png"
-dest_files=["res://.godot/imported/art_kit_grid_large_mid.png-a1bee2fda05cd0af5076e2d10d5d155d.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_light.png b/items/icons/art/art_kit_grid_light.png
deleted file mode 100644
index 8dbc12e..0000000
Binary files a/items/icons/art/art_kit_grid_light.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_light.png.import b/items/icons/art/art_kit_grid_light.png.import
deleted file mode 100644
index 6435819..0000000
--- a/items/icons/art/art_kit_grid_light.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dh2tu3ptqe6f7"
-path="res://.godot/imported/art_kit_grid_light.png-d445eba68746a34d73bbf1744fe56777.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_light.png"
-dest_files=["res://.godot/imported/art_kit_grid_light.png-d445eba68746a34d73bbf1744fe56777.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_medium_dark.png b/items/icons/art/art_kit_grid_medium_dark.png
deleted file mode 100644
index 5487617..0000000
Binary files a/items/icons/art/art_kit_grid_medium_dark.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_medium_dark.png.import b/items/icons/art/art_kit_grid_medium_dark.png.import
deleted file mode 100644
index a9a313f..0000000
--- a/items/icons/art/art_kit_grid_medium_dark.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b8odlp8s63t84"
-path="res://.godot/imported/art_kit_grid_medium_dark.png-83db4c9a8e6d53a644fd16865cb7bd28.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_medium_dark.png"
-dest_files=["res://.godot/imported/art_kit_grid_medium_dark.png-83db4c9a8e6d53a644fd16865cb7bd28.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_medium_light.png b/items/icons/art/art_kit_grid_medium_light.png
deleted file mode 100644
index 2b7365f..0000000
Binary files a/items/icons/art/art_kit_grid_medium_light.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_medium_light.png.import b/items/icons/art/art_kit_grid_medium_light.png.import
deleted file mode 100644
index 2bec287..0000000
--- a/items/icons/art/art_kit_grid_medium_light.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b5r1wq3u63r7"
-path="res://.godot/imported/art_kit_grid_medium_light.png-136caf56e94ccc972f0fa220397c6194.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_medium_light.png"
-dest_files=["res://.godot/imported/art_kit_grid_medium_light.png-136caf56e94ccc972f0fa220397c6194.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_medium_mid.png b/items/icons/art/art_kit_grid_medium_mid.png
deleted file mode 100644
index eaa71c4..0000000
Binary files a/items/icons/art/art_kit_grid_medium_mid.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_medium_mid.png.import b/items/icons/art/art_kit_grid_medium_mid.png.import
deleted file mode 100644
index ac3d78e..0000000
--- a/items/icons/art/art_kit_grid_medium_mid.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c24yrduwv2xmg"
-path="res://.godot/imported/art_kit_grid_medium_mid.png-f3b0bb2b799a61b19f22164b6b718cb5.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_medium_mid.png"
-dest_files=["res://.godot/imported/art_kit_grid_medium_mid.png-f3b0bb2b799a61b19f22164b6b718cb5.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_mid.png b/items/icons/art/art_kit_grid_mid.png
deleted file mode 100644
index 304a551..0000000
Binary files a/items/icons/art/art_kit_grid_mid.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_mid.png.import b/items/icons/art/art_kit_grid_mid.png.import
deleted file mode 100644
index f946bfc..0000000
--- a/items/icons/art/art_kit_grid_mid.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://ddo2kmjtrke6y"
-path="res://.godot/imported/art_kit_grid_mid.png-f9c988d6d2c3fb83633590231f752436.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_mid.png"
-dest_files=["res://.godot/imported/art_kit_grid_mid.png-f9c988d6d2c3fb83633590231f752436.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_restore_dark.png b/items/icons/art/art_kit_grid_restore_dark.png
deleted file mode 100644
index 1268809..0000000
Binary files a/items/icons/art/art_kit_grid_restore_dark.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_restore_dark.png.import b/items/icons/art/art_kit_grid_restore_dark.png.import
deleted file mode 100644
index bc9936b..0000000
--- a/items/icons/art/art_kit_grid_restore_dark.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cchyevpvrcrtb"
-path="res://.godot/imported/art_kit_grid_restore_dark.png-57a9e7866f9136b53631d8c686343955.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_restore_dark.png"
-dest_files=["res://.godot/imported/art_kit_grid_restore_dark.png-57a9e7866f9136b53631d8c686343955.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_restore_light.png b/items/icons/art/art_kit_grid_restore_light.png
deleted file mode 100644
index dd7cb01..0000000
Binary files a/items/icons/art/art_kit_grid_restore_light.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_restore_light.png.import b/items/icons/art/art_kit_grid_restore_light.png.import
deleted file mode 100644
index 9672eab..0000000
--- a/items/icons/art/art_kit_grid_restore_light.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://ckxdpi5sqerao"
-path="res://.godot/imported/art_kit_grid_restore_light.png-d2f2458a90dabc79430d7ff0e0e3385d.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_restore_light.png"
-dest_files=["res://.godot/imported/art_kit_grid_restore_light.png-d2f2458a90dabc79430d7ff0e0e3385d.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_restore_mid.png b/items/icons/art/art_kit_grid_restore_mid.png
deleted file mode 100644
index 50b4d9a..0000000
Binary files a/items/icons/art/art_kit_grid_restore_mid.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_restore_mid.png.import b/items/icons/art/art_kit_grid_restore_mid.png.import
deleted file mode 100644
index b994da2..0000000
--- a/items/icons/art/art_kit_grid_restore_mid.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cjaqmwiamywp7"
-path="res://.godot/imported/art_kit_grid_restore_mid.png-c4cb540f51a2d6b7fd328f089dc3cd52.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_restore_mid.png"
-dest_files=["res://.godot/imported/art_kit_grid_restore_mid.png-c4cb540f51a2d6b7fd328f089dc3cd52.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_small_dark.png b/items/icons/art/art_kit_grid_small_dark.png
deleted file mode 100644
index 7280808..0000000
Binary files a/items/icons/art/art_kit_grid_small_dark.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_small_dark.png.import b/items/icons/art/art_kit_grid_small_dark.png.import
deleted file mode 100644
index ddcf831..0000000
--- a/items/icons/art/art_kit_grid_small_dark.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bnosm2ylhkpbp"
-path="res://.godot/imported/art_kit_grid_small_dark.png-19f946c2afe9de146a9c5277988e42eb.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_small_dark.png"
-dest_files=["res://.godot/imported/art_kit_grid_small_dark.png-19f946c2afe9de146a9c5277988e42eb.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_small_light.png b/items/icons/art/art_kit_grid_small_light.png
deleted file mode 100644
index 72a1ef2..0000000
Binary files a/items/icons/art/art_kit_grid_small_light.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_small_light.png.import b/items/icons/art/art_kit_grid_small_light.png.import
deleted file mode 100644
index 3a9e7a1..0000000
--- a/items/icons/art/art_kit_grid_small_light.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c5h70o3l0xov8"
-path="res://.godot/imported/art_kit_grid_small_light.png-04b6521c5f1923d20f59baa8674be0a5.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_small_light.png"
-dest_files=["res://.godot/imported/art_kit_grid_small_light.png-04b6521c5f1923d20f59baa8674be0a5.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_small_mid.png b/items/icons/art/art_kit_grid_small_mid.png
deleted file mode 100644
index 35b00a4..0000000
Binary files a/items/icons/art/art_kit_grid_small_mid.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_small_mid.png.import b/items/icons/art/art_kit_grid_small_mid.png.import
deleted file mode 100644
index 1ea038c..0000000
--- a/items/icons/art/art_kit_grid_small_mid.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dhevyq3kafoim"
-path="res://.godot/imported/art_kit_grid_small_mid.png-07dcaf91769645ebd9fe713ecd116ebe.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_small_mid.png"
-dest_files=["res://.godot/imported/art_kit_grid_small_mid.png-07dcaf91769645ebd9fe713ecd116ebe.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_xl_dark.png b/items/icons/art/art_kit_grid_xl_dark.png
deleted file mode 100644
index d07ad1b..0000000
Binary files a/items/icons/art/art_kit_grid_xl_dark.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_xl_dark.png.import b/items/icons/art/art_kit_grid_xl_dark.png.import
deleted file mode 100644
index 02c33b3..0000000
--- a/items/icons/art/art_kit_grid_xl_dark.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://d27djk1eme6bn"
-path="res://.godot/imported/art_kit_grid_xl_dark.png-add0fb303950020a41288746fc69713d.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_xl_dark.png"
-dest_files=["res://.godot/imported/art_kit_grid_xl_dark.png-add0fb303950020a41288746fc69713d.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_xl_light.png b/items/icons/art/art_kit_grid_xl_light.png
deleted file mode 100644
index 92e4d43..0000000
Binary files a/items/icons/art/art_kit_grid_xl_light.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_xl_light.png.import b/items/icons/art/art_kit_grid_xl_light.png.import
deleted file mode 100644
index 19d9027..0000000
--- a/items/icons/art/art_kit_grid_xl_light.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://5axhxgpgvihn"
-path="res://.godot/imported/art_kit_grid_xl_light.png-f85f9418139b6a98f711d5a065adbcbf.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_xl_light.png"
-dest_files=["res://.godot/imported/art_kit_grid_xl_light.png-f85f9418139b6a98f711d5a065adbcbf.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_grid_xl_mid.png b/items/icons/art/art_kit_grid_xl_mid.png
deleted file mode 100644
index 868f427..0000000
Binary files a/items/icons/art/art_kit_grid_xl_mid.png and /dev/null differ
diff --git a/items/icons/art/art_kit_grid_xl_mid.png.import b/items/icons/art/art_kit_grid_xl_mid.png.import
deleted file mode 100644
index 5123218..0000000
--- a/items/icons/art/art_kit_grid_xl_mid.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://sqvdn521pr00"
-path="res://.godot/imported/art_kit_grid_xl_mid.png-b3445636e6690b581c7eb543adf00733.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_grid_xl_mid.png"
-dest_files=["res://.godot/imported/art_kit_grid_xl_mid.png-b3445636e6690b581c7eb543adf00733.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_marker.png b/items/icons/art/art_kit_marker.png
deleted file mode 100644
index fd7bfb6..0000000
Binary files a/items/icons/art/art_kit_marker.png and /dev/null differ
diff --git a/items/icons/art/art_kit_marker.png.import b/items/icons/art/art_kit_marker.png.import
deleted file mode 100644
index cffece6..0000000
--- a/items/icons/art/art_kit_marker.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://b20kab6kxh7ap"
-path="res://.godot/imported/art_kit_marker.png-77e7b82481305350436ed5cca046df56.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_marker.png"
-dest_files=["res://.godot/imported/art_kit_marker.png-77e7b82481305350436ed5cca046df56.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_marker_tip.png b/items/icons/art/art_kit_marker_tip.png
deleted file mode 100644
index 7a2de39..0000000
Binary files a/items/icons/art/art_kit_marker_tip.png and /dev/null differ
diff --git a/items/icons/art/art_kit_marker_tip.png.import b/items/icons/art/art_kit_marker_tip.png.import
deleted file mode 100644
index 079288e..0000000
--- a/items/icons/art/art_kit_marker_tip.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bg3ex075neqrm"
-path="res://.godot/imported/art_kit_marker_tip.png-a2b8d12e98fcc15518b673bdbdd0e1db.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_marker_tip.png"
-dest_files=["res://.godot/imported/art_kit_marker_tip.png-a2b8d12e98fcc15518b673bdbdd0e1db.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_marker_tip_fine.png b/items/icons/art/art_kit_marker_tip_fine.png
deleted file mode 100644
index 29cc8b7..0000000
Binary files a/items/icons/art/art_kit_marker_tip_fine.png and /dev/null differ
diff --git a/items/icons/art/art_kit_marker_tip_fine.png.import b/items/icons/art/art_kit_marker_tip_fine.png.import
deleted file mode 100644
index a096332..0000000
--- a/items/icons/art/art_kit_marker_tip_fine.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dja323uypy8p2"
-path="res://.godot/imported/art_kit_marker_tip_fine.png-1f54142bba82ca92ca8381ad4e1bc2e7.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_marker_tip_fine.png"
-dest_files=["res://.godot/imported/art_kit_marker_tip_fine.png-1f54142bba82ca92ca8381ad4e1bc2e7.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_marker_tip_mid.png b/items/icons/art/art_kit_marker_tip_mid.png
deleted file mode 100644
index 74c8498..0000000
Binary files a/items/icons/art/art_kit_marker_tip_mid.png and /dev/null differ
diff --git a/items/icons/art/art_kit_marker_tip_mid.png.import b/items/icons/art/art_kit_marker_tip_mid.png.import
deleted file mode 100644
index b0e26d3..0000000
--- a/items/icons/art/art_kit_marker_tip_mid.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bbdnn6chdfekq"
-path="res://.godot/imported/art_kit_marker_tip_mid.png-cc02669fb5d11d78cd31670752d6f8b9.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_marker_tip_mid.png"
-dest_files=["res://.godot/imported/art_kit_marker_tip_mid.png-cc02669fb5d11d78cd31670752d6f8b9.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_marker_tip_thick.png b/items/icons/art/art_kit_marker_tip_thick.png
deleted file mode 100644
index f7d3c19..0000000
Binary files a/items/icons/art/art_kit_marker_tip_thick.png and /dev/null differ
diff --git a/items/icons/art/art_kit_marker_tip_thick.png.import b/items/icons/art/art_kit_marker_tip_thick.png.import
deleted file mode 100644
index b47355d..0000000
--- a/items/icons/art/art_kit_marker_tip_thick.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://638sqeuw5n3g"
-path="res://.godot/imported/art_kit_marker_tip_thick.png-dae24cedd2ec60def334af28f261d280.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_marker_tip_thick.png"
-dest_files=["res://.godot/imported/art_kit_marker_tip_thick.png-dae24cedd2ec60def334af28f261d280.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/art/art_kit_marker_tip_thin.png b/items/icons/art/art_kit_marker_tip_thin.png
deleted file mode 100644
index 54a4798..0000000
Binary files a/items/icons/art/art_kit_marker_tip_thin.png and /dev/null differ
diff --git a/items/icons/art/art_kit_marker_tip_thin.png.import b/items/icons/art/art_kit_marker_tip_thin.png.import
deleted file mode 100644
index 94ced56..0000000
--- a/items/icons/art/art_kit_marker_tip_thin.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bwagkcgl5yjqm"
-path="res://.godot/imported/art_kit_marker_tip_thin.png-d3a317db15da4d5333192c86ef3bf227.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/art/art_kit_marker_tip_thin.png"
-dest_files=["res://.godot/imported/art_kit_marker_tip_thin.png-d3a317db15da4d5333192c86ef3bf227.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/bait/64_bait_minnow.png b/items/icons/bait/64_bait_minnow.png
deleted file mode 100644
index f083d65..0000000
Binary files a/items/icons/bait/64_bait_minnow.png and /dev/null differ
diff --git a/items/icons/bait/64_bait_minnow.png.import b/items/icons/bait/64_bait_minnow.png.import
deleted file mode 100644
index 67f57f5..0000000
--- a/items/icons/bait/64_bait_minnow.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://oc1nckfxeir0"
-path="res://.godot/imported/64_bait_minnow.png-25bb645caffd0cad87b30d6b38b80ca4.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/bait/64_bait_minnow.png"
-dest_files=["res://.godot/imported/64_bait_minnow.png-25bb645caffd0cad87b30d6b38b80ca4.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/bait/64_bait_roe_luminous.png b/items/icons/bait/64_bait_roe_luminous.png
deleted file mode 100644
index dc89fbd..0000000
Binary files a/items/icons/bait/64_bait_roe_luminous.png and /dev/null differ
diff --git a/items/icons/bait/64_bait_roe_luminous.png.import b/items/icons/bait/64_bait_roe_luminous.png.import
deleted file mode 100644
index b0a78d0..0000000
--- a/items/icons/bait/64_bait_roe_luminous.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bnqji6j6nigjq"
-path="res://.godot/imported/64_bait_roe_luminous.png-cfa7eaf935630a20e2e8111f803c7741.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/bait/64_bait_roe_luminous.png"
-dest_files=["res://.godot/imported/64_bait_roe_luminous.png-cfa7eaf935630a20e2e8111f803c7741.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/bait/64_bait_sardine.png b/items/icons/bait/64_bait_sardine.png
deleted file mode 100644
index 9161ba5..0000000
Binary files a/items/icons/bait/64_bait_sardine.png and /dev/null differ
diff --git a/items/icons/bait/64_bait_sardine.png.import b/items/icons/bait/64_bait_sardine.png.import
deleted file mode 100644
index 03f94bf..0000000
--- a/items/icons/bait/64_bait_sardine.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://ddbyrbcbm40i4"
-path="res://.godot/imported/64_bait_sardine.png-28b5d49b506c3007ebf15dcc8f198a94.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/bait/64_bait_sardine.png"
-dest_files=["res://.godot/imported/64_bait_sardine.png-28b5d49b506c3007ebf15dcc8f198a94.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/bait/64_bait_shrimp.png b/items/icons/bait/64_bait_shrimp.png
deleted file mode 100644
index 8a20783..0000000
Binary files a/items/icons/bait/64_bait_shrimp.png and /dev/null differ
diff --git a/items/icons/bait/64_bait_shrimp.png.import b/items/icons/bait/64_bait_shrimp.png.import
deleted file mode 100644
index 8c65f4a..0000000
--- a/items/icons/bait/64_bait_shrimp.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bpq47xg8tv6th"
-path="res://.godot/imported/64_bait_shrimp.png-f68293fc8a81e803d5eeb1f268e4f3d3.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/bait/64_bait_shrimp.png"
-dest_files=["res://.godot/imported/64_bait_shrimp.png-f68293fc8a81e803d5eeb1f268e4f3d3.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/bait/64_bait_snail.png b/items/icons/bait/64_bait_snail.png
deleted file mode 100644
index 39e6c16..0000000
Binary files a/items/icons/bait/64_bait_snail.png and /dev/null differ
diff --git a/items/icons/bait/64_bait_snail.png.import b/items/icons/bait/64_bait_snail.png.import
deleted file mode 100644
index de8d419..0000000
--- a/items/icons/bait/64_bait_snail.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://dihevd23dh5x3"
-path="res://.godot/imported/64_bait_snail.png-9a7a0c9f93be442a8a1d685a5324e3f6.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/bait/64_bait_snail.png"
-dest_files=["res://.godot/imported/64_bait_snail.png-9a7a0c9f93be442a8a1d685a5324e3f6.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/bait/64_bait_squid.png b/items/icons/bait/64_bait_squid.png
deleted file mode 100644
index 57e2b64..0000000
Binary files a/items/icons/bait/64_bait_squid.png and /dev/null differ
diff --git a/items/icons/bait/64_bait_squid.png.import b/items/icons/bait/64_bait_squid.png.import
deleted file mode 100644
index 41bf2b4..0000000
--- a/items/icons/bait/64_bait_squid.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://kf3aglrd1en3"
-path="res://.godot/imported/64_bait_squid.png-6ec6c9776ffcae93fd2fa312c8a9013d.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/bait/64_bait_squid.png"
-dest_files=["res://.godot/imported/64_bait_squid.png-6ec6c9776ffcae93fd2fa312c8a9013d.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/bait/64_bait_worms.png b/items/icons/bait/64_bait_worms.png
deleted file mode 100644
index 51cb396..0000000
Binary files a/items/icons/bait/64_bait_worms.png and /dev/null differ
diff --git a/items/icons/bait/64_bait_worms.png.import b/items/icons/bait/64_bait_worms.png.import
deleted file mode 100644
index 5fbd6cd..0000000
--- a/items/icons/bait/64_bait_worms.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bn6815x0rrspw"
-path="res://.godot/imported/64_bait_worms.png-c9f31d77527e73888fc56963d4e78e30.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/bait/64_bait_worms.png"
-dest_files=["res://.godot/imported/64_bait_worms.png-c9f31d77527e73888fc56963d4e78e30.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/consumables/64_consumable_coffee.png b/items/icons/consumables/64_consumable_coffee.png
deleted file mode 100644
index 3282a17..0000000
Binary files a/items/icons/consumables/64_consumable_coffee.png and /dev/null differ
diff --git a/items/icons/consumables/64_consumable_coffee.png.import b/items/icons/consumables/64_consumable_coffee.png.import
deleted file mode 100644
index 0d9a595..0000000
--- a/items/icons/consumables/64_consumable_coffee.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://be3j2ahyt3cpe"
-path="res://.godot/imported/64_consumable_coffee.png-ba557079916477db3de4b894fdb65382.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/consumables/64_consumable_coffee.png"
-dest_files=["res://.godot/imported/64_consumable_coffee.png-ba557079916477db3de4b894fdb65382.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/consumables/64_consumable_coffee_plus.png b/items/icons/consumables/64_consumable_coffee_plus.png
deleted file mode 100644
index 61b3849..0000000
Binary files a/items/icons/consumables/64_consumable_coffee_plus.png and /dev/null differ
diff --git a/items/icons/consumables/64_consumable_coffee_plus.png.import b/items/icons/consumables/64_consumable_coffee_plus.png.import
deleted file mode 100644
index 5e117c2..0000000
--- a/items/icons/consumables/64_consumable_coffee_plus.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cxw5la6tr67di"
-path="res://.godot/imported/64_consumable_coffee_plus.png-512713373bb8b17a715adef67a378d07.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/consumables/64_consumable_coffee_plus.png"
-dest_files=["res://.godot/imported/64_consumable_coffee_plus.png-512713373bb8b17a715adef67a378d07.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/consumables/64_consumable_energybar.png b/items/icons/consumables/64_consumable_energybar.png
deleted file mode 100644
index 1657a6f..0000000
Binary files a/items/icons/consumables/64_consumable_energybar.png and /dev/null differ
diff --git a/items/icons/consumables/64_consumable_energybar.png.import b/items/icons/consumables/64_consumable_energybar.png.import
deleted file mode 100644
index d68f7f3..0000000
--- a/items/icons/consumables/64_consumable_energybar.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://deqccg83oy4hw"
-path="res://.godot/imported/64_consumable_energybar.png-8ac97efdf3bd5f681012ff6ea4dcdf16.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/consumables/64_consumable_energybar.png"
-dest_files=["res://.godot/imported/64_consumable_energybar.png-8ac97efdf3bd5f681012ff6ea4dcdf16.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/consumables/64_consumable_energydrink_redfish.png b/items/icons/consumables/64_consumable_energydrink_redfish.png
deleted file mode 100644
index 92a55dc..0000000
Binary files a/items/icons/consumables/64_consumable_energydrink_redfish.png and /dev/null differ
diff --git a/items/icons/consumables/64_consumable_energydrink_redfish.png.import b/items/icons/consumables/64_consumable_energydrink_redfish.png.import
deleted file mode 100644
index 43d9867..0000000
--- a/items/icons/consumables/64_consumable_energydrink_redfish.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://d1jsfo8fy2cwx"
-path="res://.godot/imported/64_consumable_energydrink_redfish.png-c06a52f92093d918ce0318a12e99e46c.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/consumables/64_consumable_energydrink_redfish.png"
-dest_files=["res://.godot/imported/64_consumable_energydrink_redfish.png-c06a52f92093d918ce0318a12e99e46c.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/consumables/64_consumable_fish.png b/items/icons/consumables/64_consumable_fish.png
deleted file mode 100644
index 7b408ec..0000000
Binary files a/items/icons/consumables/64_consumable_fish.png and /dev/null differ
diff --git a/items/icons/consumables/64_consumable_fish.png.import b/items/icons/consumables/64_consumable_fish.png.import
deleted file mode 100644
index 6a685fc..0000000
--- a/items/icons/consumables/64_consumable_fish.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://m0hbd148mkit"
-path="res://.godot/imported/64_consumable_fish.png-a8d0ad1847950c457e505029adcac628.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/consumables/64_consumable_fish.png"
-dest_files=["res://.godot/imported/64_consumable_fish.png-a8d0ad1847950c457e505029adcac628.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/consumables/64_consumable_fishfinder.png b/items/icons/consumables/64_consumable_fishfinder.png
deleted file mode 100644
index 5c92ce1..0000000
Binary files a/items/icons/consumables/64_consumable_fishfinder.png and /dev/null differ
diff --git a/items/icons/consumables/64_consumable_fishfinder.png.import b/items/icons/consumables/64_consumable_fishfinder.png.import
deleted file mode 100644
index 1e4d3a1..0000000
--- a/items/icons/consumables/64_consumable_fishfinder.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://keij7qvi06oe"
-path="res://.godot/imported/64_consumable_fishfinder.png-b07857ff1ce529c6f69ccae5892d747a.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/consumables/64_consumable_fishfinder.png"
-dest_files=["res://.godot/imported/64_consumable_fishfinder.png-b07857ff1ce529c6f69ccae5892d747a.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/equipment/64_rod_base_thick.png b/items/icons/equipment/64_rod_base_thick.png
deleted file mode 100644
index fb56ec2..0000000
Binary files a/items/icons/equipment/64_rod_base_thick.png and /dev/null differ
diff --git a/items/icons/equipment/64_rod_base_thick.png.import b/items/icons/equipment/64_rod_base_thick.png.import
deleted file mode 100644
index c8594f3..0000000
--- a/items/icons/equipment/64_rod_base_thick.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://02t4tyyt0vwi"
-path="res://.godot/imported/64_rod_base_thick.png-60188964cb71ab30683aa983d1300765.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/equipment/64_rod_base_thick.png"
-dest_files=["res://.godot/imported/64_rod_base_thick.png-60188964cb71ab30683aa983d1300765.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/equipment/net.png b/items/icons/equipment/net.png
deleted file mode 100644
index 522dd65..0000000
Binary files a/items/icons/equipment/net.png and /dev/null differ
diff --git a/items/icons/equipment/net.png.import b/items/icons/equipment/net.png.import
deleted file mode 100644
index e32d295..0000000
--- a/items/icons/equipment/net.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://c026rqygllmst"
-path="res://.godot/imported/net.png-695e6b1511b4d1d2f6c885df7ccf0412.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/equipment/net.png"
-dest_files=["res://.godot/imported/net.png-695e6b1511b4d1d2f6c885df7ccf0412.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/equipment/shovel.png b/items/icons/equipment/shovel.png
deleted file mode 100644
index 089f30c..0000000
Binary files a/items/icons/equipment/shovel.png and /dev/null differ
diff --git a/items/icons/equipment/shovel.png.import b/items/icons/equipment/shovel.png.import
deleted file mode 100644
index 093a093..0000000
--- a/items/icons/equipment/shovel.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://cabeaw2e0glu"
-path="res://.godot/imported/shovel.png-cdfb898e33414742110b8f36a05aceb3.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/equipment/shovel.png"
-dest_files=["res://.godot/imported/shovel.png-cdfb898e33414742110b8f36a05aceb3.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/equipment/temp_net.png b/items/icons/equipment/temp_net.png
deleted file mode 100644
index 56ba4de..0000000
Binary files a/items/icons/equipment/temp_net.png and /dev/null differ
diff --git a/items/icons/equipment/temp_net.png.import b/items/icons/equipment/temp_net.png.import
deleted file mode 100644
index 698ae2b..0000000
--- a/items/icons/equipment/temp_net.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bokpob3mu1n8o"
-path="res://.godot/imported/temp_net.png-f80e317caaf9c9bd6739a7bb5e49e7b6.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/equipment/temp_net.png"
-dest_files=["res://.godot/imported/temp_net.png-f80e317caaf9c9bd6739a7bb5e49e7b6.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/icons/placeholder/basic_fishing_rod.svg b/items/icons/placeholder/basic_fishing_rod.svg
new file mode 100644
index 0000000..ee4a812
--- /dev/null
+++ b/items/icons/placeholder/basic_fishing_rod.svg
@@ -0,0 +1,9 @@
+
diff --git a/art/exported/characters/patterns/fox/ears_pointy_short_fox.png.import b/items/icons/placeholder/basic_fishing_rod.svg.import
similarity index 66%
rename from art/exported/characters/patterns/fox/ears_pointy_short_fox.png.import
rename to items/icons/placeholder/basic_fishing_rod.svg.import
index 70910ef..e2aef7d 100644
--- a/art/exported/characters/patterns/fox/ears_pointy_short_fox.png.import
+++ b/items/icons/placeholder/basic_fishing_rod.svg.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://bawjitj68qe5s"
-path="res://.godot/imported/ears_pointy_short_fox.png-a42bd3e51180136284f946a738c44a1d.ctex"
+uid="uid://c6krnuo5lp861"
+path="res://.godot/imported/basic_fishing_rod.svg-bc9101c1a69623deb55f1b3189ae4567.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://art/exported/characters/patterns/fox/ears_pointy_short_fox.png"
-dest_files=["res://.godot/imported/ears_pointy_short_fox.png-a42bd3e51180136284f946a738c44a1d.ctex"]
+source_file="res://items/icons/placeholder/basic_fishing_rod.svg"
+dest_files=["res://.godot/imported/basic_fishing_rod.svg-bc9101c1a69623deb55f1b3189ae4567.ctex"]
[params]
@@ -38,3 +38,6 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/items/icons/placeholder/coffee.svg b/items/icons/placeholder/coffee.svg
new file mode 100644
index 0000000..d30c96a
--- /dev/null
+++ b/items/icons/placeholder/coffee.svg
@@ -0,0 +1,5 @@
+
diff --git a/art/exported/characters/patterns/bengal/body_main_bengal.png.import b/items/icons/placeholder/coffee.svg.import
similarity index 67%
rename from art/exported/characters/patterns/bengal/body_main_bengal.png.import
rename to items/icons/placeholder/coffee.svg.import
index 389ec4f..f8cbaa9 100644
--- a/art/exported/characters/patterns/bengal/body_main_bengal.png.import
+++ b/items/icons/placeholder/coffee.svg.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://cn71sswmj0tue"
-path="res://.godot/imported/body_main_bengal.png-59815350b807220c52317e683582847e.ctex"
+uid="uid://ddx3pbgh2ele5"
+path="res://.godot/imported/coffee.svg-aecf16cc8c38e5e0f432cd5395949bcf.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://art/exported/characters/patterns/bengal/body_main_bengal.png"
-dest_files=["res://.godot/imported/body_main_bengal.png-59815350b807220c52317e683582847e.ctex"]
+source_file="res://items/icons/placeholder/coffee.svg"
+dest_files=["res://.godot/imported/coffee.svg-aecf16cc8c38e5e0f432cd5395949bcf.ctex"]
[params]
@@ -38,3 +38,6 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/items/icons/placeholder/cooler_expansion.svg b/items/icons/placeholder/cooler_expansion.svg
new file mode 100644
index 0000000..105bff7
--- /dev/null
+++ b/items/icons/placeholder/cooler_expansion.svg
@@ -0,0 +1,5 @@
+
diff --git a/art/exported/system_icons/straywild_1024.png.import b/items/icons/placeholder/cooler_expansion.svg.import
similarity index 65%
rename from art/exported/system_icons/straywild_1024.png.import
rename to items/icons/placeholder/cooler_expansion.svg.import
index dea156a..fbcc85c 100644
--- a/art/exported/system_icons/straywild_1024.png.import
+++ b/items/icons/placeholder/cooler_expansion.svg.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://bkh5hw8wwu8pc"
-path="res://.godot/imported/straywild_1024.png-b9b5c159c9fcecc21b04324370d4522f.ctex"
+uid="uid://d115jiqughf40"
+path="res://.godot/imported/cooler_expansion.svg-a3d63bc21ba230007f1039372b518c8f.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://art/exported/system_icons/straywild_1024.png"
-dest_files=["res://.godot/imported/straywild_1024.png-b9b5c159c9fcecc21b04324370d4522f.ctex"]
+source_file="res://items/icons/placeholder/cooler_expansion.svg"
+dest_files=["res://.godot/imported/cooler_expansion.svg-a3d63bc21ba230007f1039372b518c8f.ctex"]
[params]
@@ -38,3 +38,6 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/items/icons/placeholder/energy_drink.svg b/items/icons/placeholder/energy_drink.svg
new file mode 100644
index 0000000..da04ac7
--- /dev/null
+++ b/items/icons/placeholder/energy_drink.svg
@@ -0,0 +1,4 @@
+
diff --git a/items/icons/placeholder/energy_drink.svg.import b/items/icons/placeholder/energy_drink.svg.import
new file mode 100644
index 0000000..0e79f6a
--- /dev/null
+++ b/items/icons/placeholder/energy_drink.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d3elrkttv1f1h"
+path="res://.godot/imported/energy_drink.svg-8d7bc90bd15610c7852e4cf522a5b1ca.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://items/icons/placeholder/energy_drink.svg"
+dest_files=["res://.godot/imported/energy_drink.svg-8d7bc90bd15610c7852e4cf522a5b1ca.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/items/icons/placeholder/fish_coin.svg b/items/icons/placeholder/fish_coin.svg
new file mode 100644
index 0000000..ac4da92
--- /dev/null
+++ b/items/icons/placeholder/fish_coin.svg
@@ -0,0 +1,6 @@
+
diff --git a/art/exported/characters/patterns/calico/body_main_calico.png.import b/items/icons/placeholder/fish_coin.svg.import
similarity index 66%
rename from art/exported/characters/patterns/calico/body_main_calico.png.import
rename to items/icons/placeholder/fish_coin.svg.import
index 25f60c0..1dca3c6 100644
--- a/art/exported/characters/patterns/calico/body_main_calico.png.import
+++ b/items/icons/placeholder/fish_coin.svg.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://m1ujlrpjghq2"
-path="res://.godot/imported/body_main_calico.png-c5644e883b1aaedc3c3114ce40eaae02.ctex"
+uid="uid://trqvs4dgd4rt"
+path="res://.godot/imported/fish_coin.svg-9d00b5918d94a555938a84ee65e06386.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://art/exported/characters/patterns/calico/body_main_calico.png"
-dest_files=["res://.godot/imported/body_main_calico.png-c5644e883b1aaedc3c3114ce40eaae02.ctex"]
+source_file="res://items/icons/placeholder/fish_coin.svg"
+dest_files=["res://.godot/imported/fish_coin.svg-9d00b5918d94a555938a84ee65e06386.ctex"]
[params]
@@ -38,3 +38,6 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/items/icons/placeholder/fish_finder.svg b/items/icons/placeholder/fish_finder.svg
new file mode 100644
index 0000000..7bbc0b1
--- /dev/null
+++ b/items/icons/placeholder/fish_finder.svg
@@ -0,0 +1,6 @@
+
diff --git a/items/icons/placeholder/fish_finder.svg.import b/items/icons/placeholder/fish_finder.svg.import
new file mode 100644
index 0000000..df63a31
--- /dev/null
+++ b/items/icons/placeholder/fish_finder.svg.import
@@ -0,0 +1,43 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cxsaix6vp7t6u"
+path="res://.godot/imported/fish_finder.svg-33ab7d731566e03c2f84e185d3503b7d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://items/icons/placeholder/fish_finder.svg"
+dest_files=["res://.godot/imported/fish_finder.svg-33ab7d731566e03c2f84e185d3503b7d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/uastc_level=0
+compress/rdo_quality_loss=0.0
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/channel_remap/red=0
+process/channel_remap/green=1
+process/channel_remap/blue=2
+process/channel_remap/alpha=3
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/items/icons/placeholder/reel_speed_upgrade.svg b/items/icons/placeholder/reel_speed_upgrade.svg
new file mode 100644
index 0000000..a92d9fb
--- /dev/null
+++ b/items/icons/placeholder/reel_speed_upgrade.svg
@@ -0,0 +1,12 @@
+
diff --git a/ui/icons/main_menu/new_game.png.import b/items/icons/placeholder/reel_speed_upgrade.svg.import
similarity index 70%
rename from ui/icons/main_menu/new_game.png.import
rename to items/icons/placeholder/reel_speed_upgrade.svg.import
index 8f78965..211c08f 100644
--- a/ui/icons/main_menu/new_game.png.import
+++ b/items/icons/placeholder/reel_speed_upgrade.svg.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://s4g23wyojpq7"
-path="res://.godot/imported/new_game.png-5da99ab1028706b5372a3db83952bc63.ctex"
+uid="uid://c2hal1gkmnuqa"
+path="res://.godot/imported/reel_speed_upgrade.svg-6bce0cfdbf18895938d0bcb4850467c8.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://ui/icons/main_menu/new_game.png"
-dest_files=["res://.godot/imported/new_game.png-5da99ab1028706b5372a3db83952bc63.ctex"]
+source_file="res://items/icons/placeholder/reel_speed_upgrade.svg"
+dest_files=["res://.godot/imported/reel_speed_upgrade.svg-6bce0cfdbf18895938d0bcb4850467c8.ctex"]
[params]
@@ -38,3 +38,6 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/items/icons/placeholder/snack.svg b/items/icons/placeholder/snack.svg
new file mode 100644
index 0000000..edff1ec
--- /dev/null
+++ b/items/icons/placeholder/snack.svg
@@ -0,0 +1,5 @@
+
diff --git a/art/exported/characters/patterns/bengal/head_round_bengal.png.import b/items/icons/placeholder/snack.svg.import
similarity index 67%
rename from art/exported/characters/patterns/bengal/head_round_bengal.png.import
rename to items/icons/placeholder/snack.svg.import
index aa5113b..5ad06b4 100644
--- a/art/exported/characters/patterns/bengal/head_round_bengal.png.import
+++ b/items/icons/placeholder/snack.svg.import
@@ -2,16 +2,16 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://rv36ws5wuakb"
-path="res://.godot/imported/head_round_bengal.png-dfa502a4514c1605f5c8aff839dd8765.ctex"
+uid="uid://i611n7iwr8i6"
+path="res://.godot/imported/snack.svg-dfbab25d47c1c79c46c1d7fcf9b21cfe.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://art/exported/characters/patterns/bengal/head_round_bengal.png"
-dest_files=["res://.godot/imported/head_round_bengal.png-dfa502a4514c1605f5c8aff839dd8765.ctex"]
+source_file="res://items/icons/placeholder/snack.svg"
+dest_files=["res://.godot/imported/snack.svg-dfbab25d47c1c79c46c1d7fcf9b21cfe.ctex"]
[params]
@@ -38,3 +38,6 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/items/icons/shop/32_currency.png b/items/icons/shop/32_currency.png
deleted file mode 100644
index de2a80c..0000000
Binary files a/items/icons/shop/32_currency.png and /dev/null differ
diff --git a/items/icons/shop/32_currency.png.import b/items/icons/shop/32_currency.png.import
deleted file mode 100644
index 1614c30..0000000
--- a/items/icons/shop/32_currency.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://bf3jlmqhmmuge"
-path="res://.godot/imported/32_currency.png-c07c9577bae5b0fbbae9513440339b6f.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://items/icons/shop/32_currency.png"
-dest_files=["res://.godot/imported/32_currency.png-c07c9577bae5b0fbbae9513440339b6f.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/items/item_catalog.gd b/items/item_catalog.gd
index 511604b..b4245a0 100644
--- a/items/item_catalog.gd
+++ b/items/item_catalog.gd
@@ -28,16 +28,3 @@ func get_valid_items() -> Array[ItemDataType]:
seen[item.item_id] = true
result.append(item)
return result
-
-
-func get_available_item_by_id(item_id: StringName) -> ItemDataType:
- var item: ItemDataType = get_item_by_id(item_id)
- return item if item != null and item.is_available() else null
-
-
-func get_available_items() -> Array[ItemDataType]:
- var result: Array[ItemDataType] = []
- for item: ItemDataType in get_valid_items():
- if item.active:
- result.append(item)
- return result
diff --git a/items/item_data.gd b/items/item_data.gd
index 72ac0d6..ebcca91 100644
--- a/items/item_data.gd
+++ b/items/item_data.gd
@@ -9,20 +9,12 @@ enum Category {
UTILITY,
QUEST,
COSMETIC,
- LURE,
}
@export var item_id: StringName
@export var display_name: String
@export_multiline var description: String
-@export_category("Developer Catalog")
-## Inactive items remain resolvable for existing saves, but cannot be newly
-## acquired, equipped, used, or presented in shops.
-@export var active: bool = true
-@export_category("Item")
@export var category: Category = Category.UTILITY
-@export var bait_tags: Array[StringName] = []
-@export var lure_effects: Array[StringName] = []
@export var icon: Texture2D
@export var stackable: bool = false
@export_range(1, 999, 1) var max_stack: int = 1
@@ -40,17 +32,5 @@ func is_valid() -> bool:
)
-func is_available() -> bool:
- return active and is_valid()
-
-
func get_category_name() -> String:
return Category.keys()[category].to_lower()
-
-
-func is_bait() -> bool:
- return category == Category.BAIT and not bait_tags.is_empty()
-
-
-func is_lure() -> bool:
- return category == Category.LURE and equippable
diff --git a/items/owned_item.gd b/items/owned_item.gd
index ade8cd1..ead5b34 100644
--- a/items/owned_item.gd
+++ b/items/owned_item.gd
@@ -1,20 +1,12 @@
class_name OwnedItem
extends Resource
-const MAX_STORAGE_SLOT_INDEX: int = 254
-
@export var item_id: StringName
@export var quantity: int = 1
-@export var storage_slot: int = -1
func is_valid() -> bool:
- return (
- not item_id.is_empty()
- and quantity > 0
- and storage_slot >= -1
- and storage_slot <= MAX_STORAGE_SLOT_INDEX
- )
+ return not item_id.is_empty() and quantity > 0
func duplicate_record():
@@ -25,5 +17,4 @@ func to_save_dict() -> Dictionary:
return {
"item_id": String(item_id),
"quantity": quantity,
- "storage_slot": storage_slot,
}
diff --git a/jobs/job_catalog.gd b/jobs/job_catalog.gd
deleted file mode 100644
index eb3ea02..0000000
--- a/jobs/job_catalog.gd
+++ /dev/null
@@ -1,576 +0,0 @@
-class_name JobCatalog
-extends RefCounted
-
-const FishDataType = preload("res://fish/fish_data.gd")
-
-enum Kind {
- CATCH_TOTAL,
- CATCH_WATER,
- SELL_TOTAL,
- CATCH_QUALITY,
- CATCH_PHASE,
- CATCH_WEATHER,
- CATCH_SPECIES,
- REACH_LEVEL,
- DISCOVER_SPECIES,
- MASTER_QUALITIES,
- CATCH_CREATURE_GROUP,
- DISCOVER_CREATURE_GROUP,
- MASTER_CREATURE_GROUP,
-}
-
-const DAILY_JOB_COUNT: int = 4
-const WEATHER_SEGMENT_HOURS: float = (
- WorldWeatherService.DAILY_PLAN_SEGMENT_HOURS
-)
-const WEATHER_SEGMENT_COUNT: int = (
- WorldWeatherService.DAILY_PLAN_SEGMENT_COUNT
-)
-const LEGACY_WEATHER_SEGMENT_HOURS: float = 2.0
-const LEGACY_WEATHER_SEGMENT_COUNT: int = 12
-const MAX_JOB_REWARD_COINS: int = 100000
-const MAX_JOB_REWARD_EXPERIENCE: int = 10000
-
-
-static func generate_daily_jobs(
- plan_id: String,
- candidates: Array[FishDataType],
- allow_weather_jobs: bool = true,
-) -> Array[Dictionary]:
- var rng := RandomNumberGenerator.new()
- rng.seed = plan_id.hash()
- var jobs: Array[Dictionary] = [
- _job(
- "fresh_catches",
- "fresh catch delivery",
- "catch 5 fresh water fish",
- Kind.CATCH_WATER,
- 5,
- 35,
- 50,
- {"water_type": int(WaterType.Type.FRESH_WATER)},
- ),
- _job(
- "sell_fish",
- "market supply",
- "sell 5 fish",
- Kind.SELL_TOTAL,
- 5,
- 40,
- 50,
- ),
- ]
- var optional: Array[Dictionary] = [
- _job(
- "quality_catch",
- "quality check",
- "catch an impressive fish or better",
- Kind.CATCH_QUALITY,
- 1,
- 55,
- 75,
- {"minimum_quality": int(FishQuality.Tier.IMPRESSIVE)},
- ),
- _job(
- "night_catch",
- "night shift",
- "catch 3 fish at night",
- Kind.CATCH_PHASE,
- 3,
- 35,
- 50,
- {"phase": int(WorldTimeService.Phase.NIGHT)},
- ),
- ]
- if allow_weather_jobs:
- optional.append(_job(
- "fog_catch",
- "through the fog",
- "catch 2 fish in foggy weather",
- Kind.CATCH_WEATHER,
- 2,
- 65,
- 75,
- {"weather": int(WorldWeatherService.Weather.FOGGY)},
- ))
- var species: Array[FishDataType] = []
- for fish: FishDataType in candidates:
- if fish != null and fish.is_fishable():
- species.append(fish)
- if not species.is_empty():
- species.sort_custom(
- func(a: FishDataType, b: FishDataType) -> bool:
- return String(a.id) < String(b.id)
- )
- var species_index: int = rng.randi_range(0, species.size() - 1)
- var selected: FishDataType = species[species_index]
- optional.append(_job(
- "species_%s" % String(selected.id),
- "specific request",
- "catch 3 %s" % selected.display_name.to_lower(),
- Kind.CATCH_SPECIES,
- 3,
- 45,
- 50,
- {"fish_id": String(selected.id)},
- ))
- if _has_creature_group(candidates, FishDataType.CreatureGroup.INSECT):
- optional.append(_job(
- "insect_roundup",
- "bug hunt",
- "catch 3 insects",
- Kind.CATCH_CREATURE_GROUP,
- 3,
- 40,
- 50,
- {"creature_group": int(FishDataType.CreatureGroup.INSECT)},
- ))
- if _has_creature_group(candidates, FishDataType.CreatureGroup.SHELLFISH):
- optional.append(_job(
- "shellfish_search",
- "shore patrol",
- "catch 3 shellfish",
- Kind.CATCH_CREATURE_GROUP,
- 3,
- 45,
- 50,
- {"creature_group": int(FishDataType.CreatureGroup.SHELLFISH)},
- ))
- _shuffle(optional, rng)
- while jobs.size() < DAILY_JOB_COUNT and not optional.is_empty():
- jobs.append(optional.pop_back())
- return jobs
-
-
-static func generate_weather_schedule(
- plan_id: String,
- jobs: Array[Dictionary],
- anchor_index: int = 0,
-) -> Array[Dictionary]:
- anchor_index = clampi(anchor_index, 0, WEATHER_SEGMENT_COUNT - 1)
- var required: Array[int] = []
- for job: Dictionary in jobs:
- if int(job.get("kind", -1)) != Kind.CATCH_WEATHER:
- continue
- var weather: int = int(job.get("weather", -1))
- if WorldWeatherService.is_valid_weather(weather) and weather not in required:
- required.append(weather)
- var rng := RandomNumberGenerator.new()
- rng.seed = plan_id.hash() ^ 0x57454154
- var weather_values: Array[int] = []
- for index: int in WEATHER_SEGMENT_COUNT:
- var roll: float = rng.randf()
- var weather: int = int(WorldWeatherService.Weather.SUNNY)
- if roll >= 0.42 and roll < 0.70:
- weather = int(WorldWeatherService.Weather.CLOUDY)
- elif roll >= 0.70 and roll < 0.88:
- weather = int(WorldWeatherService.Weather.RAINY)
- elif roll >= 0.88:
- weather = int(WorldWeatherService.Weather.FOGGY)
- weather_values.append(weather)
- for requirement_index: int in required.size():
- var early_index: int = mini(
- anchor_index + requirement_index,
- WEATHER_SEGMENT_COUNT - 2,
- )
- var remaining_segments: int = WEATHER_SEGMENT_COUNT - early_index
- var later_index: int = mini(
- early_index + maxi(
- 1, floori(float(remaining_segments) / 2.0)
- ),
- WEATHER_SEGMENT_COUNT - 1,
- )
- weather_values[early_index] = required[requirement_index]
- weather_values[later_index] = required[requirement_index]
- var schedule: Array[Dictionary] = []
- for index: int in WEATHER_SEGMENT_COUNT:
- schedule.append({
- "start_hour": fposmod(
- WorldTimeService.DAY_START_HOUR
- + float(index) * WEATHER_SEGMENT_HOURS,
- WorldTimeService.HOURS_PER_DAY,
- ),
- "weather": weather_values[index],
- })
- return schedule
-
-
-static func lifetime_chains(
- registered_species_count: int,
- registered_insect_count: int = 0,
- registered_shellfish_count: int = 0,
-) -> Array[Dictionary]:
- var chains: Array[Dictionary] = [
- _chain("catch", Kind.CATCH_TOTAL, [100, 1000, 5000], [
- [150, 250], [700, 1000], [2500, 3000],
- ]),
- _chain("sell", Kind.SELL_TOTAL, [50, 500, 2500], [
- [125, 200], [600, 800], [2200, 2400],
- ]),
- _chain("level", Kind.REACH_LEVEL, [5, 10, 25, 50], [
- [100, 150], [250, 350], [1000, 1200], [3000, 3500],
- ]),
- ]
- if registered_species_count > 0:
- chains.append(_chain(
- "discover",
- Kind.DISCOVER_SPECIES,
- [registered_species_count],
- [[500, 750]],
- ))
- chains.append(_chain(
- "master",
- Kind.MASTER_QUALITIES,
- [registered_species_count],
- [[1500, 2000]],
- ))
- _append_creature_lifetime_chains(
- chains,
- "insect",
- FishDataType.CreatureGroup.INSECT,
- registered_insect_count,
- )
- _append_creature_lifetime_chains(
- chains,
- "shellfish",
- FishDataType.CreatureGroup.SHELLFISH,
- registered_shellfish_count,
- )
- return chains
-
-
-static func visible_lifetime_jobs(
- registered_species_count: int,
- claimed_ids: Array[String],
- registered_insect_count: int = 0,
- registered_shellfish_count: int = 0,
-) -> Array[Dictionary]:
- var visible: Array[Dictionary] = []
- for chain: Dictionary in lifetime_chains(
- registered_species_count,
- registered_insect_count,
- registered_shellfish_count,
- ):
- var targets: Array = chain.get("targets", [])
- var rewards: Array = chain.get("rewards", [])
- for tier_index: int in targets.size():
- var job_id: String = "%s_%d" % [
- str(chain.get("id", "")),
- int(targets[tier_index]),
- ]
- if job_id in claimed_ids:
- continue
- var reward: Array = rewards[tier_index]
- var extra: Dictionary = {}
- if chain.has("creature_group"):
- extra["creature_group"] = int(chain["creature_group"])
- visible.append(_job(
- job_id,
- _lifetime_title(str(chain.get("id", "")), int(targets[tier_index])),
- _lifetime_description(
- str(chain.get("id", "")), int(targets[tier_index])
- ),
- int(chain.get("kind", Kind.CATCH_TOTAL)) as Kind,
- int(targets[tier_index]),
- int(reward[0]),
- int(reward[1]),
- extra,
- ))
- break
- return visible
-
-
-static func weather_requirements(jobs: Array[Dictionary]) -> Array[int]:
- var result: Array[int] = []
- for job: Dictionary in jobs:
- if int(job.get("kind", -1)) != Kind.CATCH_WEATHER:
- continue
- var weather: int = int(job.get("weather", -1))
- if WorldWeatherService.is_valid_weather(weather) and weather not in result:
- result.append(weather)
- return result
-
-
-static func is_valid_job(value: Variant) -> bool:
- if typeof(value) != TYPE_DICTIONARY:
- return false
- var job: Dictionary = value
- var kind: int = int(job.get("kind", -1))
- if not (
- typeof(job.get("id")) == TYPE_STRING
- and not str(job["id"]).is_empty()
- and str(job["id"]).length() <= 96
- and typeof(job.get("title")) == TYPE_STRING
- and str(job["title"]).length() <= 96
- and typeof(job.get("description")) == TYPE_STRING
- and str(job["description"]).length() <= 160
- and is_bounded_integer(
- job.get("kind"), Kind.CATCH_TOTAL, Kind.MASTER_CREATURE_GROUP
- )
- and kind >= Kind.CATCH_TOTAL
- and kind <= Kind.MASTER_CREATURE_GROUP
- and is_bounded_integer(job.get("target"), 1, 1000000000)
- and is_bounded_integer(
- job.get("fish_coin"), 0, MAX_JOB_REWARD_COINS
- )
- and is_bounded_integer(
- job.get("experience"), 0, MAX_JOB_REWARD_EXPERIENCE
- )
- ):
- return false
- match kind:
- Kind.CATCH_WATER:
- var water_type: int = int(job.get("water_type", -1))
- return (
- is_bounded_integer(
- job.get("water_type"),
- WaterType.Type.FRESH_WATER,
- WaterType.Type.OTHER,
- )
- and water_type >= WaterType.Type.FRESH_WATER
- and water_type <= WaterType.Type.OTHER
- )
- Kind.CATCH_QUALITY:
- return (
- is_bounded_integer(
- job.get("minimum_quality"),
- FishQuality.Tier.BORING,
- FishQuality.Tier.SHINY,
- )
- and FishQuality.is_valid(int(job.get("minimum_quality", -1)))
- )
- Kind.CATCH_PHASE:
- var phase: int = int(job.get("phase", -1))
- return (
- is_bounded_integer(
- job.get("phase"),
- WorldTimeService.Phase.DAWN,
- WorldTimeService.Phase.NIGHT,
- )
- and phase >= WorldTimeService.Phase.DAWN
- and phase <= WorldTimeService.Phase.NIGHT
- )
- Kind.CATCH_WEATHER:
- return (
- is_bounded_integer(
- job.get("weather"),
- WorldWeatherService.Weather.SUNNY,
- WorldWeatherService.Weather.FOGGY,
- )
- and WorldWeatherService.is_valid_weather(
- int(job.get("weather", -1))
- )
- )
- Kind.CATCH_SPECIES:
- return (
- typeof(job.get("fish_id")) == TYPE_STRING
- and not str(job.get("fish_id", "")).is_empty()
- and str(job.get("fish_id", "")).length() <= 96
- )
- Kind.CATCH_CREATURE_GROUP, Kind.DISCOVER_CREATURE_GROUP, Kind.MASTER_CREATURE_GROUP:
- return is_bounded_integer(
- job.get("creature_group"),
- FishDataType.CreatureGroup.FISH,
- FishDataType.CreatureGroup.SHELLFISH,
- )
- return true
-
-
-static func is_bounded_integer(
- value: Variant,
- minimum: int,
- maximum: int,
-) -> bool:
- if typeof(value) == TYPE_INT:
- return int(value) >= minimum and int(value) <= maximum
- if typeof(value) != TYPE_FLOAT:
- return false
- var number: float = float(value)
- return (
- is_finite(number)
- and number >= float(minimum)
- and number <= float(maximum)
- and is_equal_approx(number, round(number))
- )
-
-
-static func is_valid_weather_schedule(value: Variant) -> bool:
- return WorldWeatherService.is_valid_daily_plan_schedule(value)
-
-
-static func is_valid_legacy_weather_schedule(value: Variant) -> bool:
- if typeof(value) != TYPE_ARRAY:
- return false
- var schedule: Array = value
- if schedule.size() != LEGACY_WEATHER_SEGMENT_COUNT:
- return false
- for index: int in schedule.size():
- if typeof(schedule[index]) != TYPE_DICTIONARY:
- return false
- var entry: Dictionary = schedule[index]
- var expected_hour: float = fposmod(
- WorldTimeService.DAY_START_HOUR
- + float(index) * LEGACY_WEATHER_SEGMENT_HOURS,
- WorldTimeService.HOURS_PER_DAY,
- )
- if (
- typeof(entry.get("start_hour")) not in [TYPE_FLOAT, TYPE_INT]
- or not is_equal_approx(
- float(entry.get("start_hour", -1.0)), expected_hour
- )
- or not is_bounded_integer(
- entry.get("weather"),
- WorldWeatherService.Weather.SUNNY,
- WorldWeatherService.Weather.FOGGY,
- )
- ):
- return false
- return true
-
-
-static func _job(
- id: String,
- title: String,
- description: String,
- kind: Kind,
- target: int,
- fish_coin: int,
- experience: int,
- extra: Dictionary = {},
-) -> Dictionary:
- var result: Dictionary = {
- "id": id,
- "title": title,
- "description": description,
- "kind": int(kind),
- "target": target,
- "fish_coin": fish_coin,
- "experience": experience,
- }
- result.merge(extra, true)
- return result
-
-
-static func _chain(
- id: String,
- kind: Kind,
- targets: Array[int],
- rewards: Array,
- extra: Dictionary = {},
-) -> Dictionary:
- var result: Dictionary = {
- "id": id,
- "kind": int(kind),
- "targets": targets,
- "rewards": rewards,
- }
- result.merge(extra, true)
- return result
-
-
-static func _lifetime_title(chain_id: String, target: int) -> String:
- match chain_id:
- "catch":
- return "seasoned angler"
- "sell":
- return "market regular"
- "level":
- return "reach level %d" % target
- "discover":
- return "complete the catalog"
- "master":
- return "quality master"
- "insect_catch":
- return "seasoned bug catcher"
- "insect_discover":
- return "complete the insect catalog"
- "insect_master":
- return "insect quality master"
- "shellfish_catch":
- return "seasoned beachcomber"
- "shellfish_discover":
- return "complete the shellfish catalog"
- "shellfish_master":
- return "shellfish quality master"
- return "long-term job"
-
-
-static func _lifetime_description(chain_id: String, target: int) -> String:
- match chain_id:
- "catch":
- return "catch %d fish" % target
- "sell":
- return "sell %d fish" % target
- "level":
- return "reach player level %d" % target
- "discover":
- return "discover all %d cataloged fish" % target
- "master":
- return "collect every quality of all %d fish" % target
- "insect_catch":
- return "catch %d insects" % target
- "insect_discover":
- return "discover all %d cataloged insects" % target
- "insect_master":
- return "collect every quality of all %d insects" % target
- "shellfish_catch":
- return "catch %d shellfish" % target
- "shellfish_discover":
- return "discover all %d cataloged shellfish" % target
- "shellfish_master":
- return "collect every quality of all %d shellfish" % target
- return "keep fishing"
-
-
-static func _has_creature_group(
- candidates: Array[FishDataType],
- creature_group: FishDataType.CreatureGroup,
-) -> bool:
- for fish: FishDataType in candidates:
- if (
- fish != null
- and fish.is_selectable()
- and fish.get_creature_group() == creature_group
- ):
- return true
- return false
-
-
-static func _append_creature_lifetime_chains(
- chains: Array[Dictionary],
- id_prefix: String,
- creature_group: FishDataType.CreatureGroup,
- registered_count: int,
-) -> void:
- if registered_count <= 0:
- return
- var group_data := {"creature_group": int(creature_group)}
- chains.append(_chain(
- "%s_catch" % id_prefix,
- Kind.CATCH_CREATURE_GROUP,
- [10, 50, 250],
- [[75, 100], [250, 350], [900, 1100]],
- group_data,
- ))
- chains.append(_chain(
- "%s_discover" % id_prefix,
- Kind.DISCOVER_CREATURE_GROUP,
- [registered_count],
- [[250, 350]],
- group_data,
- ))
- chains.append(_chain(
- "%s_master" % id_prefix,
- Kind.MASTER_CREATURE_GROUP,
- [registered_count],
- [[750, 1000]],
- group_data,
- ))
-
-
-static func _shuffle(values: Array[Dictionary], rng: RandomNumberGenerator) -> void:
- for index: int in range(values.size() - 1, 0, -1):
- var swap_index: int = rng.randi_range(0, index)
- var value: Dictionary = values[index]
- values[index] = values[swap_index]
- values[swap_index] = value
diff --git a/jobs/job_catalog.gd.uid b/jobs/job_catalog.gd.uid
deleted file mode 100644
index f54ab48..0000000
--- a/jobs/job_catalog.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c52vqhdties3o
diff --git a/jobs/player_job_service.gd b/jobs/player_job_service.gd
deleted file mode 100644
index 7ede866..0000000
--- a/jobs/player_job_service.gd
+++ /dev/null
@@ -1,997 +0,0 @@
-class_name PlayerJobService
-extends Node
-
-const FishCatchType = preload("res://fish/fish_catch.gd")
-const FishDataType = preload("res://fish/fish_data.gd")
-const FishPoolType = preload("res://fish/fish_pool.gd")
-
-const FORMAT_VERSION: int = 1
-const MAX_PROGRESS_VALUE: int = 1000000000
-const MAX_PENDING_REWARDS: int = 64
-const DAILY_REFRESH_HOUR: float = WorldTimeService.DAY_START_HOUR
-
-signal changed
-signal board_changed
-signal reward_claimed(title: String, fish_coin: int, experience: int)
-signal status_changed(message: String)
-
-var _wallet: PlayerWallet
-var _experience: PlayerExperience
-var _collection: CollectionLog
-var _catalog: FishPoolType
-var _world_time: WorldTimeService
-var _world_weather: WorldWeatherService
-var _session: NetworkSession
-var _save_manager: PlayerSaveManager
-var _network_fishing: NetworkFishingService
-var _network_sale: NetworkSaleService
-var _network_world_spawns: NetworkWorldSpawnService
-
-var _progression_ready: bool = false
-var _host_board: Dictionary = {}
-var _active_board: Dictionary = {}
-var _active_plan_id: String = ""
-var _daily_progress: Dictionary[String, int] = {}
-var _daily_completions: Dictionary[String, int] = {}
-var _pending_rewards: Array[Dictionary] = []
-var _lifetime_claimed: Array[String] = []
-var _total_catches: int = 0
-var _total_insects: int = 0
-var _total_shellfish: int = 0
-var _total_sold: int = 0
-var _daily_clock_hour: float = WorldTimeService.DEFAULT_START_HOUR
-
-
-func setup(
- wallet: PlayerWallet,
- experience: PlayerExperience,
- collection: CollectionLog,
- catalog: FishPoolType,
- world_time: WorldTimeService,
- world_weather: WorldWeatherService,
- session: NetworkSession,
-) -> void:
- _wallet = wallet
- _experience = experience
- _collection = collection
- _catalog = catalog
- _world_time = world_time
- _world_weather = world_weather
- _session = session
- _daily_clock_hour = _world_time.get_time_hours()
- _world_time.natural_time_advanced.connect(_on_natural_time_advanced)
- _session.state_changed.connect(_on_session_state_changed)
- _collection.collection_changed.connect(_on_current_state_changed)
- _experience.experience_changed.connect(_on_experience_changed)
-
-
-func bind_authoritative_services(
- network_fishing: NetworkFishingService,
- network_sale: NetworkSaleService,
- network_world_spawns: NetworkWorldSpawnService,
-) -> void:
- _network_fishing = network_fishing
- _network_sale = network_sale
- _network_world_spawns = network_world_spawns
- if not _network_fishing.local_catch_received.is_connected(
- _on_authoritative_catch
- ):
- _network_fishing.local_catch_received.connect(_on_authoritative_catch)
- if not _network_sale.local_sale_finished.is_connected(
- _on_authoritative_sale_finished
- ):
- _network_sale.local_sale_finished.connect(
- _on_authoritative_sale_finished
- )
- if not _network_world_spawns.local_capture_received.is_connected(
- _on_authoritative_catch
- ):
- _network_world_spawns.local_capture_received.connect(
- _on_authoritative_catch
- )
-
-
-func set_save_manager(save_manager: PlayerSaveManager) -> void:
- _save_manager = save_manager
-
-
-func begin_progression_session() -> void:
- _progression_ready = true
- if _session.is_host():
- _activate_host_board()
- changed.emit()
-
-
-func end_progression_session() -> void:
- _progression_ready = false
- _active_board = {}
- changed.emit()
-
-
-func get_daily_jobs() -> Array[Dictionary]:
- var result: Array[Dictionary] = []
- var jobs: Array = _active_board.get("jobs", [])
- for value: Variant in jobs:
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var job: Dictionary = (value as Dictionary).duplicate(true)
- var job_id: String = str(job.get("id", ""))
- job["progress"] = mini(
- int(_daily_progress.get(job_id, 0)), int(job.get("target", 1))
- )
- job["completed_count"] = int(_daily_completions.get(job_id, 0))
- var pending: Array[Dictionary] = _pending_for_job(
- _active_plan_id, job_id
- )
- job["pending_count"] = pending.size()
- job["claim_id"] = (
- str(pending[0].get("claim_id", "")) if not pending.is_empty() else ""
- )
- job["claimable"] = not pending.is_empty()
- result.append(job)
- return result
-
-
-func get_lifetime_jobs() -> Array[Dictionary]:
- var result: Array[Dictionary] = JobCatalog.visible_lifetime_jobs(
- _registered_species_count(),
- _lifetime_claimed,
- _registered_creature_count(FishDataType.CreatureGroup.INSECT),
- _registered_creature_count(FishDataType.CreatureGroup.SHELLFISH),
- )
- for job: Dictionary in result:
- var progress: int = _lifetime_progress(job)
- job["progress"] = mini(progress, int(job.get("target", 1)))
- job["complete"] = progress >= int(job.get("target", 1))
- job["claimable"] = bool(job["complete"])
- return result
-
-
-func get_pending_rewards() -> Array[Dictionary]:
- return _pending_rewards.duplicate(true)
-
-
-func get_forecast() -> Array[Dictionary]:
- var schedule: Array = _active_board.get("weather_schedule", [])
- var result: Array[Dictionary] = []
- for value: Variant in schedule:
- if typeof(value) == TYPE_DICTIONARY:
- result.append((value as Dictionary).duplicate(true))
- return result
-
-
-func get_plan_id() -> String:
- return _active_plan_id
-
-
-func has_active_board() -> bool:
- return validate_board(_active_board)
-
-
-func get_time_until_refresh_text() -> String:
- if _active_plan_id.is_empty() or _world_time == null:
- return "daily jobs unavailable"
- var elapsed: float = fposmod(
- _daily_clock_hour - DAILY_REFRESH_HOUR,
- WorldTimeService.HOURS_PER_DAY,
- )
- var hours_remaining: float = WorldTimeService.HOURS_PER_DAY - elapsed
- var real_seconds: int = ceili(
- hours_remaining / WorldTimeService.HOURS_PER_REAL_SECOND
- )
- return "refreshes in %d:%02d" % [
- floori(float(real_seconds) / 60.0), real_seconds % 60,
- ]
-
-
-func claim(claim_id: String) -> bool:
- if claim_id.is_empty() or _wallet == null or _experience == null:
- return false
- for index: int in _pending_rewards.size():
- var reward: Dictionary = _pending_rewards[index]
- if str(reward.get("claim_id", "")) != claim_id:
- continue
- return _claim_pending_reward(index, reward)
- for lifetime_job: Dictionary in get_lifetime_jobs():
- if str(lifetime_job.get("id", "")) != claim_id:
- continue
- if not bool(lifetime_job.get("claimable", false)):
- return false
- return _claim_lifetime_reward(lifetime_job)
- return false
-
-
-func get_host_board_network_data() -> Dictionary:
- var board := _host_board.duplicate(true)
- if not board.is_empty():
- board["daily_clock_hour"] = _daily_clock_hour
- return board
-
-
-func apply_remote_board(board: Dictionary) -> bool:
- if not validate_board(board) or not _matches_canonical_board(board):
- return false
- var plan_id: String = str(board.get("plan_id", ""))
- if plan_id != _active_plan_id:
- _expire_incomplete_daily_jobs()
- _active_plan_id = plan_id
- _daily_progress.clear()
- _daily_completions.clear()
- _active_board = board.duplicate(true)
- if board.has("daily_clock_hour"):
- _daily_clock_hour = float(board["daily_clock_hour"])
- changed.emit()
- return true
-
-
-func clear_remote_board() -> void:
- if _session != null and _session.is_host():
- _activate_host_board()
- return
- _active_board = {}
- changed.emit()
-
-
-func to_save_data() -> Dictionary:
- var progress: Dictionary = {}
- for job_id: String in _daily_progress:
- progress[job_id] = _daily_progress[job_id]
- var completions: Dictionary = {}
- for job_id: String in _daily_completions:
- completions[job_id] = _daily_completions[job_id]
- return {
- "format_version": FORMAT_VERSION,
- "host_board": _host_board.duplicate(true),
- "active_plan_id": _active_plan_id,
- "daily_progress": progress,
- "daily_completions": completions,
- "daily_clock_hour": _daily_clock_hour,
- "pending_rewards": _pending_rewards.duplicate(true),
- "lifetime_claimed": _lifetime_claimed.duplicate(),
- "statistics": {
- "fish_caught": _total_catches,
- "insects_caught": _total_insects,
- "shellfish_caught": _total_shellfish,
- "fish_sold": _total_sold,
- },
- }
-
-
-func restore_from_save_data(data: Dictionary) -> bool:
- if not validate_save_data(data):
- return false
- var received_board: Dictionary = _active_board.duplicate(true)
- var saved_host_board: Dictionary = data.get("host_board", {})
- var legacy_weather_board: bool = (
- not saved_host_board.is_empty()
- and not validate_board(saved_host_board)
- and _validate_legacy_board(saved_host_board)
- )
- _host_board = (
- {}
- if legacy_weather_board
- else saved_host_board.duplicate(true)
- )
- _active_plan_id = (
- "" if legacy_weather_board else str(data.get("active_plan_id", ""))
- )
- _daily_progress.clear()
- var progress: Dictionary = data.get("daily_progress", {})
- if not legacy_weather_board:
- for key: Variant in progress:
- _daily_progress[str(key)] = int(progress[key])
- _daily_completions.clear()
- var completions: Dictionary = data.get("daily_completions", {})
- if not legacy_weather_board:
- for key: Variant in completions:
- _daily_completions[str(key)] = mini(int(completions[key]), 1)
- _daily_clock_hour = float(data.get(
- "daily_clock_hour",
- _world_time.get_time_hours()
- if _world_time != null
- else WorldTimeService.DEFAULT_START_HOUR,
- ))
- _pending_rewards.clear()
- var rewards: Array = data.get("pending_rewards", [])
- var restored_daily_rewards: Dictionary[String, bool] = {}
- for value: Variant in rewards:
- var reward: Dictionary = value
- var reward_key: String = "%s/%s" % [
- str(reward.get("source_plan_id", "")),
- str(reward.get("source_job_id", "")),
- ]
- if restored_daily_rewards.has(reward_key):
- continue
- restored_daily_rewards[reward_key] = true
- _pending_rewards.append(reward.duplicate(true))
- _lifetime_claimed.clear()
- var claimed: Array = data.get("lifetime_claimed", [])
- for value: Variant in claimed:
- _lifetime_claimed.append(str(value))
- var statistics: Dictionary = data.get("statistics", {})
- _total_catches = int(statistics.get("fish_caught", 0))
- _total_insects = int(statistics.get("insects_caught", 0))
- _total_shellfish = int(statistics.get("shellfish_caught", 0))
- _total_sold = int(statistics.get("fish_sold", 0))
- if _session != null and _session.is_host():
- _active_board = _host_board.duplicate(true)
- _active_plan_id = str(_host_board.get("plan_id", ""))
- elif (
- _session != null
- and _session.is_joined_client()
- and not received_board.is_empty()
- ):
- apply_remote_board(received_board)
- _apply_host_weather_plan()
- changed.emit()
- return true
-
-
-func reset_to_defaults() -> void:
- _host_board = {}
- _active_board = {}
- _active_plan_id = ""
- _daily_progress.clear()
- _daily_completions.clear()
- _pending_rewards.clear()
- _lifetime_claimed.clear()
- _total_catches = 0
- _total_insects = 0
- _total_shellfish = 0
- _total_sold = 0
- _daily_clock_hour = WorldTimeService.DEFAULT_START_HOUR
- changed.emit()
-
-
-static func default_save_data() -> Dictionary:
- return {
- "format_version": FORMAT_VERSION,
- "host_board": {},
- "active_plan_id": "",
- "daily_progress": {},
- "daily_completions": {},
- "daily_clock_hour": WorldTimeService.DEFAULT_START_HOUR,
- "pending_rewards": [],
- "lifetime_claimed": [],
- "statistics": {
- "fish_caught": 0,
- "insects_caught": 0,
- "shellfish_caught": 0,
- "fish_sold": 0,
- },
- }
-
-
-static func validate_board(value: Variant) -> bool:
- if typeof(value) != TYPE_DICTIONARY:
- return false
- var board: Dictionary = value
- if (
- typeof(board.get("plan_id")) != TYPE_STRING
- or str(board.get("plan_id", "")).is_empty()
- or str(board.get("plan_id", "")).length() > 96
- or not JobCatalog.is_bounded_integer(
- board.get("cycle"), 0, MAX_PROGRESS_VALUE
- )
- or not JobCatalog.is_bounded_integer(
- board.get("schedule_anchor_index"),
- 0,
- JobCatalog.WEATHER_SEGMENT_COUNT - 1,
- )
- or typeof(board.get("jobs")) != TYPE_ARRAY
- or not JobCatalog.is_valid_weather_schedule(
- board.get("weather_schedule", [])
- )
- ):
- return false
- if (
- board.has("calendar_cycle_id")
- and (
- typeof(board["calendar_cycle_id"]) != TYPE_STRING
- or str(board["calendar_cycle_id"]).length() != 10
- )
- ):
- return false
- var jobs: Array = board.get("jobs", [])
- if jobs.is_empty() or jobs.size() > 8:
- return false
- var seen: Dictionary[String, bool] = {}
- for job_value: Variant in jobs:
- if not JobCatalog.is_valid_job(job_value):
- return false
- var job: Dictionary = job_value
- var job_id: String = str(job.get("id", ""))
- if seen.has(job_id):
- return false
- seen[job_id] = true
- return true
-
-
-static func validate_save_data(value: Variant) -> bool:
- if typeof(value) != TYPE_DICTIONARY:
- return false
- var data: Dictionary = value
- if (
- not JobCatalog.is_bounded_integer(
- data.get("format_version"), FORMAT_VERSION, FORMAT_VERSION
- )
- or typeof(data.get("host_board")) != TYPE_DICTIONARY
- or typeof(data.get("active_plan_id")) != TYPE_STRING
- or typeof(data.get("daily_progress")) != TYPE_DICTIONARY
- or typeof(data.get("daily_completions")) != TYPE_DICTIONARY
- or typeof(data.get("pending_rewards")) != TYPE_ARRAY
- or typeof(data.get("lifetime_claimed")) != TYPE_ARRAY
- or typeof(data.get("statistics")) != TYPE_DICTIONARY
- ):
- return false
- var host_board: Dictionary = data.get("host_board", {})
- if (
- not host_board.is_empty()
- and not validate_board(host_board)
- and not _validate_legacy_board(host_board)
- ):
- return false
- if (
- data.has("daily_clock_hour")
- and (
- typeof(data["daily_clock_hour"]) not in [TYPE_FLOAT, TYPE_INT]
- or not is_finite(float(data["daily_clock_hour"]))
- or float(data["daily_clock_hour"]) < 0.0
- or float(data["daily_clock_hour"]) >= WorldTimeService.HOURS_PER_DAY
- )
- ):
- return false
- var progress: Dictionary = data.get("daily_progress", {})
- if progress.size() > 8:
- return false
- for key: Variant in progress:
- if (
- typeof(key) != TYPE_STRING
- or str(key).is_empty()
- or str(key).length() > 96
- or not JobCatalog.is_bounded_integer(
- progress[key], 0, MAX_PROGRESS_VALUE
- )
- ):
- return false
- var completions: Dictionary = data.get("daily_completions", {})
- if completions.size() > 8:
- return false
- for key: Variant in completions:
- if (
- typeof(key) != TYPE_STRING
- or str(key).is_empty()
- or str(key).length() > 96
- or not JobCatalog.is_bounded_integer(
- completions[key], 0, MAX_PROGRESS_VALUE
- )
- ):
- return false
- var rewards: Array = data.get("pending_rewards", [])
- if rewards.size() > MAX_PENDING_REWARDS:
- return false
- for reward: Variant in rewards:
- if not _valid_reward(reward):
- return false
- var claimed: Array = data.get("lifetime_claimed", [])
- if claimed.size() > 64 or not _valid_string_array(claimed, 96):
- return false
- var statistics: Dictionary = data.get("statistics", {})
- return (
- JobCatalog.is_bounded_integer(
- statistics.get("fish_caught"), 0, MAX_PROGRESS_VALUE
- )
- and JobCatalog.is_bounded_integer(
- statistics.get("insects_caught", 0), 0, MAX_PROGRESS_VALUE
- )
- and JobCatalog.is_bounded_integer(
- statistics.get("shellfish_caught", 0), 0, MAX_PROGRESS_VALUE
- )
- and JobCatalog.is_bounded_integer(
- statistics.get("fish_sold"), 0, MAX_PROGRESS_VALUE
- )
- )
-
-
-static func _validate_legacy_board(value: Variant) -> bool:
- if typeof(value) != TYPE_DICTIONARY:
- return false
- var board: Dictionary = value
- if (
- typeof(board.get("plan_id")) != TYPE_STRING
- or str(board.get("plan_id", "")).is_empty()
- or str(board.get("plan_id", "")).length() > 96
- or not JobCatalog.is_bounded_integer(
- board.get("cycle"), 0, MAX_PROGRESS_VALUE
- )
- or not JobCatalog.is_bounded_integer(
- board.get("schedule_anchor_index"),
- 0,
- JobCatalog.LEGACY_WEATHER_SEGMENT_COUNT - 1,
- )
- or typeof(board.get("jobs")) != TYPE_ARRAY
- or not JobCatalog.is_valid_legacy_weather_schedule(
- board.get("weather_schedule", [])
- )
- ):
- return false
- var jobs: Array = board.get("jobs", [])
- if jobs.is_empty() or jobs.size() > 8:
- return false
- for job: Variant in jobs:
- if not JobCatalog.is_valid_job(job):
- return false
- return true
-
-
-func _activate_host_board() -> void:
- if _world_time != null:
- _daily_clock_hour = _world_time.get_time_hours()
- if _host_board.is_empty() or _host_board_is_stale():
- var next_cycle: int = int(_host_board.get("cycle", -1)) + 1
- _generate_host_board(maxi(next_cycle, 0))
- else:
- _active_board = _host_board.duplicate(true)
- _switch_plan(str(_host_board.get("plan_id", "")))
- _apply_host_weather_plan()
- board_changed.emit()
- changed.emit()
-
-
-func _generate_host_board(cycle: int) -> void:
- _expire_incomplete_daily_jobs()
- var random_suffix: String = Crypto.new().generate_random_bytes(12).hex_encode()
- var plan_id: String = "daily:%d:%s" % [cycle, random_suffix]
- var candidates: Array[FishDataType] = []
- if _catalog != null:
- for fish: FishDataType in _catalog.candidates:
- if fish != null and fish.is_selectable():
- candidates.append(fish)
- var schedule_anchor_index: int = _current_weather_segment()
- var allow_weather_jobs: bool = (
- JobCatalog.WEATHER_SEGMENT_COUNT - schedule_anchor_index >= 2
- )
- var jobs: Array[Dictionary] = JobCatalog.generate_daily_jobs(
- plan_id, candidates, allow_weather_jobs
- )
- _host_board = {
- "plan_id": plan_id,
- "cycle": cycle,
- "schedule_anchor_index": schedule_anchor_index,
- "jobs": jobs,
- "weather_schedule": JobCatalog.generate_weather_schedule(
- plan_id, jobs, schedule_anchor_index
- ),
- }
- var calendar_cycle_id: String = _world_time.get_calendar_cycle_id(
- DAILY_REFRESH_HOUR
- )
- if not calendar_cycle_id.is_empty():
- _host_board["calendar_cycle_id"] = calendar_cycle_id
- _active_board = _host_board.duplicate(true)
- _switch_plan(plan_id)
- _apply_host_weather_plan()
- board_changed.emit()
- changed.emit()
-
-
-func _switch_plan(plan_id: String) -> void:
- if plan_id == _active_plan_id:
- return
- _active_plan_id = plan_id
- _daily_progress.clear()
- _daily_completions.clear()
-
-
-func _apply_host_weather_plan() -> void:
- if (
- _world_weather == null
- or _world_time == null
- or _session == null
- or not _session.is_host()
- or _host_board.is_empty()
- ):
- return
- var schedule: Array = _host_board.get("weather_schedule", [])
- _world_weather.configure_daily_plan(
- str(_host_board.get("plan_id", "")), schedule, _world_time
- )
-
-
-func _on_natural_time_advanced(hours: float) -> void:
- if not is_finite(hours) or hours <= 0.0:
- return
- var previous_hour := _daily_clock_hour
- _daily_clock_hour = fposmod(
- _daily_clock_hour + hours,
- WorldTimeService.HOURS_PER_DAY,
- )
- if (
- _progression_ready
- and _session != null
- and _session.is_host()
- and (
- hours >= WorldTimeService.HOURS_PER_DAY
- or _crossed_daily_refresh(previous_hour, _daily_clock_hour)
- )
- ):
- var cycle: int = int(_host_board.get("cycle", -1)) + 1
- _generate_host_board(maxi(cycle, 0))
-
-
-func _on_session_state_changed(state: NetworkSession.State) -> void:
- if not _progression_ready:
- return
- if state in [NetworkSession.State.PRIVATE_HOST, NetworkSession.State.OPEN_HOST]:
- _activate_host_board()
- elif state == NetworkSession.State.JOINED_CLIENT:
- _active_board = {}
- changed.emit()
- elif state in [
- NetworkSession.State.INACTIVE,
- NetworkSession.State.DISCONNECTING,
- NetworkSession.State.CONNECTION_FAILED,
- NetworkSession.State.SERVER_LOST,
- ]:
- clear_remote_board()
-
-
-func _on_authoritative_catch(fish_catch: FishCatchType) -> void:
- if not _progression_ready or fish_catch == null or not fish_catch.is_valid():
- return
- match fish_catch.fish.get_creature_group():
- FishDataType.CreatureGroup.FISH:
- _total_catches = mini(_total_catches + 1, MAX_PROGRESS_VALUE)
- FishDataType.CreatureGroup.INSECT:
- _total_insects = mini(_total_insects + 1, MAX_PROGRESS_VALUE)
- FishDataType.CreatureGroup.SHELLFISH:
- _total_shellfish = mini(_total_shellfish + 1, MAX_PROGRESS_VALUE)
- _update_daily_for_catch(fish_catch)
- changed.emit()
-
-
-func _on_authoritative_sale_finished(
- _request_id: String,
- accepted: bool,
- _message: String,
- catch_ids: Array[StringName],
- _payout: int,
-) -> void:
- if not _progression_ready or not accepted or catch_ids.is_empty():
- return
- _total_sold = mini(_total_sold + catch_ids.size(), MAX_PROGRESS_VALUE)
- for job: Dictionary in get_daily_jobs():
- if int(job.get("kind", -1)) == JobCatalog.Kind.SELL_TOTAL:
- _advance_daily(job, catch_ids.size())
- changed.emit()
-
-
-func _update_daily_for_catch(fish_catch: FishCatchType) -> void:
- var creature_group: int = int(fish_catch.fish.get_creature_group())
- var is_fish: bool = creature_group == FishDataType.CreatureGroup.FISH
- for job: Dictionary in get_daily_jobs():
- var matches: bool = false
- match int(job.get("kind", -1)):
- JobCatalog.Kind.CATCH_TOTAL:
- matches = is_fish
- JobCatalog.Kind.CATCH_WATER:
- matches = is_fish and fish_catch.fish.is_allowed_in_water(
- int(job.get("water_type", WaterType.Type.OTHER)) as WaterType.Type
- )
- JobCatalog.Kind.CATCH_QUALITY:
- matches = (
- is_fish
- and fish_catch.quality >= int(job.get("minimum_quality", 0))
- )
- JobCatalog.Kind.CATCH_PHASE:
- matches = (
- is_fish
- and int(_world_time.get_phase()) == int(job.get("phase", -1))
- )
- JobCatalog.Kind.CATCH_WEATHER:
- matches = (
- is_fish
- and int(_world_weather.get_weather())
- == int(job.get("weather", -1))
- )
- JobCatalog.Kind.CATCH_SPECIES:
- matches = (
- is_fish
- and String(fish_catch.fish_id) == str(job.get("fish_id", ""))
- )
- JobCatalog.Kind.CATCH_CREATURE_GROUP:
- matches = creature_group == int(job.get("creature_group", -1))
- if matches:
- _advance_daily(job, 1)
-
-
-func _advance_daily(job: Dictionary, amount: int) -> void:
- var job_id: String = str(job.get("id", ""))
- if job_id.is_empty() or amount <= 0:
- return
- if int(_daily_completions.get(job_id, 0)) > 0:
- return
- var target: int = int(job.get("target", 1))
- var progress: int = mini(
- int(_daily_progress.get(job_id, 0)) + amount,
- target,
- )
- if progress >= target and _pending_rewards.size() < MAX_PENDING_REWARDS:
- _daily_completions[job_id] = 1
- var reward: Dictionary = {
- "claim_id": _daily_claim_id(_active_plan_id, job_id),
- "source_plan_id": _active_plan_id,
- "source_job_id": job_id,
- "title": str(job.get("title", "daily job")),
- "fish_coin": int(job.get("fish_coin", 0)),
- "experience": int(job.get("experience", 0)),
- }
- _pending_rewards.append(reward)
- status_changed.emit("job complete — payment ready on the net")
- _daily_progress[job_id] = progress
-
-
-func _claim_pending_reward(index: int, reward: Dictionary) -> bool:
- var state_snapshot: Dictionary = to_save_data()
- var wallet_snapshot: int = _wallet.get_balance()
- var experience_snapshot: int = _experience.get_total_experience()
- _pending_rewards.remove_at(index)
- if not _apply_reward(reward) or not _save_claim_transaction():
- _wallet.restore_balance(wallet_snapshot)
- _experience.restore_total_experience(experience_snapshot)
- restore_from_save_data(state_snapshot)
- return false
- _reward_claimed(reward)
- return true
-
-
-func _claim_lifetime_reward(job: Dictionary) -> bool:
- var job_id: String = str(job.get("id", ""))
- if job_id.is_empty() or job_id in _lifetime_claimed:
- return false
- var state_snapshot: Dictionary = to_save_data()
- var wallet_snapshot: int = _wallet.get_balance()
- var experience_snapshot: int = _experience.get_total_experience()
- _lifetime_claimed.append(job_id)
- if not _apply_reward(job) or not _save_claim_transaction():
- _wallet.restore_balance(wallet_snapshot)
- _experience.restore_total_experience(experience_snapshot)
- restore_from_save_data(state_snapshot)
- return false
- _reward_claimed(job)
- return true
-
-
-func _apply_reward(reward: Dictionary) -> bool:
- var fish_coin: int = int(reward.get("fish_coin", 0))
- var experience: int = int(reward.get("experience", 0))
- if fish_coin > 0 and not _wallet.credit(fish_coin):
- return false
- if experience > 0 and not _experience.award_experience(experience):
- return false
- changed.emit()
- return true
-
-
-func _save_claim_transaction() -> bool:
- return _save_manager != null and _save_manager.save_if_dirty()
-
-
-func _reward_claimed(reward: Dictionary) -> void:
- var title: String = str(reward.get("title", "job"))
- var fish_coin: int = int(reward.get("fish_coin", 0))
- var experience: int = int(reward.get("experience", 0))
- reward_claimed.emit(title, fish_coin, experience)
- status_changed.emit("payment received — %d xp" % experience)
- changed.emit()
-
-
-func _lifetime_progress(job: Dictionary) -> int:
- match int(job.get("kind", -1)):
- JobCatalog.Kind.CATCH_TOTAL:
- return _total_catches
- JobCatalog.Kind.SELL_TOTAL:
- return _total_sold
- JobCatalog.Kind.REACH_LEVEL:
- return _experience.get_level() if _experience != null else 0
- JobCatalog.Kind.DISCOVER_SPECIES:
- return _collection_progress(
- FishDataType.CreatureGroup.FISH, false
- )
- JobCatalog.Kind.MASTER_QUALITIES:
- return _collection_progress(
- FishDataType.CreatureGroup.FISH, true
- )
- JobCatalog.Kind.CATCH_CREATURE_GROUP:
- match int(job.get("creature_group", -1)):
- FishDataType.CreatureGroup.INSECT:
- return _total_insects
- FishDataType.CreatureGroup.SHELLFISH:
- return _total_shellfish
- JobCatalog.Kind.DISCOVER_CREATURE_GROUP:
- return _collection_progress(
- int(job.get("creature_group", -1)) as FishDataType.CreatureGroup,
- false,
- )
- JobCatalog.Kind.MASTER_CREATURE_GROUP:
- return _collection_progress(
- int(job.get("creature_group", -1)) as FishDataType.CreatureGroup,
- true,
- )
- return 0
-
-
-func _registered_species_count() -> int:
- return _registered_creature_count(FishDataType.CreatureGroup.FISH)
-
-
-func _registered_creature_count(
- creature_group: FishDataType.CreatureGroup,
-) -> int:
- var count: int = 0
- if _catalog != null:
- for fish: FishDataType in _catalog.candidates:
- if (
- fish != null
- and fish.is_selectable()
- and fish.get_creature_group() == creature_group
- ):
- count += 1
- return count
-
-
-func _collection_progress(
- creature_group: FishDataType.CreatureGroup,
- require_mastery: bool,
-) -> int:
- var progress: int = 0
- if _collection == null or _catalog == null:
- return progress
- for fish: FishDataType in _catalog.candidates:
- if (
- fish == null
- or not fish.is_selectable()
- or fish.get_creature_group() != creature_group
- ):
- continue
- if (
- _collection.has_mastered(fish.id)
- if require_mastery
- else _collection.has_discovered(fish.id)
- ):
- progress += 1
- return progress
-
-
-func _expire_incomplete_daily_jobs() -> void:
- _daily_progress.clear()
- _daily_completions.clear()
-
-
-func _on_current_state_changed() -> void:
- if _progression_ready:
- changed.emit()
-
-
-func _on_experience_changed(_total: int, _level: int) -> void:
- if _progression_ready:
- changed.emit()
-
-
-func _pending_for_job(
- plan_id: String,
- job_id: String,
-) -> Array[Dictionary]:
- var result: Array[Dictionary] = []
- for reward: Dictionary in _pending_rewards:
- if (
- str(reward.get("source_plan_id", "")) == plan_id
- and str(reward.get("source_job_id", "")) == job_id
- ):
- result.append(reward)
- return result
-
-
-func _matches_canonical_board(board: Dictionary) -> bool:
- var plan_id: String = str(board.get("plan_id", ""))
- var candidates: Array[FishDataType] = []
- if _catalog != null:
- for fish: FishDataType in _catalog.candidates:
- if fish != null and fish.is_selectable():
- candidates.append(fish)
- var anchor_index: int = int(board.get("schedule_anchor_index", -1))
- var allow_weather_jobs: bool = (
- JobCatalog.WEATHER_SEGMENT_COUNT - anchor_index >= 2
- )
- var jobs: Array[Dictionary] = JobCatalog.generate_daily_jobs(
- plan_id, candidates, allow_weather_jobs
- )
- var schedule: Array[Dictionary] = JobCatalog.generate_weather_schedule(
- plan_id, jobs, anchor_index
- )
- return (
- board.get("jobs", []) == jobs
- and board.get("weather_schedule", []) == schedule
- )
-
-
-func _current_weather_segment() -> int:
- if _world_time == null:
- return 0
- var elapsed_hours: float = fposmod(
- _world_time.get_time_hours() - WorldTimeService.DAY_START_HOUR,
- WorldTimeService.HOURS_PER_DAY,
- )
- return clampi(
- floori(elapsed_hours / JobCatalog.WEATHER_SEGMENT_HOURS),
- 0,
- JobCatalog.WEATHER_SEGMENT_COUNT - 1,
- )
-
-
-func _host_board_is_stale() -> bool:
- if _world_time == null:
- return false
- var current_cycle_id: String = _world_time.get_calendar_cycle_id(
- DAILY_REFRESH_HOUR
- )
- return (
- not current_cycle_id.is_empty()
- and str(_host_board.get("calendar_cycle_id", ""))
- != current_cycle_id
- )
-
-
-static func _daily_claim_id(
- plan_id: String,
- job_id: String,
-) -> String:
- return "%s/%s" % [plan_id, job_id]
-
-
-static func _crossed_daily_refresh(previous: float, current: float) -> bool:
- if current >= previous:
- return previous < DAILY_REFRESH_HOUR and current >= DAILY_REFRESH_HOUR
- return (
- previous < DAILY_REFRESH_HOUR
- or current >= DAILY_REFRESH_HOUR
- )
-
-
-static func _valid_string_array(values: Array, maximum_length: int) -> bool:
- var seen: Dictionary[String, bool] = {}
- for value: Variant in values:
- if typeof(value) != TYPE_STRING:
- return false
- var text: String = str(value)
- if text.is_empty() or text.length() > maximum_length or seen.has(text):
- return false
- seen[text] = true
- return true
-
-
-static func _valid_reward(value: Variant) -> bool:
- if typeof(value) != TYPE_DICTIONARY:
- return false
- var reward: Dictionary = value
- return (
- typeof(reward.get("claim_id")) == TYPE_STRING
- and not str(reward.get("claim_id", "")).is_empty()
- and str(reward.get("claim_id", "")).length() <= 196
- and typeof(reward.get("source_plan_id")) == TYPE_STRING
- and not str(reward.get("source_plan_id", "")).is_empty()
- and str(reward.get("source_plan_id", "")).length() <= 96
- and typeof(reward.get("source_job_id")) == TYPE_STRING
- and not str(reward.get("source_job_id", "")).is_empty()
- and str(reward.get("source_job_id", "")).length() <= 96
- and typeof(reward.get("title")) == TYPE_STRING
- and str(reward.get("title", "")).length() <= 96
- and JobCatalog.is_bounded_integer(
- reward.get("fish_coin"), 0, JobCatalog.MAX_JOB_REWARD_COINS
- )
- and JobCatalog.is_bounded_integer(
- reward.get("experience"),
- 0,
- JobCatalog.MAX_JOB_REWARD_EXPERIENCE,
- )
- )
diff --git a/jobs/player_job_service.gd.uid b/jobs/player_job_service.gd.uid
deleted file mode 100644
index 110c6e2..0000000
--- a/jobs/player_job_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://daqtje84gqgu0
diff --git a/main/main.gd b/main/main.gd
index 00d2b40..51e645b 100644
--- a/main/main.gd
+++ b/main/main.gd
@@ -6,43 +6,24 @@ const FishPoolType = preload("res://fish/fish_pool.gd")
const GameUIType = preload("res://ui/game_ui.gd")
const PlayerType = preload("res://player/player.gd")
const TestWorldType = preload("res://world/test_world.gd")
-const WorldLayoutType = preload("res://world/world_layout.gd")
const WaterRecoveryControllerType = preload(
"res://world/water_recovery_controller.gd"
)
const PlayerSaveManagerType = preload(
"res://save/player_save_manager.gd"
)
-const PlayerSaveSlotCatalogType = preload(
- "res://save/player_save_slot_catalog.gd"
-)
const PlayerSettingsManagerType = preload(
"res://settings/player_settings_manager.gd"
)
const PlayerSettingsType = preload("res://settings/player_settings.gd")
-const ControllerMappingManagerType = preload(
- "res://settings/controller_mapping_manager.gd"
-)
-const KeyboardMouseMappingManagerType = preload(
- "res://settings/keyboard_mouse_mapping_manager.gd"
-)
const TitleScreenType = preload("res://ui/title_screen.gd")
const PauseMenuType = preload("res://ui/pause_menu.gd")
const ItemCatalogType = preload("res://items/item_catalog.gd")
-const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
const ItemDataType = preload("res://items/item_data.gd")
-const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
-const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
const FishingShopType = preload("res://ui/fishing_shop.gd")
const FishingShopInteractionType = preload(
"res://world/fishing_shop_interaction.gd"
)
-const DecorShopInteractionType = preload(
- "res://world/decor_shop_interaction.gd"
-)
-const PlayerStorageInteractionType = preload(
- "res://world/player_storage_interaction.gd"
-)
const UIPixelationPresenterType = preload(
"res://ui/ui_pixelation_presenter.gd"
)
@@ -52,112 +33,16 @@ const PixelationResetOverlayType = preload(
const WorldPixelationPostprocessType = preload(
"res://main/world_pixelation_postprocess.gd"
)
-const RuntimePerformanceProfileType = preload(
- "res://main/runtime_performance_profile.gd"
-)
-const NetworkSessionType = preload("res://network/network_session.gd")
-const DiscoveryClientType = preload("res://network/discovery_client.gd")
-const DedicatedServerConfigType = preload(
- "res://server/dedicated_server_config.gd"
-)
-const NetworkProfilePreferencesType = preload(
- "res://network/network_profile_preferences.gd"
-)
-const SavedServerStoreType = preload(
- "res://network/saved_server_store.gd"
-)
-const PlayerSpawnServiceType = preload(
- "res://network/player_spawn_service.gd"
-)
-const NetworkFishingServiceType = preload(
- "res://network/network_fishing_service.gd"
-)
-const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
-const DecorCatalogType = preload("res://homes/decor_catalog.gd")
-const NetworkSaleServiceType = preload(
- "res://network/network_sale_service.gd"
-)
-const NetworkShopServiceType = preload(
- "res://network/network_shop_service.gd"
-)
-const NetworkItemUseServiceType = preload(
- "res://network/network_item_use_service.gd"
-)
-const NetworkFishShowcaseServiceType = preload(
- "res://network/network_fish_showcase_service.gd"
-)
-const NetworkSurfaceDrawingServiceType = preload(
- "res://network/network_surface_drawing_service.gd"
-)
-const NetworkChatServiceType = preload(
- "res://network/network_chat_service.gd"
-)
-const NetworkMailServiceType = preload(
- "res://network/network_mail_service.gd"
-)
-const PlayerAssetReservationServiceType = preload(
- "res://progression/player_asset_reservation_service.gd"
-)
-const PlayerAppearanceStoreType = preload(
- "res://progression/player_appearance_store.gd"
-)
-const NetworkProfileServiceType = preload(
- "res://network/network_profile_service.gd"
-)
-const WorldTimeServiceType = preload("res://world/world_time_service.gd")
-const WorldTimeVisualControllerType = preload(
- "res://world/world_time_visual_controller.gd"
-)
-const NetworkWorldTimeServiceType = preload(
- "res://network/network_world_time_service.gd"
-)
-const WorldWeatherServiceType = preload("res://world/world_weather_service.gd")
-const NetworkWorldWeatherServiceType = preload(
- "res://network/network_world_weather_service.gd"
-)
-const RainAmbienceType = preload("res://world/rain_ambience.gd")
-const PlayerJobServiceType = preload("res://jobs/player_job_service.gd")
-const NetworkJobServiceType = preload("res://network/network_job_service.gd")
-const GatherableCatalogType = preload(
- "res://gathering/gatherable_catalog.gd"
-)
-const GatheringControllerType = preload(
- "res://gathering/gathering_controller.gd"
-)
-const NetworkWorldSpawnServiceType = preload(
- "res://network/network_world_spawn_service.gd"
-)
-const PlayerHomeStateType = preload("res://homes/player_home_state.gd")
-const HomeWorldServiceType = preload("res://homes/home_world_service.gd")
-const NetworkHomeServiceType = preload(
- "res://network/network_home_service.gd"
-)
-const HomeDecorControllerType = preload(
- "res://homes/home_decor_controller.gd"
-)
const TITLE_MUSIC_SILENCE_DB: float = -80.0
-const TITLE_MUSIC_PATH: String = (
- "res://audio/music/title/as_in_four_wolves.ogg"
-)
-const NEW_GAME_MUSIC_PATH: String = "res://audio/music/world/tulips.wav"
-const DUSK_MUSIC_PATH: String = "res://audio/music/world/craft.mp3"
-const TIME_CROSSING_EPSILON_HOURS: float = 0.000001
-const PLAYER_MENU_PATTERN_SCALE: float = 0.85
-const PLAYER_MENU_PATTERN_SCROLL_VELOCITY := Vector2(-7.0, -5.0)
-const SHOP_PATTERN_SCALE: float = 1.75
-const DEDICATED_IDLE_PHYSICS_TICKS_PER_SECOND: int = 10
-const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30
@export var fish_catalog: FishPoolType
@export var pelican_buyer_profile: FishBuyerProfileType
@export var main_shop_buyer_profile: FishBuyerProfileType
@export var item_catalog: ItemCatalogType
-@export var gatherable_catalog: GatherableCatalogType
@export_category("Title Music")
@export_range(-40.0, 0.0, 0.5) var title_music_volume_db: float = -6.0
@export_range(0.0, 10.0, 0.05) var title_music_fade_out_seconds: float = 5.0
-@export_range(-40.0, 0.0, 0.5) var new_game_music_volume_db: float = -6.0
@onready var _test_world: TestWorldType = $TestWorld
@onready var _player: PlayerType = %Player
@@ -165,18 +50,8 @@ const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30
@onready var _game_ui: GameUIType = %GameUI
@onready var _water_recovery: WaterRecoveryControllerType = %WaterRecovery
@onready var _save_manager: PlayerSaveManagerType = %PlayerSaveManager
-@onready var _home_state: PlayerHomeStateType = %PlayerHomeState
@onready var _settings_manager: PlayerSettingsManagerType = %PlayerSettingsManager
-@onready var _controller_mapping_manager: ControllerMappingManagerType = (
- %ControllerMappingManager
-)
-@onready var _keyboard_mouse_mapping_manager: KeyboardMouseMappingManagerType = (
- %KeyboardMouseMappingManager
-)
-@onready var _interface_fonts: InterfaceFontController = %InterfaceFontController
@onready var _title_music: AudioStreamPlayer = %TitleMusic
-@onready var _new_game_music: AudioStreamPlayer = %NewGameMusic
-@onready var _dusk_music: AudioStreamPlayer = %DuskMusic
@onready var _ui_pixelation: UIPixelationPresenterType = %UIPresentation
@onready var _pixelation_reset: PixelationResetOverlayType = (
%PixelationResetOverlay
@@ -184,599 +59,29 @@ const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30
@onready var _world_pixelation: WorldPixelationPostprocessType = (
%WorldPixelationPostprocess
)
-@onready var _network_session: NetworkSessionType = %NetworkSession
-@onready var _discovery: DiscoveryClientType = %DiscoveryClient
-@onready var _world_time: WorldTimeServiceType = %WorldTimeService
-@onready var _world_time_visuals: WorldTimeVisualControllerType = (
- %WorldTimeVisualController
-)
-@onready var _network_world_time: NetworkWorldTimeServiceType = (
- %NetworkWorldTimeService
-)
-@onready var _world_weather: WorldWeatherServiceType = %WorldWeatherService
-@onready var _network_world_weather: NetworkWorldWeatherServiceType = (
- %NetworkWorldWeatherService
-)
-@onready var _player_jobs: PlayerJobServiceType = %PlayerJobService
-@onready var _network_jobs: NetworkJobServiceType = %NetworkJobService
-@onready var _network_world_spawns: NetworkWorldSpawnServiceType = (
- %NetworkWorldSpawnService
-)
-@onready var _gathering_controller: GatheringControllerType = (
- %GatheringController
-)
-@onready var _data_root: PlayerDataRoot = %PlayerDataRoot
-@onready var _identity_backups: IdentityBackupService = %IdentityBackupService
-@onready var _network_profile: NetworkProfilePreferencesType = (
- %NetworkProfilePreferences
-)
-@onready var _player_identity: PlayerIdentityStore = %PlayerIdentityStore
-@onready var _host_identity: HostIdentityStore = %HostIdentityStore
-@onready var _known_players: KnownPlayerStore = %KnownPlayerStore
-@onready var _server_trust: ServerTrustStore = %ServerTrustStore
-@onready var _relationships: PlayerRelationshipStore = %PlayerRelationshipStore
-@onready var _host_bans: HostBanStore = %HostBanStore
-@onready var _saved_servers: SavedServerStoreType = %SavedServerStore
-@onready var _player_spawn_service: PlayerSpawnServiceType = (
- %PlayerSpawnService
-)
-@onready var _network_fishing: NetworkFishingServiceType = (
- %NetworkFishingService
-)
-@onready var _network_sale: NetworkSaleServiceType = %NetworkSaleService
-@onready var _network_shop: NetworkShopServiceType = %NetworkShopService
-@onready var _network_home: NetworkHomeServiceType = %NetworkHomeService
-@onready var _home_decor_controller: HomeDecorControllerType = (
- %HomeDecorController
-)
-@onready var _network_item_use: NetworkItemUseServiceType = (
- %NetworkItemUseService
-)
-@onready var _network_fish_showcase: NetworkFishShowcaseServiceType = (
- %NetworkFishShowcaseService
-)
-@onready var _network_surface_drawing: NetworkSurfaceDrawingServiceType = (
- %NetworkSurfaceDrawingService
-)
-@onready var _network_chat: NetworkChatServiceType = %NetworkChatService
-@onready var _network_mail: NetworkMailServiceType = %NetworkMailService
-@onready var _network_player_list: NetworkPlayerListService = %NetworkPlayerListService
-@onready var _asset_reservations: PlayerAssetReservationServiceType = (
- %PlayerAssetReservationService
-)
-@onready var _appearance_store: PlayerAppearanceStoreType = %PlayerAppearanceStore
-@onready var _network_profile_service: NetworkProfileServiceType = (
- %NetworkProfileService
-)
-@onready var _players_root: Node3D = $Players
-@onready var _surface_drawings_root: Node3D = $SurfaceDrawings
-@onready var _world_gatherables_root: Node3D = $WorldGatherables
-@onready var _home_world: HomeWorldServiceType = %HomeWorldService
-@onready var _title_background: ColorRect = %TitleBackground
-@onready var _player_menu_backdrop: ColorRect = %PlayerMenuBackdrop
-@onready var _shop_backdrop: ColorRect = %ShopBackdrop
var _gameplay_started: bool = false
-var _save_slots := PlayerSaveSlotCatalogType.new()
var _shop_interaction: FishingShopInteractionType
-var _decor_shop_interaction: DecorShopInteractionType
-var _storage_interaction: PlayerStorageInteractionType
var _title_music_tween: Tween
var _title_music_transition_generation: int = 0
var _title_music_requested: bool = false
-var _dusk_music_played_for_natural_day: bool = false
var _quit_in_progress: bool = false
-var _join_requested_from_title: bool = false
-var _join_requested_from_pause: bool = false
-var _player_menu_backdrop_tween: Tween
-var _shop_backdrop_tween: Tween
-var _pending_join_endpoint: String = ""
-var _social_prompt_queue: Array[Dictionary] = []
-var _active_social_prompt: Dictionary = {}
-var _pending_social_invite_join: bool = false
-var _pending_social_invite_gameplay: bool = false
-var _server_trust_dialog: ConfirmationDialog
-var _pending_trust_changed: bool = false
-var _identity_notice_dialog: AcceptDialog
-var _data_setup_dialog: ConfirmationDialog
-var _data_setup_choose_button: Button
-var _data_setup_current_button: Button
-var _data_folder_dialog: FileDialog
-var _data_folder_picker_generation: int = 0
-var _restore_data_setup_after_picker: bool = false
-var _application_initialized := false
-var _performance_profile: RuntimePerformanceProfileType
-var _pending_existing_root_path := ""
-var _local_recovery_attempt_id: String = ""
-var _dedicated_runtime: bool = false
-var _dedicated_metrics_accumulator: float = 0.0
-
-@onready var _shoreline_ambience: ShorelineAmbience = %ShorelineAmbience
-var _rain_ambience: RainAmbienceType
func _ready() -> void:
- _data_root.prepare_legacy_user_data()
- _performance_profile = RuntimePerformanceProfileType.from_environment()
- _dedicated_runtime = _is_dedicated_server_runtime()
- if _dedicated_runtime:
- Engine.max_fps = 30
- Engine.physics_ticks_per_second = 30
- _disable_dedicated_presentation_tree()
- call_deferred("_start_dedicated_server")
- return
- _world_pixelation.set_light_performance_profile(
- _performance_profile.is_light()
- )
- _test_world.set_light_performance_profile(
- _performance_profile.is_light()
- )
- _configure_presentation_performance_profile(
- _performance_profile.is_light()
- )
- DisplayServer.window_set_title("straywild")
- _rain_ambience = RainAmbienceType.new()
- _rain_ambience.name = "RainAmbience"
- add_child(_rain_ambience)
- _rain_ambience.configure(_world_weather)
- _configure_audio_performance_profile(_performance_profile.is_light())
- _shoreline_ambience.configure(
- _player,
- _test_world.get_saltwater_shoreline_mesh(),
- )
- get_window().size_changed.connect(_resize_native_overlays)
- _resize_native_overlays()
- if not _settings_manager.settings_changed.is_connected(
- _apply_runtime_settings
- ):
- _settings_manager.settings_changed.connect(_apply_runtime_settings)
- _settings_manager.load_settings()
- _interface_fonts.enforce_standard_font()
- _interface_fonts.set_controller_text_entry_request(
- Callable(_game_ui, "request_controller_text_entry_for"),
- Callable(_game_ui, "is_controller_text_entry_open"),
- )
- if _data_root.resolve():
- _configure_portable_stores()
- _initialize_after_data_root()
- return
- _show_data_root_setup()
-
-
-func _configure_presentation_performance_profile(light_profile: bool) -> void:
- _game_ui.set_light_performance_profile(light_profile)
- var title_background_material := (
- _title_background.material as ShaderMaterial
- )
- if title_background_material != null:
- title_background_material.set_shader_parameter(
- "animation_enabled",
- not light_profile,
- )
- var menu_pattern_material := (
- _player_menu_backdrop.material as ShaderMaterial
- )
- if menu_pattern_material != null:
- menu_pattern_material.set_shader_parameter(
- "animation_enabled",
- not light_profile,
- )
- menu_pattern_material.set_shader_parameter(
- "display_scale",
- PLAYER_MENU_PATTERN_SCALE,
- )
- menu_pattern_material.set_shader_parameter(
- "scroll_velocity_pixels",
- (
- Vector2.ZERO
- if light_profile
- else PLAYER_MENU_PATTERN_SCROLL_VELOCITY
- ),
- )
- var shop_pattern_material := _shop_backdrop.material as ShaderMaterial
- if shop_pattern_material != null:
- shop_pattern_material.set_shader_parameter(
- "animation_enabled",
- not light_profile,
- )
- shop_pattern_material.set_shader_parameter(
- "display_scale",
- SHOP_PATTERN_SCALE,
- )
- shop_pattern_material.set_shader_parameter(
- "scroll_velocity_pixels",
- (
- Vector2.ZERO
- if light_profile
- else PLAYER_MENU_PATTERN_SCROLL_VELOCITY
- ),
- )
-
-
-func _configure_audio_performance_profile(light_profile: bool) -> void:
- _title_music.stream = _load_optional_audio_stream(TITLE_MUSIC_PATH)
- _new_game_music.stream = _load_optional_audio_stream(NEW_GAME_MUSIC_PATH)
- _dusk_music.stream = _load_optional_audio_stream(DUSK_MUSIC_PATH)
- if light_profile:
- _shoreline_ambience.set_audio_enabled(false)
- _rain_ambience.set_audio_enabled(false)
- return
- _shoreline_ambience.set_audio_enabled(true)
- _rain_ambience.set_audio_enabled(true)
-
-
-func _load_optional_audio_stream(path: String) -> AudioStream:
- if not ResourceLoader.exists(path, "AudioStream"):
- return null
- return load(path) as AudioStream
-
-
-func _is_dedicated_server_runtime() -> bool:
- if OS.has_feature("dedicated_server"):
- return true
- return "--dedicated-server" in OS.get_cmdline_user_args()
-
-
-func _disable_dedicated_presentation_tree() -> void:
- for path: NodePath in [
- NodePath("TitleBackgroundLayer"),
- NodePath("PlayerMenuBackdrop"),
- NodePath("ShopBackdrop"),
- NodePath("WorldPixelationPostprocess"),
- NodePath("UIPresentation"),
- NodePath("PixelationResetOverlay"),
- NodePath("WorldTimeVisualController"),
- NodePath("GatheringController"),
- NodePath("FishingSpot"),
- NodePath("WaterRecovery"),
- NodePath("ControllerMappingManager"),
- NodePath("KeyboardMouseMappingManager"),
- ]:
- var presentation_node: Node = get_node_or_null(path)
- if presentation_node == null:
- continue
- presentation_node.process_mode = Node.PROCESS_MODE_DISABLED
- if presentation_node is CanvasItem:
- (presentation_node as CanvasItem).visible = false
- elif presentation_node is CanvasLayer:
- (presentation_node as CanvasLayer).visible = false
- for audio_path: NodePath in [
- NodePath("TitleMusic"),
- NodePath("NewGameMusic"),
- NodePath("DuskMusic"),
- NodePath("ShorelineAmbience"),
- ]:
- var audio_node: Node = get_node_or_null(audio_path)
- if audio_node == null:
- continue
- if audio_node.has_method("stop"):
- audio_node.call("stop")
- audio_node.process_mode = Node.PROCESS_MODE_DISABLED
-
-
-func _print_dedicated_metrics() -> void:
- var metrics: Dictionary = {
- "players": _network_session.get_player_count(),
- "fps": Engine.get_frames_per_second(),
- "frame_process_ms": Performance.get_monitor(
- Performance.TIME_PROCESS
- ) * 1000.0,
- "frame_physics_ms": Performance.get_monitor(
- Performance.TIME_PHYSICS_PROCESS
- ) * 1000.0,
- "memory_mib": Performance.get_monitor(
- Performance.MEMORY_STATIC
- ) / (1024.0 * 1024.0),
- "nodes": int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT)),
- "physics_active_objects": int(Performance.get_monitor(
- Performance.PHYSICS_3D_ACTIVE_OBJECTS
- )),
- "physics_collision_pairs": int(Performance.get_monitor(
- Performance.PHYSICS_3D_COLLISION_PAIRS
- )),
- "movement": _network_session.get_movement_metrics(),
- }
- if _network_fishing.has_method("get_network_metrics"):
- metrics["fishing"] = _network_fishing.call("get_network_metrics")
- if _network_world_spawns.has_method("get_network_metrics"):
- metrics["gatherables"] = _network_world_spawns.call(
- "get_network_metrics"
- )
- print("straywild server metrics: %s" % JSON.stringify(metrics))
-
-
-func _start_dedicated_server() -> void:
- var config: DedicatedServerConfigType = DedicatedServerConfigType.from_runtime()
- if not config.is_valid():
- _fail_dedicated_server(config.error_message)
- return
- var data_path: String = config.data_directory
- if not _data_root.has_data_manifest(data_path):
- var created: Dictionary = _data_root.create_unbound_root(data_path)
- if not bool(created.get("ok", false)):
- _fail_dedicated_server(str(created.get(
- "message", "Could not create the server data directory."
- )))
- return
- if not _data_root.activate_process_root(data_path):
- _fail_dedicated_server(_data_root.error_message)
- return
- var identity_directory: String = data_path.path_join("server")
- if DirAccess.make_dir_recursive_absolute(identity_directory) != OK:
- _fail_dedicated_server("Could not create the server identity directory.")
- return
- _host_identity.configure("host_identity", true, identity_directory)
- if not _discovery.set_base_url_override(config.discovery_url):
- _fail_dedicated_server("The discovery URL is invalid.")
- return
- if not _network_session.configure_dedicated_operators(
- config.operator_fingerprints
- ):
- _fail_dedicated_server("The server operator list is invalid.")
- return
- if not _network_chat.configure_dedicated_history(
- config.chat_logging,
- config.chat_log_path,
- ):
- _fail_dedicated_server(
- "The dedicated-server chat privacy configuration is invalid."
- )
- return
- if not _apply_world(
- WorldLayoutType.GENERATED,
- config.world_seed,
- false,
- ):
- _fail_dedicated_server("The configured world seed could not be generated.")
- return
- _test_world.set_dedicated_simulation(true)
- if not _network_session.set_host_world(
- WorldLayoutType.GENERATED,
- config.world_seed,
- ):
- _fail_dedicated_server("The configured world seed is invalid.")
- return
- _configure_portable_stores()
- if not _discovery.configure_dedicated_runtime(config.server_name):
- _fail_dedicated_server("The server room name is invalid.")
- return
- _initialize_application(true)
- _player.set_local_control(false)
- _player.set_network_simulation_only(true)
- _player.visible = false
- _player.collision_layer = 0
- _player.collision_mask = 0
- _player.set_physics_process(false)
- if not _network_session.peer_count_changed.is_connected(
- _on_dedicated_peer_count_changed
- ):
- _network_session.peer_count_changed.connect(
- _on_dedicated_peer_count_changed
- )
- if not _network_session.start_dedicated_host(
- config.port,
- config.max_players,
- config.bind_address,
- ):
- _fail_dedicated_server("Could not start the dedicated server.")
- return
- _network_session.set_session_display_name(config.server_name)
- if not _network_session.set_host_open(true):
- _fail_dedicated_server("Could not open the dedicated server.")
- return
- _on_dedicated_peer_count_changed(
- _network_session.get_player_count(),
- config.max_players,
- )
- _player_jobs.begin_progression_session()
- if config.public_listing and not _discovery.set_discoverable(true):
- _network_session.disconnect_session("Discovery setup failed.")
- _fail_dedicated_server(_discovery.get_host_status_message())
- return
- print(
- "straywild dedicated server ready: %s on %s:%d (%d players, %s, timezone %s)"
- % [
- config.server_name,
- config.bind_address,
- config.port,
- config.max_players,
- "public" if config.public_listing else "unlisted",
- str(Time.get_time_zone_from_system().get("name", "local")),
- ]
- )
- print(
- "Server identity: %s"
- % _network_session.get_host_identity_fingerprint()
- )
-
-
-func _on_dedicated_peer_count_changed(
- player_count: int,
- _max_players: int,
-) -> void:
- if not _dedicated_runtime:
- return
- Engine.physics_ticks_per_second = (
- DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND
- if player_count > 0
- else DEDICATED_IDLE_PHYSICS_TICKS_PER_SECOND
- )
-
-
-func _fail_dedicated_server(message: String) -> void:
- push_error("Dedicated server startup failed: %s" % message)
- get_tree().quit(1)
-
-
-func _configure_portable_stores() -> void:
- if _dedicated_runtime:
- _save_manager.configure_storage(
- _data_root.path_for(&"player_save"), _data_root
- )
- elif not _save_slots.configure(_data_root, _save_manager):
- push_error(
- "Save-slot catalog setup failed: %s"
- % _save_slots.get_error_message()
- )
- _network_profile.configure_storage(
- _data_root.path_for(&"network_profile"), _data_root
- )
- _appearance_store.configure_storage(
- _data_root.path_for(&"player_appearance"), _data_root
- )
- _saved_servers.configure_storage(
- _data_root.path_for(&"saved_servers"), _data_root
- )
- _known_players.configure_storage(
- _data_root.path_for(&"known_players"), _data_root
- )
- _relationships.configure_storage(
- _data_root.path_for(&"player_relationships"), _data_root
- )
- _server_trust.configure_storage(
- _data_root.path_for(&"server_trust"), _data_root
- )
- _host_bans.configure_storage(
- _data_root.path_for(&"host_bans"), _data_root
- )
-
-
-func _initialize_after_data_root() -> void:
- _initialize_application(false)
-
-
-func _initialize_application(dedicated: bool) -> void:
- if _application_initialized:
- return
- _application_initialized = true
_player.global_transform = _test_world.get_player_spawn_transform()
_player.velocity = Vector3.ZERO
- _player_spawn_service.setup(
- _players_root,
- _player,
- _test_world.get_player_spawn_transform(),
- dedicated,
- )
- _network_session.setup(
- _network_profile,
- _saved_servers,
- _player_spawn_service,
- _player_identity,
- _host_identity,
- _known_players,
- _server_trust,
- _host_bans,
- dedicated,
- )
- _discovery.setup(_network_session, _relationships)
- if not dedicated:
- _world_time_visuals.setup(
- _world_time,
- _test_world.get_world_environment(),
- _test_world.get_sun(),
- _world_weather,
- _player,
- Callable(_player, "get_active_gameplay_camera"),
- _performance_profile.is_light(),
- )
- if not _world_time.natural_time_advanced.is_connected(
- _on_natural_time_advanced
- ):
- _world_time.natural_time_advanced.connect(
- _on_natural_time_advanced
- )
- _network_world_time.setup(_network_session, _world_time)
- _network_world_weather.setup(_network_session, _world_weather)
- _player_jobs.setup(
- _player.wallet,
- _player.experience,
- _player.collection_log,
- fish_catalog,
- _world_time,
- _world_weather,
- _network_session,
- )
- _network_jobs.setup(_network_session, _player_jobs)
- _identity_backups.setup(_data_root, _player_identity, _host_identity)
- _network_profile_service.setup(
- _network_session,
- _network_profile,
- _appearance_store,
- _player_spawn_service,
- )
- _network_player_list.setup(
- _network_session,
- _relationships,
- _host_bans,
- _known_players,
- _player_spawn_service,
- _network_chat,
- _network_mail,
- )
- if not dedicated:
- _network_session.set_local_appearance_snapshot(
- _appearance_store.get_snapshot()
- )
- _network_session.join_authenticated.connect(
- _on_network_join_authenticated
- )
- _network_session.connection_error.connect(
- _on_network_connection_error
- )
- _network_session.server_trust_required.connect(
- _on_server_trust_required
- )
- _network_session.server_lost.connect(_on_network_server_lost)
- _network_session.peer_identity_observed.connect(
- _on_peer_identity_observed
- )
- _network_session.remote_recovery_requested.connect(
- _on_remote_recovery_requested
- )
- _network_session.remote_recovery_presentation_changed.connect(
- _on_remote_recovery_presentation_changed
- )
-
-
_player.fish_sale_service.setup(
_player.inventory,
- _player.wallet,
- _asset_reservations
+ _player.wallet
)
_player.bag.setup(item_catalog)
- _player.inventory_layout.setup(
- _player.bag,
- _player.inventory,
- item_catalog,
- _player.cooler_capacity,
- )
- _player.bag.set_inventory_layout(_player.inventory_layout)
- _player.inventory.set_inventory_layout(_player.inventory_layout)
- _player.hotbar.setup(
- _player.bag,
- item_catalog,
- _player.inventory,
- _player.inventory_layout,
- )
+ _player.hotbar.setup(_player.bag, item_catalog)
_shop_interaction = _test_world.get_fishing_shop()
- _decor_shop_interaction = _test_world.get_decor_shop()
- _storage_interaction = _test_world.get_player_storage()
- if not dedicated:
- _shop_interaction.setup_local_player(_player)
- _shop_interaction.local_player_range_changed.connect(
- _on_shop_range_changed
- )
- _game_ui.set_shop_npc_player_in_range(
- _shop_interaction.is_local_player_in_range()
- )
- _decor_shop_interaction.setup_local_player(_player)
- _decor_shop_interaction.local_player_range_changed.connect(
- _on_decor_shop_range_changed
- )
- _storage_interaction.setup_local_player(_player)
- _storage_interaction.local_player_range_changed.connect(
- _on_storage_range_changed
- )
+ _shop_interaction.setup_local_player(_player)
+ _shop_interaction.local_player_range_changed.connect(
+ _on_shop_range_changed
+ )
_save_manager.setup(
_player.inventory,
_player.collection_log,
@@ -784,195 +89,26 @@ func _initialize_application(dedicated: bool) -> void:
fish_catalog,
_player.bag,
_player.hotbar,
- _player.inventory_layout,
item_catalog,
_player.fishing_upgrades,
- _player.cooler_capacity,
- _player.art_unlocks,
- _player.experience,
- _world_time,
- _world_weather,
- _player_jobs,
- _player,
- _home_state,
+ _player.cooler_capacity
)
- _home_world.setup(
- _test_world,
- _player_spawn_service,
- _network_session,
- dedicated,
- )
- _network_home.setup(
- _network_session,
- _player_spawn_service,
- _home_state,
- _save_manager,
- _home_world,
- _player.bag,
- _player.inventory_layout,
- _player.hotbar,
- )
- _player_jobs.set_save_manager(_save_manager)
_save_manager.set_autosave_enabled(false)
- _asset_reservations.setup(
- _player.wallet, _player.inventory, _player.bag, item_catalog
- )
- _player.inventory.set_reservation_service(_asset_reservations)
- _network_mail.setup(
- _network_session,
- _asset_reservations,
- _player.wallet,
- _player.inventory,
- _player.bag,
- _player.collection_log,
- _player.cooler_capacity,
- item_catalog,
- fish_catalog,
- _save_manager
- )
- _network_item_use.setup(
- _network_session,
- _player_spawn_service,
- item_catalog,
- _player.bag,
- _player.item_effects,
- _save_manager,
- _asset_reservations
- )
- _network_world_spawns.setup(
- _network_session,
- _player_spawn_service,
- _test_world,
- _world_gatherables_root,
- gatherable_catalog,
- fish_catalog,
- _player.inventory,
- _player.collection_log,
- _player.cooler_capacity,
- _player.experience,
- _save_manager,
- _network_item_use,
- _world_time,
- not dedicated,
- )
- _gathering_controller.setup(
- _player,
- _player.bag,
- _player.hotbar,
- _fishing_spot,
- _network_world_spawns,
- )
- _network_world_spawns.local_capture_received.connect(
- _fishing_spot.present_external_catch
- )
- _gathering_controller.status_changed.connect(
- _fishing_spot.report_external_status
- )
- _network_fish_showcase.setup(
- _network_session,
- _player_spawn_service,
- fish_catalog,
- _player.inventory,
- _player.hotbar,
- )
- _network_surface_drawing.setup(
- _network_session,
- _player_spawn_service,
- _relationships,
- _player,
- _surface_drawings_root,
- _player.bag,
- _player.hotbar,
- _player.art_unlocks,
- _data_root,
- )
- _network_player_list.set_surface_drawing_service(
- _network_surface_drawing
- )
- _network_chat.setup(_network_session)
- _network_fishing.setup(
- _network_session,
- _player_spawn_service,
- _fishing_spot,
- _player.inventory,
- _player.bag,
- _player.collection_log,
- _player.cooler_capacity,
- _player.experience,
- _save_manager,
- item_catalog,
- fish_catalog,
- _network_item_use,
- not dedicated,
- )
- var sale_buyers: Array[FishBuyerProfileType] = [
- pelican_buyer_profile,
- main_shop_buyer_profile,
- ]
- _network_sale.setup(
- _network_session,
- _player_spawn_service,
- _network_fishing,
- _shop_interaction,
- _player.inventory,
- _player.bag,
- item_catalog,
- _player.wallet,
- _player.fish_sale_service,
- _save_manager,
- fish_catalog,
- sale_buyers,
- _asset_reservations,
- _player.inventory_layout,
- )
- _player_jobs.bind_authoritative_services(
- _network_fishing,
- _network_sale,
- _network_world_spawns,
- )
- _network_shop.setup(
- _network_session,
- _player_spawn_service,
- _network_fishing,
- _shop_interaction,
- _player.wallet,
- _player.bag,
- item_catalog,
- _player.fishing_upgrades,
- _player.cooler_capacity,
- _player.art_unlocks,
- _save_manager,
- _asset_reservations,
- _player.inventory_layout,
- _home_state,
- _network_home,
- _decor_shop_interaction,
- )
_fishing_spot.setup(
_player,
_player.inventory,
_player.collection_log,
- _player.experience,
_player.bag,
_player.hotbar,
item_catalog,
_player.fishing_upgrades,
_player.item_effects,
- _network_item_use,
- _player.cooler_capacity,
- _network_session,
- _network_fishing,
- _world_time,
- _world_weather,
- Callable(_test_world, "get_fishable_water_regions"),
+ _player.cooler_capacity
)
- if dedicated:
- return
_game_ui.setup(
_player,
_player.inventory,
_player.collection_log,
- _player.experience,
_player.wallet,
_player.fish_sale_service,
pelican_buyer_profile,
@@ -980,97 +116,25 @@ func _initialize_application(dedicated: bool) -> void:
_fishing_spot,
_player.bag,
_player.hotbar,
- _player.inventory_layout,
item_catalog,
main_shop_buyer_profile,
_player.fishing_upgrades,
_shop_interaction,
- _storage_interaction,
- _decor_shop_interaction,
_player.item_effects,
- _player.cooler_capacity,
- _network_session,
- _network_sale,
- _network_shop,
- _home_state,
- _network_chat,
- _network_profile,
- _player_spawn_service,
- _network_mail,
- _asset_reservations,
- _network_profile_service,
- _network_player_list,
- _discovery,
- _settings_manager,
- _network_surface_drawing,
- _player.art_unlocks,
- _world_time,
- _world_weather,
- _player_jobs,
- _test_world.get_world_environment(),
- _test_world.get_sun(),
+ _player.cooler_capacity
)
- _home_decor_controller.setup(
- _player,
- _player.hotbar,
- _player.bag,
- _home_state,
- _network_home,
- _home_world,
- _fishing_spot,
- _game_ui.get_home_decor_toolbar(),
- )
- _game_ui.get_player_storage().decorate_requested.connect(
- _home_decor_controller.begin_decorating
- )
- _network_home.local_space_changed.connect(_on_local_home_space_changed)
- _game_ui.rv_storage_requested.connect(_on_rv_storage_requested)
- _game_ui.setup_data_and_identity(
- _data_root,
- _identity_backups,
- _player_identity,
- _host_identity,
- _network_session,
- _interface_fonts,
- )
- _game_ui.data_root_changed.connect(_on_data_root_changed)
- _game_ui.setup_controller_mapping(_controller_mapping_manager)
- _ui_pixelation.setup_controller_mapping(_controller_mapping_manager)
- _game_ui.setup_keyboard_mouse_mapping(_keyboard_mouse_mapping_manager)
- _network_player_list.friend_request_received.connect(
- _on_friend_request_received
- )
- _discovery.friend_invite_received.connect(_on_friend_invite_received)
- _discovery.public_join_prepared.connect(
- _on_social_public_join_prepared
- )
- _discovery.public_join_status_changed.connect(
- _on_social_public_join_status_changed
- )
- _game_ui.social_prompt_accepted.connect(_on_social_prompt_accepted)
- _game_ui.social_prompt_declined.connect(_on_social_prompt_declined)
- _data_root.conflict_detected.connect(_on_portable_conflict)
- if (
- PortableFileGuard.has_syncthing_conflict(
- _data_root.root_path.path_join("player")
- )
- or PortableFileGuard.has_syncthing_conflict(
- _data_root.root_path.path_join("social")
- )
- ):
- call_deferred(
- "_on_data_root_status",
- "Syncthing conflict copies were found. Review the data folder.",
- )
_water_recovery.setup(
_player,
_fishing_spot,
_game_ui,
_game_ui.get_screen_fade(),
_test_world.get_player_water_triggers(),
- _test_world.get_safe_respawn_points(),
- Callable(_test_world, "get_water_recovery_position"),
+ _test_world.get_safe_respawn_points()
)
+ if not _settings_manager.settings_changed.is_connected(
+ _apply_runtime_settings
+ ):
+ _settings_manager.settings_changed.connect(_apply_runtime_settings)
_ui_pixelation.effective_pixel_size_changed.connect(
_on_effective_ui_pixel_size_changed
)
@@ -1083,49 +147,24 @@ func _initialize_application(dedicated: bool) -> void:
_game_ui.interactive_pointer_ui_changed.connect(
_ui_pixelation.set_interactive_ui_open
)
- _game_ui.passive_pointer_ui_changed.connect(
- _ui_pixelation.set_passive_pointer_ui_enabled
- )
- _game_ui.player_menu_backdrop_visibility_changed.connect(
- _set_player_menu_backdrop_visible
- )
- _game_ui.shop_backdrop_visibility_changed.connect(
- _set_shop_backdrop_visible
- )
_pixelation_reset.return_to_settings_requested.connect(
_game_ui.focus_open_settings_back_button
)
_pixelation_reset.reset_requested.connect(_reset_pixelation)
+ _settings_manager.load_settings()
_apply_runtime_settings(_settings_manager.current_settings)
_set_gameplay_active(false)
var title_screen: TitleScreenType = _game_ui.get_title_screen()
- title_screen.slot_play_requested.connect(_on_slot_play_requested)
- title_screen.new_slot_requested.connect(_on_new_slot_requested)
+ title_screen.gameplay_requested.connect(_on_gameplay_requested)
title_screen.quit_requested.connect(_on_quit_requested)
- title_screen.setup(
- _save_manager,
- _save_slots,
- _settings_manager,
- _network_session,
- _saved_servers,
- _server_trust,
- _discovery,
- _data_root,
- _interface_fonts,
- )
+ title_screen.setup(_save_manager, _settings_manager)
var pause_menu: PauseMenuType = _game_ui.get_pause_menu()
pause_menu.setup(
_player,
_save_manager,
_settings_manager,
- _fishing_spot,
- _network_session,
- _saved_servers,
- _server_trust,
- _discovery,
+ _fishing_spot
)
- title_screen.join_game_requested.connect(_on_title_join_game_requested)
- pause_menu.join_game_requested.connect(_on_pause_join_game_requested)
pause_menu.return_to_title_requested.connect(
_on_return_to_title_requested
)
@@ -1136,409 +175,33 @@ func _initialize_application(dedicated: bool) -> void:
pause_menu.menu_visibility_changed.connect(
_game_ui.set_system_menu_open
)
- _player.hotbar.selected_assignment_changed.connect(
+ _player.hotbar.selected_slot_changed.connect(
_on_active_hotbar_item_changed
)
- _player.bag.contents_changed.connect(_refresh_active_hotbar_item)
_fishing_spot.ready_for_equipment_refresh.connect(
_refresh_active_hotbar_item
)
_water_recovery.recovery_starting.connect(
_on_water_recovery_starting
)
- _water_recovery.recovery_finished.connect(
- _on_water_recovery_finished
+ _on_active_hotbar_item_changed(
+ _player.hotbar.get_selected_slot(),
+ _player.hotbar.get_selected_item_id()
)
- _water_recovery.local_respawn_completed.connect(
- _on_local_respawn_completed
- )
- _refresh_active_hotbar_item()
_show_title_music(true)
-func _show_data_root_setup() -> void:
- if _data_setup_dialog == null:
- _data_setup_dialog = ConfirmationDialog.new()
- _data_setup_dialog.title = "straywild player data"
- _data_setup_dialog.ok_button_text = "Use This Folder"
- _data_setup_dialog.cancel_button_text = "Quit"
- _data_setup_dialog.confirmed.connect(_use_default_data_root)
- _data_setup_dialog.canceled.connect(get_tree().quit)
- _data_setup_choose_button = _data_setup_dialog.add_button(
- "Choose Another Folder", false, "choose"
- )
- _data_setup_current_button = _data_setup_dialog.add_button(
- "Keep Current Location", false, "current"
- )
- _data_setup_dialog.custom_action.connect(_on_data_setup_action)
- _interface_fonts.apply_utility_theme(_data_setup_dialog)
- add_child(_data_setup_dialog)
- var default_path: String = _data_root.default_visible_path()
- var legacy: bool = PortableDataMigration.legacy_files_present()
- _data_setup_dialog.dialog_text = (
- ("Move your existing straywild data to an easy-to-find folder."
- if legacy else "Choose where straywild stores your player data.")
- + "\n\n%s\n\nYou can choose a Syncthing folder."
- % (default_path if not default_path.is_empty() else "Choose a folder")
- )
- _data_setup_dialog.ok_button_text = (
- (
- "Move to Documents/straywild"
- if legacy else "Use This Folder"
- )
- if not default_path.is_empty()
- else "Keep Current Location"
- )
- _data_setup_current_button.visible = not default_path.is_empty()
- if not _data_root.error_message.is_empty():
- _data_setup_dialog.dialog_text = (
- "Your straywild data folder is unavailable.\n\n%s"
- % _data_root.error_message
- )
- _data_setup_dialog.popup_centered(Vector2i(640, 360))
- _focus_data_setup_actions.call_deferred()
-
-
-func _focus_data_setup_actions() -> void:
- if _data_setup_dialog == null or not _data_setup_dialog.visible:
- return
- var actions: Array[Button] = [
- _data_setup_dialog.get_ok_button(),
- _data_setup_choose_button,
- ]
- if _data_setup_current_button.visible:
- actions.append(_data_setup_current_button)
- actions.append(_data_setup_dialog.get_cancel_button())
- for index: int in actions.size():
- var action: Button = actions[index]
- var previous: Button = actions[maxi(index - 1, 0)]
- var next: Button = actions[mini(index + 1, actions.size() - 1)]
- action.focus_mode = Control.FOCUS_ALL
- action.focus_neighbor_left = action.get_path_to(previous)
- action.focus_neighbor_right = action.get_path_to(next)
- action.focus_neighbor_top = action.get_path_to(action)
- action.focus_neighbor_bottom = action.get_path_to(action)
- _data_setup_dialog.get_ok_button().grab_focus()
-
-
-func _use_default_data_root() -> void:
- var path: String = _data_root.default_visible_path()
- if path.is_empty():
- _activate_selected_data_path(
- ProjectSettings.globalize_path(
- PlayerDataRoot.APP_DATA_PORTABLE_PATH
- ),
- true,
- )
- return
- _activate_selected_data_path(path)
-
-
-func _on_data_setup_action(action: StringName) -> void:
- if action == &"choose":
- _show_folder_picker()
- elif action == &"current":
- _activate_selected_data_path(
- ProjectSettings.globalize_path(PlayerDataRoot.APP_DATA_PORTABLE_PATH), true
- )
-
-
-func _show_folder_picker() -> void:
- if _data_folder_dialog == null:
- _data_folder_dialog = FileDialog.new()
- _data_folder_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
- _data_folder_dialog.access = FileDialog.ACCESS_FILESYSTEM
- _data_folder_dialog.use_native_dialog = false
- _data_folder_dialog.dir_selected.connect(_activate_selected_data_path)
- _data_folder_dialog.canceled.connect(_on_data_folder_picker_canceled)
- _interface_fonts.apply_utility_theme(_data_folder_dialog)
- add_child(_data_folder_dialog)
- if _data_folder_dialog.visible:
- return
- _data_folder_picker_generation += 1
- var generation: int = _data_folder_picker_generation
- _restore_data_setup_after_picker = (
- _data_setup_dialog != null and _data_setup_dialog.visible
- )
- if _data_setup_dialog != null:
- _data_setup_dialog.hide()
- _open_folder_picker_after_modal.call_deferred(generation)
-
-
-func _open_folder_picker_after_modal(generation: int) -> void:
- await get_tree().process_frame
- if (
- generation != _data_folder_picker_generation
- or _data_folder_dialog == null
- or _data_folder_dialog.visible
- ):
- return
- _data_folder_dialog.current_dir = (
- _data_root.default_visible_path().get_base_dir()
- if not _data_root.default_visible_path().is_empty()
- else OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS)
- )
- _interface_fonts.popup_file_dialog(_data_folder_dialog)
-
-
-func _on_data_folder_picker_canceled() -> void:
- _data_folder_picker_generation += 1
- var generation: int = _data_folder_picker_generation
- var should_restore_setup: bool = _restore_data_setup_after_picker
- _restore_data_setup_after_picker = false
- if _data_folder_dialog != null:
- _data_folder_dialog.hide()
- if should_restore_setup:
- _restore_data_setup_after_picker_after_modal.call_deferred(generation)
-
-
-func _restore_data_setup_after_picker_after_modal(generation: int) -> void:
- await get_tree().process_frame
- if generation != _data_folder_picker_generation:
- return
- _show_data_root_setup()
-
-
-func _activate_selected_data_path(path: String, app_data: bool = false) -> void:
- _restore_data_setup_after_picker = false
- var ok: bool = false
- if PortableDataMigration.legacy_files_present():
- var result: Dictionary = (
- PortableDataMigration.adopt_legacy_app_data(_data_root)
- if app_data
- else PortableDataMigration.migrate_legacy_to(_data_root, path)
- )
- ok = bool(result.get("ok", false))
- if not ok:
- if bool(result.get("requires_existing_root_decision", false)):
- _show_existing_root_choice(path)
- return
- _show_data_error(str(result.get("message", "Migration failed.")))
- return
- else:
- ok = _data_root.select_new_root(path, app_data)
- if not ok:
- _show_data_error(_data_root.error_message)
- return
- _data_setup_dialog.hide()
- _configure_portable_stores()
- _initialize_after_data_root()
-
-
-func _show_existing_root_choice(path: String) -> void:
- _pending_existing_root_path = path
- var dialog: ConfirmationDialog = ConfirmationDialog.new()
- dialog.title = "Existing straywild data"
- dialog.ok_button_text = "Use Data Already in Selected Folder"
- dialog.cancel_button_text = "Cancel"
- dialog.dialog_text = (
- "This folder already contains straywild player data.\n\n"
- + "Choose which complete data set to use. Data is never merged automatically."
- )
- dialog.add_button(
- "Replace with This Device's Data", false, "replace"
- )
- dialog.confirmed.connect(func() -> void:
- if _data_root.use_existing_root(_pending_existing_root_path):
- dialog.queue_free()
- _data_setup_dialog.hide()
- _configure_portable_stores()
- _initialize_after_data_root()
- else:
- _show_data_error(_data_root.error_message)
- )
- dialog.custom_action.connect(func(action: StringName) -> void:
- if action != &"replace":
- return
- var result: Dictionary = PortableDataMigration.replace_existing_with_legacy(
- _data_root, _pending_existing_root_path
- )
- if bool(result.get("ok", false)):
- dialog.queue_free()
- _data_setup_dialog.hide()
- _configure_portable_stores()
- _initialize_after_data_root()
- else:
- _show_data_error(str(result.get("message", "Migration failed.")))
- )
- _interface_fonts.apply_utility_theme(dialog)
- add_child(dialog)
- dialog.popup_centered(Vector2i(680, 360))
- _configure_popup_dialog.call_deferred(
- dialog, dialog.get_cancel_button()
- )
-
-
-func _show_data_error(message: String) -> void:
- if _data_setup_dialog != null:
- _data_setup_dialog.dialog_text = message
- _data_setup_dialog.popup_centered(Vector2i(640, 300))
- _focus_data_setup_actions.call_deferred()
-
-
-func _on_portable_conflict(message: String, _path: String) -> void:
- var dialog: AcceptDialog = AcceptDialog.new()
- _interface_fonts.apply_utility_theme(dialog)
- dialog.title = "Player data conflict"
- dialog.dialog_text = (
- message
- + "\n\nDo not play the same profile on two devices at the same time."
- + "\n\nThe data folder can be opened from Settings > Data."
- )
- dialog.confirmed.connect(dialog.queue_free)
- _game_ui.add_child(dialog)
- dialog.popup_centered(Vector2i(560, 300))
- _configure_popup_dialog.call_deferred(dialog, dialog.get_ok_button())
-
-
-func _on_data_root_status(message: String) -> void:
- if "Syncthing conflict" not in message:
- return
- var dialog: AcceptDialog = AcceptDialog.new()
- _interface_fonts.apply_utility_theme(dialog)
- dialog.title = "Synced data needs review"
- dialog.dialog_text = (
- message + "\n\nThe data folder can be opened from Settings > Data."
- )
- dialog.confirmed.connect(dialog.queue_free)
- _game_ui.add_child(dialog)
- dialog.popup_centered(Vector2i(560, 260))
- _configure_popup_dialog.call_deferred(dialog, dialog.get_ok_button())
-
-
-func _on_data_root_changed() -> void:
- _save_manager.set_autosave_enabled(false)
- _save_manager.cancel_pending_autosave()
- _configure_portable_stores()
- if not _network_profile_service.reload_persisted_profile():
- push_warning("The selected data folder's player profile could not be loaded.")
- _network_session.set_local_appearance_snapshot(
- _appearance_store.get_snapshot()
- )
- _game_ui.get_title_screen().refresh_after_data_root_change()
-
-
-func _on_server_trust_required(
- endpoint: String,
- expected_fingerprint: String,
- received_fingerprint: String,
- is_changed: bool,
-) -> void:
- if _server_trust_dialog == null:
- _server_trust_dialog = ConfirmationDialog.new()
- _server_trust_dialog.title = "Server identity"
- _server_trust_dialog.ok_button_text = "Trust & Connect"
- _server_trust_dialog.cancel_button_text = "Cancel"
- _server_trust_dialog.confirmed.connect(_confirm_server_trust)
- _server_trust_dialog.canceled.connect(
- _network_session.resolve_server_trust.bind(false)
- )
- _game_ui.add_child(_server_trust_dialog)
- _pending_trust_changed = is_changed
- if is_changed:
- _server_trust_dialog.dialog_text = (
- "Server identity changed.\n\n"
- + "This may mean the server was reinstalled, moved, or is being "
- + "impersonated.\n\nExpected:\n%s\n\nReceived:\n%s"
- % [
- NetworkIdentityCrypto.format_fingerprint(expected_fingerprint),
- NetworkIdentityCrypto.format_fingerprint(received_fingerprint),
- ]
- )
- _server_trust_dialog.ok_button_text = "Trust New Identity"
- else:
- _server_trust_dialog.dialog_text = (
- "First time connecting to this server.\n\n%s\n\nHost identity:\n%s"
- % [
- endpoint,
- NetworkIdentityCrypto.format_fingerprint(received_fingerprint),
- ]
- )
- _server_trust_dialog.ok_button_text = "Trust & Connect"
- _server_trust_dialog.popup_centered(Vector2i(560, 360))
- _configure_popup_dialog.call_deferred(
- _server_trust_dialog,
- _server_trust_dialog.get_cancel_button(),
- )
-
-
-func _confirm_server_trust() -> void:
- if _pending_trust_changed:
- _pending_trust_changed = false
- _server_trust_dialog.dialog_text = (
- "Replace the saved server identity pin?\n\n"
- + "Only continue if you expected this server identity to change."
- )
- _server_trust_dialog.ok_button_text = "Replace Pin"
- _server_trust_dialog.popup_centered(Vector2i(520, 260))
- _configure_popup_dialog.call_deferred(
- _server_trust_dialog,
- _server_trust_dialog.get_cancel_button(),
- )
- return
- if _server_trust_dialog != null:
- _server_trust_dialog.hide()
- _network_session.resolve_server_trust(true)
-
-
-func _on_peer_identity_observed(_peer_id: int, status: String) -> void:
- if status != "New identity using a familiar name":
- return
- if _identity_notice_dialog == null:
- _identity_notice_dialog = AcceptDialog.new()
- _identity_notice_dialog.title = "Player identity"
- _game_ui.add_child(_identity_notice_dialog)
- _identity_notice_dialog.dialog_text = (
- "New identity using a familiar name.\n\n"
- + "This may be a different player using the same display name."
- )
- _identity_notice_dialog.popup_centered(Vector2i(480, 240))
- _configure_popup_dialog.call_deferred(
- _identity_notice_dialog,
- _identity_notice_dialog.get_ok_button(),
- )
-
-
-func _configure_popup_dialog(
- dialog: Window,
- preferred_control: Control = null,
-) -> void:
- if dialog != null and is_instance_valid(dialog) and dialog.visible:
- FileDialogControllerNavigation.configure_scope(
- dialog, preferred_control
- )
-
-
func _input(event: InputEvent) -> void:
- # Do not let controller aliases close or manipulate the world/menu behind
- # the modal on-screen keyboard. The keyboard consumes the event itself.
- if _game_ui.is_controller_text_entry_open():
- return
- if _handle_data_root_controller_input(event):
- get_viewport().set_input_as_handled()
- return
- if _game_ui.is_input_mapping_capturing():
- return
if (
- not (
- event.is_action_pressed("ui_cancel")
- or event.is_action_pressed("open_system_menu")
- )
+ not event.is_action_pressed("ui_cancel")
or (event is InputEventKey and event.echo)
):
return
- var pause_open_requested: bool = _is_pause_open_request(event)
var title_screen: TitleScreenType = _game_ui.get_title_screen()
if title_screen.visible or not _gameplay_started:
return
var pause_menu: PauseMenuType = _game_ui.get_pause_menu()
var fishing_shop: FishingShopType = _game_ui.get_fishing_shop()
- if _game_ui.get_player_storage().consume_escape():
- get_viewport().set_input_as_handled()
- return
- if _game_ui.get_decor_shop().consume_escape():
- get_viewport().set_input_as_handled()
- return
if fishing_shop.consume_escape():
get_viewport().set_input_as_handled()
return
@@ -1552,8 +215,7 @@ func _input(event: InputEvent) -> void:
get_viewport().set_input_as_handled()
return
if (
- pause_open_requested
- and not _water_recovery.is_recovery_active()
+ not _water_recovery.is_recovery_active()
and _fishing_spot.can_open_system_menu()
):
_game_ui.close_player_menu_for_game_menu()
@@ -1561,128 +223,13 @@ func _input(event: InputEvent) -> void:
get_viewport().set_input_as_handled()
-func _handle_data_root_controller_input(event: InputEvent) -> bool:
- if _game_ui.is_controller_text_entry_open():
- return false
- var button_event := event as InputEventJoypadButton
- if button_event == null or not button_event.pressed:
- return false
- var setup_visible: bool = (
- _data_setup_dialog != null and _data_setup_dialog.visible
- )
- var picker_visible: bool = (
- _data_folder_dialog != null and _data_folder_dialog.visible
- )
- if not setup_visible and not picker_visible:
- return false
- var picker_scope: Window = (
- FileDialogControllerNavigation.active_scope(_data_folder_dialog)
- if picker_visible
- else null
- )
- var use_mapping: bool = _controller_mapping_manager != null
- var accept_pressed: bool = (
- _controller_mapping_manager.event_matches_role(
- event,
- ControllerMappingManagerType.ROLE_A,
- )
- if use_mapping
- else (
- button_event.button_index == JOY_BUTTON_A
- or event.is_action_pressed("ui_accept")
- )
- )
- var cancel_pressed: bool = (
- _controller_mapping_manager.event_matches_role(
- event,
- ControllerMappingManagerType.ROLE_B,
- )
- if use_mapping
- else (
- button_event.button_index == JOY_BUTTON_B
- or event.is_action_pressed("ui_cancel")
- )
- )
- if cancel_pressed:
- if picker_visible:
- if picker_scope != null and picker_scope != _data_folder_dialog:
- picker_scope.hide()
- else:
- _on_data_folder_picker_canceled()
- else:
- _data_setup_dialog.get_cancel_button().pressed.emit()
- return true
- if not accept_pressed:
- return false
- var focused: Control = (
- picker_scope.gui_get_focus_owner()
- if picker_visible
- else _data_setup_dialog.gui_get_focus_owner()
- )
- if (
- (focused is LineEdit or focused is TextEdit)
- and _game_ui.request_controller_text_entry_for(focused)
- ):
- return true
- # Buttons are activated once through Godot's native ui_accept path. This
- # handler only owns modal back behavior and controller text entry.
- return false
-
-
-func _is_pause_open_request(event: InputEvent) -> bool:
- return (
- event.is_action_pressed("open_system_menu")
- or event is InputEventKey
- )
-
-
func _unhandled_input(event: InputEvent) -> void:
- if (
- _gameplay_started
- and event.is_action_pressed("fish_primary")
- and not (event is InputEventKey and event.echo)
- and _fishing_spot.try_use_active_non_fishing_item()
- ):
- get_viewport().set_input_as_handled()
- return
if (
not _gameplay_started
or not event.is_action_pressed("interact")
or (event is InputEventKey and event.echo)
):
return
- var home_owner: String = _network_home.get_local_home_owner_fingerprint()
- if not home_owner.is_empty():
- if _home_world.is_avatar_near_interior_exit(_player, home_owner):
- if _network_home.request_exit():
- _game_ui.set_storage_prompt_visible(false)
- get_viewport().set_input_as_handled()
- return
- if (
- _network_home.can_local_decorate()
- and _fishing_spot.can_open_fishing_shop()
- and _game_ui.get_player_storage().open_storage_from_home(true)
- ):
- _game_ui.set_storage_prompt_visible(false)
- get_viewport().set_input_as_handled()
- return
- var nearby_home_owner: String = _home_world.find_nearby_exterior_owner(
- _player
- )
- if not nearby_home_owner.is_empty():
- if _network_home.request_enter(nearby_home_owner):
- _game_ui.set_storage_prompt_visible(false)
- get_viewport().set_input_as_handled()
- return
- if (
- _decor_shop_interaction != null
- and _decor_shop_interaction.is_local_player_in_range()
- and _fishing_spot.can_open_fishing_shop()
- and _game_ui.get_decor_shop().open_shop()
- ):
- _game_ui.set_shop_prompt_visible(false)
- get_viewport().set_input_as_handled()
- return
if (
_shop_interaction != null
and _shop_interaction.is_local_player_in_range()
@@ -1693,103 +240,33 @@ func _unhandled_input(event: InputEvent) -> void:
get_viewport().set_input_as_handled()
-func _process(delta: float) -> void:
- if _dedicated_runtime:
- _dedicated_metrics_accumulator += delta
- if _dedicated_metrics_accumulator >= 60.0:
- _dedicated_metrics_accumulator = fmod(
- _dedicated_metrics_accumulator,
- 60.0,
- )
- _print_dedicated_metrics()
- return
- var show_decor_shop_prompt := _can_show_decor_shop_prompt()
- var show_shop_prompt := show_decor_shop_prompt or _can_show_shop_prompt()
- var shop_prompt_anchor := (
- _decor_shop_interaction.get_prompt_anchor_position()
- if show_decor_shop_prompt
- else (
- _shop_interaction.get_prompt_anchor_position()
- if _shop_interaction != null else Vector3.ZERO
- )
- )
- _game_ui.set_shop_prompt_visible(
- show_shop_prompt,
- shop_prompt_anchor,
- )
- var home_prompt := _get_home_prompt()
- _game_ui.set_storage_prompt_visible(
- not home_prompt.is_empty(),
- home_prompt.get("anchor", Vector3.ZERO),
- str(home_prompt.get("label", "enter RV")),
- )
- _game_ui.set_rv_storage_button_visible(
- _can_show_rv_storage_button()
- )
+func _process(_delta: float) -> void:
+ _game_ui.set_shop_prompt_visible(_can_show_shop_prompt())
func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
if settings == null:
return
- var current_window_mode: DisplayServer.WindowMode = (
- DisplayServer.window_get_mode()
- )
- var requested_window_mode: DisplayServer.WindowMode = resolve_window_mode(
- settings.fullscreen_enabled,
- current_window_mode,
- )
- if current_window_mode != requested_window_mode:
- DisplayServer.window_set_mode(requested_window_mode)
_apply_world_pixelation(settings.world_pixel_size)
_ui_pixelation.set_pixel_size(settings.ui_pixel_size)
- _ui_pixelation.set_on_screen_keyboard_enabled(
- settings.on_screen_keyboard_enabled
- )
_game_ui.get_title_screen().set_world_pixelation(
settings.world_pixel_size
)
_player.apply_camera_settings(
settings.mouse_camera_sensitivity,
settings.controller_camera_sensitivity,
- settings.invert_camera_y,
- settings.swap_hotbar_camera_scroll,
+ settings.invert_camera_y
)
_fishing_spot.configure_accessibility_auto_click(
settings.auto_click_enabled,
settings.auto_click_interval
)
- _game_ui.set_edge_docks(
- settings.chat_dock_right,
- settings.paint_dock_right,
- settings.chat_mobile_mode,
- )
-
-
-static func resolve_window_mode(
- fullscreen_enabled: bool,
- current_mode: DisplayServer.WindowMode,
-) -> DisplayServer.WindowMode:
- var is_fullscreen: bool = current_mode in [
- DisplayServer.WINDOW_MODE_FULLSCREEN,
- DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN,
- ]
- if fullscreen_enabled:
- return (
- current_mode
- if is_fullscreen
- else DisplayServer.WINDOW_MODE_FULLSCREEN
- )
- if is_fullscreen:
- return DisplayServer.WINDOW_MODE_WINDOWED
- return current_mode
func _apply_world_pixelation(pixel_size: int) -> void:
var root_viewport: Viewport = get_viewport()
root_viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_NEAREST
- root_viewport.scaling_3d_scale = (
- _performance_profile.get_world_render_scale()
- )
+ root_viewport.scaling_3d_scale = 1.0
root_viewport.msaa_3d = Viewport.MSAA_DISABLED
root_viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED
root_viewport.use_taa = false
@@ -1812,348 +289,22 @@ func _reset_pixelation() -> void:
func _set_gameplay_active(active: bool) -> void:
- var was_gameplay_started: bool = _gameplay_started
_gameplay_started = active
- _shoreline_ambience.set_active(active)
- _rain_ambience.set_active(active)
- if active and not was_gameplay_started:
- var current_hour: float = _world_time.get_time_hours()
- _dusk_music_played_for_natural_day = (
- current_hour >= WorldTimeServiceType.DUSK_START_HOUR
- or current_hour < WorldTimeServiceType.DAWN_START_HOUR
- )
- elif not active:
- _dusk_music.stop()
- _title_background.visible = not active
_world_pixelation.set_gameplay_active(active)
_ui_pixelation.set_gameplay_active(active)
if not active:
_player.item_effects.reset_all()
_player.set_movement_enabled(active)
_player.set_camera_input_enabled(active)
- _player.set_camera_active(active)
_fishing_spot.set_gameplay_input_enabled(active)
- _gathering_controller.set_gameplay_input_enabled(active)
_water_recovery.set_recovery_enabled(active)
_game_ui.set_gameplay_ui_enabled(active)
_save_manager.set_autosave_enabled(active)
- if active:
- _player_jobs.begin_progression_session()
- _refresh_active_hotbar_item()
- else:
- _player_jobs.end_progression_session()
- _set_player_menu_backdrop_visible(false)
- _set_shop_backdrop_visible(false)
-func _on_natural_time_advanced(advanced_hours: float) -> void:
- if not _gameplay_started or advanced_hours <= 0.0:
- return
- var current_hour: float = _world_time.get_time_hours()
- var previous_hour: float = fposmod(
- current_hour - advanced_hours,
- WorldTimeServiceType.HOURS_PER_DAY,
- )
- if _natural_interval_crosses_hour(
- previous_hour,
- advanced_hours,
- WorldTimeServiceType.DAWN_START_HOUR,
- ):
- _dusk_music_played_for_natural_day = false
- if (
- not _dusk_music_played_for_natural_day
- and _natural_interval_crosses_hour(
- previous_hour,
- advanced_hours,
- WorldTimeServiceType.DUSK_START_HOUR,
- )
- ):
- _dusk_music_played_for_natural_day = true
- if (
- _dusk_music.stream != null
- and not _new_game_music.playing
- ):
- _dusk_music.play(0.0)
-
-
-static func _natural_interval_crosses_hour(
- previous_hour: float,
- advanced_hours: float,
- target_hour: float,
-) -> bool:
- if advanced_hours >= WorldTimeServiceType.HOURS_PER_DAY:
- return true
- var distance_to_target: float = fposmod(
- target_hour - previous_hour,
- WorldTimeServiceType.HOURS_PER_DAY,
- )
- return (
- distance_to_target > 0.0
- and distance_to_target
- <= advanced_hours + TIME_CROSSING_EPSILON_HOURS
- )
-
-
-func _set_player_menu_backdrop_visible(requested_visible: bool) -> void:
- if _player_menu_backdrop_tween != null:
- _player_menu_backdrop_tween.kill()
- _player_menu_backdrop_tween = null
- var should_show: bool = requested_visible and _gameplay_started
- if should_show:
- var was_visible: bool = _player_menu_backdrop.visible
- _player_menu_backdrop.visible = true
- if not was_visible:
- _player_menu_backdrop.modulate.a = 0.0
- _player_menu_backdrop_tween = create_tween()
- _player_menu_backdrop_tween.tween_property(
- _player_menu_backdrop,
- "modulate:a",
- 1.0,
- UIMotion.PLAYER_MENU_ENTER_DURATION,
- ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
- return
- if not _player_menu_backdrop.visible:
- return
- _player_menu_backdrop_tween = create_tween()
- _player_menu_backdrop_tween.tween_property(
- _player_menu_backdrop,
- "modulate:a",
- 0.0,
- UIMotion.PLAYER_MENU_EXIT_DURATION,
- ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
- _player_menu_backdrop_tween.finished.connect(
- func() -> void:
- _player_menu_backdrop.visible = false
- _player_menu_backdrop.modulate.a = 1.0
- _player_menu_backdrop_tween = null
- )
-
-
-func _set_shop_backdrop_visible(requested_visible: bool) -> void:
- if _shop_backdrop_tween != null:
- _shop_backdrop_tween.kill()
- _shop_backdrop_tween = null
- var should_show: bool = requested_visible and _gameplay_started
- if should_show:
- var was_visible: bool = _shop_backdrop.visible
- _shop_backdrop.visible = true
- if not was_visible:
- _shop_backdrop.modulate.a = 0.0
- _shop_backdrop_tween = create_tween()
- _shop_backdrop_tween.tween_property(
- _shop_backdrop,
- "modulate:a",
- 1.0,
- UIMotion.PLAYER_MENU_ENTER_DURATION,
- ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
- return
- if not _shop_backdrop.visible:
- return
- _shop_backdrop_tween = create_tween()
- _shop_backdrop_tween.tween_property(
- _shop_backdrop,
- "modulate:a",
- 0.0,
- UIMotion.PLAYER_MENU_EXIT_DURATION,
- ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
- _shop_backdrop_tween.finished.connect(
- func() -> void:
- _shop_backdrop.visible = false
- _shop_backdrop.modulate.a = 1.0
- _shop_backdrop_tween = null
- )
-
-
-func _resize_native_overlays() -> void:
- if not is_node_ready():
- return
- _player_menu_backdrop.position = Vector2.ZERO
- _player_menu_backdrop.size = Vector2(get_window().size)
- _shop_backdrop.position = Vector2.ZERO
- _shop_backdrop.size = Vector2(get_window().size)
-
-
-func _on_new_game_requested(
- world_layout: StringName,
- world_seed: int,
-) -> void:
+func _on_gameplay_requested() -> void:
if _gameplay_started or _quit_in_progress:
return
- _start_new_game_music()
- if not _apply_world(world_layout, world_seed, true):
- _show_title_music(true)
- _game_ui.get_title_screen().report_network_error(
- "Could not prepare that world. Try another seed or the starter island."
- )
- return
- if not _prepare_host_world(world_layout, world_seed):
- _show_title_music(true)
- _game_ui.get_title_screen().report_network_error(
- "Could not prepare that world for hosting."
- )
- return
- if not _prepare_private_host():
- _show_title_music(true)
- return
- if (
- not _save_manager.delete_progression_save()
- or not _save_manager.initialize_new_game(world_seed, world_layout)
- ):
- _network_session.disconnect_session("New Game setup failed.")
- _show_title_music(true)
- _game_ui.get_title_screen().report_network_error(
- "Could not initialize local progression. Existing data was preserved where possible."
- )
- return
- _enter_gameplay()
-
-
-func _on_continue_game_requested() -> void:
- if _gameplay_started or _quit_in_progress:
- return
- if not _save_manager.load_player_data():
- _game_ui.get_title_screen().report_network_error(
- "Failed to load save. The original was preserved."
- )
- return
- var world_layout: StringName = _save_manager.get_world_layout()
- var world_seed: int = _save_manager.get_world_seed()
- if (
- not _apply_world(world_layout, world_seed, true)
- or not _prepare_host_world(world_layout, world_seed)
- ):
- _game_ui.get_title_screen().report_network_error(
- "The saved world could not be loaded. The save was preserved."
- )
- return
- if not _prepare_private_host():
- return
- _enter_gameplay()
-
-
-func _on_slot_play_requested(slot_id: String) -> void:
- if _gameplay_started or _quit_in_progress:
- return
- if not _save_slots.activate_slot(slot_id):
- _game_ui.get_title_screen().report_network_error(
- "The selected save slot could not be opened."
- )
- return
- if not _save_manager.load_player_data():
- _game_ui.get_title_screen().report_network_error(
- "Failed to load that save slot. The original was preserved."
- )
- return
- var world_layout: StringName = _save_manager.get_world_layout()
- var world_seed: int = _save_manager.get_world_seed()
- if (
- not _apply_world(world_layout, world_seed, true)
- or not _prepare_host_world(world_layout, world_seed)
- ):
- _game_ui.get_title_screen().report_network_error(
- "The saved world could not be loaded. The save was preserved."
- )
- return
- if not _prepare_private_host():
- return
- _save_slots.mark_played(slot_id)
- _enter_gameplay()
-
-
-func _on_new_slot_requested(
- display_name: String,
- world_layout: StringName,
- world_seed: int,
- duplicate_source_slot_id: String,
-) -> void:
- if _gameplay_started or _quit_in_progress:
- return
- var created: Dictionary = (
- _save_slots.duplicate_slot(
- duplicate_source_slot_id,
- display_name,
- world_layout,
- world_seed,
- )
- if not duplicate_source_slot_id.is_empty()
- else _save_slots.create_empty_slot(display_name)
- )
- if not bool(created.get("ok", false)):
- _game_ui.get_title_screen().report_network_error(
- str(created.get("message", "The save slot could not be created."))
- )
- return
- var slot_id: String = str(created.get("slot_id", ""))
- if not _save_slots.activate_slot(slot_id):
- _game_ui.get_title_screen().report_network_error(
- "The new save slot could not be selected."
- )
- return
- if not duplicate_source_slot_id.is_empty():
- _on_slot_play_requested(slot_id)
- return
- _start_new_game_music()
- if not _apply_world(world_layout, world_seed, true):
- _show_title_music(true)
- _game_ui.get_title_screen().report_network_error(
- "Could not prepare that world. Try another seed or the starter island."
- )
- return
- if not _prepare_host_world(world_layout, world_seed):
- _show_title_music(true)
- _game_ui.get_title_screen().report_network_error(
- "Could not prepare that world for hosting."
- )
- return
- if not _prepare_private_host():
- _show_title_music(true)
- return
- if (
- not _save_manager.delete_progression_save()
- or not _save_manager.initialize_new_game(world_seed, world_layout)
- ):
- _network_session.disconnect_session("New Game setup failed.")
- _show_title_music(true)
- _game_ui.get_title_screen().report_network_error(
- "Could not initialize local progression."
- )
- return
- _save_slots.mark_played(slot_id)
- _enter_gameplay()
-
-
-func _prepare_private_host() -> bool:
- _join_requested_from_title = false
- _join_requested_from_pause = false
- if _network_session.state in [
- NetworkSessionType.State.CONNECTION_FAILED,
- NetworkSessionType.State.SERVER_LOST,
- ]:
- _network_session.reset_failure()
- if not _network_session.start_private_host(
- NetworkSessionType.DEFAULT_PORT,
- NetworkSessionType.DEFAULT_PRIVATE_HOST_PORT_ATTEMPTS,
- ):
- _game_ui.get_title_screen().report_network_error(
- "Could not start the private multiplayer session."
- )
- return false
- return true
-
-
-func _prepare_host_world(
- world_layout: StringName,
- world_seed: int,
-) -> bool:
- if _network_session.state in [
- NetworkSessionType.State.CONNECTION_FAILED,
- NetworkSessionType.State.SERVER_LOST,
- ]:
- _network_session.reset_failure()
- return _network_session.set_host_world(world_layout, world_seed)
-
-
-func _enter_gameplay() -> void:
_fade_out_title_music()
_game_ui.get_title_screen().hide()
_set_gameplay_active(true)
@@ -2163,317 +314,12 @@ func _on_return_to_title_requested() -> void:
if _quit_in_progress:
return
var pause_menu: PauseMenuType = _game_ui.get_pause_menu()
- if not _save_manager.save_world_time_checkpoint():
- pause_menu.report_network_error(
- "Could not save progression before returning to title."
- )
- return
pause_menu.close_for_title_transition()
- _network_session.disconnect_session("Returned to title.")
_set_gameplay_active(false)
_game_ui.get_title_screen().reopen()
_show_title_music(true)
-func _on_title_join_game_requested(endpoint: String) -> void:
- if _quit_in_progress or _gameplay_started:
- return
- var active_slot: Dictionary = _save_slots.ensure_active_slot()
- if not bool(active_slot.get("ok", false)):
- _game_ui.get_title_screen().report_network_error(
- str(active_slot.get("message", "Local progression is unavailable."))
- )
- return
- if _network_session.state != NetworkSessionType.State.INACTIVE:
- _network_session.disconnect_session("Preparing direct connection.")
- _join_requested_from_title = true
- _join_requested_from_pause = false
- _pending_join_endpoint = endpoint
- if not _network_session.join_direct(endpoint):
- _join_requested_from_title = false
-
-
-func _on_pause_join_game_requested(endpoint: String) -> void:
- if _quit_in_progress or not _gameplay_started:
- return
- if not _save_manager.save_world_time_checkpoint():
- _game_ui.get_pause_menu().report_network_error(
- "Could not save progression before leaving this session."
- )
- return
- _join_requested_from_pause = true
- _join_requested_from_title = false
- _pending_join_endpoint = endpoint
- _game_ui.get_pause_menu().close_for_title_transition()
- var preserved_public_join: bool = (
- _discovery.preserve_public_join_for_session_switch()
- )
- _network_session.disconnect_session("Connecting to another game.")
- if not _network_session.join_direct(endpoint):
- if preserved_public_join:
- _discovery.cancel_pending_public_join()
- _handle_failed_session_switch(
- "Could not begin the direct connection."
- )
-
-
-func _on_friend_request_received(
- request_id: String,
- fingerprint: String,
- display_name: String,
-) -> void:
- _queue_social_prompt({
- "kind": "friend_request",
- "request_id": request_id,
- "fingerprint": fingerprint,
- "display_name": display_name,
- })
-
-
-func _on_friend_invite_received(
- fingerprint: String,
- display_name: String,
- room: Dictionary,
-) -> void:
- if (
- not _relationships.is_friend(fingerprint)
- or _relationships.is_blocked(fingerprint)
- ):
- return
- _queue_social_prompt({
- "kind": "friend_invite",
- "fingerprint": fingerprint,
- "display_name": display_name,
- "room": room.duplicate(true),
- })
-
-
-func _queue_social_prompt(details: Dictionary) -> void:
- _social_prompt_queue.append(details.duplicate(true))
- _show_next_social_prompt()
-
-
-func _show_next_social_prompt() -> void:
- if (
- not _active_social_prompt.is_empty()
- or _game_ui.is_social_prompt_open()
- or _social_prompt_queue.is_empty()
- ):
- return
- _active_social_prompt = _social_prompt_queue.pop_front()
- var kind := str(_active_social_prompt.get("kind", ""))
- var display_name := str(
- _active_social_prompt.get("display_name", "Player")
- )
- var shown: bool = false
- if kind == "friend_request":
- shown = _game_ui.show_social_prompt(
- "%s wants to add you as a friend.\n" % display_name
- + "Friendships are saved on this device.",
- "accept",
- "decline",
- )
- elif kind == "friend_invite":
- var room: Dictionary = _active_social_prompt.get("room", {})
- var room_name := str(room.get("room_name", "their room"))
- shown = _game_ui.show_social_prompt(
- "%s invited you to join %s." % [display_name, room_name],
- "join",
- "not now",
- )
- if not shown:
- _active_social_prompt.clear()
- call_deferred("_show_next_social_prompt")
-
-
-func _on_social_prompt_accepted() -> void:
- var details: Dictionary = _active_social_prompt.duplicate(true)
- _active_social_prompt.clear()
- match str(details.get("kind", "")):
- "friend_request":
- _network_player_list.respond_friend_request(
- str(details.get("request_id", "")), true
- )
- "friend_invite":
- var room: Dictionary = details.get("room", {})
- _pending_social_invite_join = true
- _pending_social_invite_gameplay = _gameplay_started
- if room.is_empty() or not _discovery.prepare_public_join(room):
- _pending_social_invite_join = false
- _report_social_join_error(
- "This person needs to be online to do this."
- )
- call_deferred("_show_next_social_prompt")
-
-
-func _on_social_prompt_declined() -> void:
- var details: Dictionary = _active_social_prompt.duplicate(true)
- _active_social_prompt.clear()
- if str(details.get("kind", "")) == "friend_request":
- _network_player_list.respond_friend_request(
- str(details.get("request_id", "")), false
- )
- call_deferred("_show_next_social_prompt")
-
-
-func _on_social_public_join_prepared(endpoint: String) -> void:
- if not _pending_social_invite_join:
- return
- var from_gameplay := _pending_social_invite_gameplay
- _pending_social_invite_join = false
- _pending_social_invite_gameplay = false
- if from_gameplay:
- _on_pause_join_game_requested(endpoint)
- else:
- _on_title_join_game_requested(endpoint)
-
-
-func _on_social_public_join_status_changed(
- message: String,
- is_error: bool,
-) -> void:
- if not _pending_social_invite_join or not is_error:
- return
- _pending_social_invite_join = false
- _pending_social_invite_gameplay = false
- _report_social_join_error(message)
-
-
-func _report_social_join_error(message: String) -> void:
- if _gameplay_started:
- _game_ui.get_pause_menu().report_network_error(message)
- else:
- _game_ui.get_title_screen().report_network_error(message)
-
-
-func _on_network_join_authenticated() -> void:
- var connected_endpoint: ConnectionEndpoint = (
- _network_session.get_current_endpoint()
- )
- var server_metadata: Dictionary = (
- _network_session.get_last_server_metadata()
- )
- if connected_endpoint != null:
- _saved_servers.record_successful_connection(
- connected_endpoint,
- int(server_metadata.get("max_players", 0)),
- str(server_metadata.get("server_display_name", "")),
- int(server_metadata.get("protocol_version", 0)),
- int(server_metadata.get("player_count", 0)),
- )
- if _join_requested_from_title:
- var inspection = _save_manager.inspect_save()
- var progression_ready: bool = (
- _save_manager.load_player_data()
- if inspection.can_continue()
- else _save_manager.initialize_new_game()
- )
- if not progression_ready:
- _network_session.disconnect_session(
- "Local progression could not be prepared."
- )
- _game_ui.get_title_screen().report_network_error(
- "Your local progression could not be prepared."
- )
- _join_requested_from_title = false
- return
- _save_slots.mark_played(_save_slots.get_active_slot_id())
- if not _apply_joined_world(server_metadata):
- return
- _fade_out_title_music()
- _game_ui.get_title_screen().hide()
- _set_gameplay_active(true)
- elif _join_requested_from_pause:
- if not _apply_joined_world(server_metadata):
- return
- _set_gameplay_active(true)
- _join_requested_from_title = false
- _join_requested_from_pause = false
- _pending_join_endpoint = ""
-
-
-func _on_network_connection_error(message: String) -> void:
- var failed_endpoint: ConnectionEndpoint = (
- _network_session.get_current_endpoint()
- )
- if failed_endpoint != null:
- _saved_servers.record_connection_failure(
- failed_endpoint,
- _connection_result_code(message),
- )
- if _join_requested_from_title:
- _game_ui.get_title_screen().report_network_error(message)
- elif _join_requested_from_pause:
- _handle_failed_session_switch(message)
-
-
-func _connection_result_code(message: String) -> String:
- var lowered: String = message.to_lower()
- if lowered.contains("protocol"):
- return "PROTOCOL_MISMATCH"
- if lowered.contains("full"):
- return "SERVER_FULL"
- if lowered.contains("cancel"):
- return "CANCELLED"
- if lowered.contains("timed out"):
- return "TIMEOUT"
- return "UNAVAILABLE"
-
-
-func _handle_failed_session_switch(message: String) -> void:
- _join_requested_from_pause = false
- _set_gameplay_active(false)
- var title_screen: TitleScreenType = _game_ui.get_title_screen()
- title_screen.reopen()
- title_screen.open_join_game_page(_pending_join_endpoint)
- title_screen.report_network_error(message)
- _show_title_music(true)
-
-
-func _on_network_server_lost(message: String) -> void:
- if not _gameplay_started:
- return
- _join_requested_from_title = false
- _join_requested_from_pause = false
- _pending_join_endpoint = ""
- _set_gameplay_active(false)
- var title_screen: TitleScreenType = _game_ui.get_title_screen()
- title_screen.reopen_to_menu()
- title_screen.report_network_error(message)
- _show_title_music(true)
-
-
-func _on_local_respawn_completed(entry_position: Vector3) -> void:
- if _network_session.is_host():
- _network_session.publish_authoritative_teleport(
- _player.get_network_peer_id()
- )
- elif _network_session.is_joined_client():
- _network_session.request_safe_respawn(entry_position)
-
-
-func _on_remote_recovery_requested(
- peer_id: int,
- entry_position: Vector3,
-) -> void:
- if not _network_session.is_host():
- return
- var avatar: PlayerType = _player_spawn_service.get_avatar(peer_id)
- if avatar == null:
- return
- _network_fishing.cancel_peer_attempt(
- peer_id,
- "Fishing attempt ended."
- )
- var target_position := _test_world.get_water_recovery_position(
- entry_position
- )
- target_position.y += _water_recovery.respawn_height_offset
- avatar.global_position = target_position
- avatar.velocity = Vector3.ZERO
- _network_session.publish_authoritative_teleport(peer_id)
-
-
func _on_reset_progress_requested() -> void:
if _quit_in_progress:
return
@@ -2481,15 +327,7 @@ func _on_reset_progress_requested() -> void:
if not _save_manager.delete_progression_save():
pause_menu.report_reset_failure()
return
- var world_seed: int = PlayerSaveManager.roll_world_seed()
- if not _save_manager.initialize_new_game(
- world_seed,
- WorldLayoutType.GENERATED,
- ):
- pause_menu.report_reset_failure()
- return
- _network_session.disconnect_session("Progress reset.")
- if not _apply_world(WorldLayoutType.GENERATED, world_seed, true):
+ if not _save_manager.initialize_new_game():
pause_menu.report_reset_failure()
return
pause_menu.close_for_title_transition()
@@ -2498,237 +336,32 @@ func _on_reset_progress_requested() -> void:
_show_title_music(true)
-func _apply_joined_world(server_metadata: Dictionary) -> bool:
- var world_layout: StringName = WorldLayoutType.normalized(
- server_metadata.get(
- "world_layout",
- String(WorldLayoutType.GENERATED),
- ),
- &"",
- )
- var world_seed: int = int(server_metadata.get("world_seed", 0))
- if (
- not world_layout.is_empty()
- and _apply_world(world_layout, world_seed, true)
- ):
- return true
- _network_session.disconnect_session("World generation failed.")
- _join_requested_from_title = false
- _join_requested_from_pause = false
- _set_gameplay_active(false)
- var title_screen: TitleScreenType = _game_ui.get_title_screen()
- title_screen.reopen_to_menu()
- title_screen.report_network_error(
- "The host's world could not be built locally."
- )
- _show_title_music(true)
- return false
-
-
-func _apply_world(
- world_layout: StringName,
- world_seed: int,
- reposition_player: bool,
-) -> bool:
- if not _test_world.activate_world(world_layout, world_seed):
- return false
- var spawn_transform: Transform3D = _test_world.get_player_spawn_transform()
- _player_spawn_service.set_spawn_transform(spawn_transform)
- if reposition_player:
- _player.global_transform = spawn_transform
- _player.velocity = Vector3.ZERO
- if _application_initialized:
- _refresh_active_world_bindings()
- _network_world_spawns.refresh_world_context()
- _network_home.refresh_world_presentations()
- if _application_initialized and not _dedicated_runtime:
- _water_recovery.update_world_context(
- spawn_transform,
- _test_world.get_player_water_triggers(),
- _test_world.get_safe_respawn_points(),
- Callable(_test_world, "get_water_recovery_position"),
- )
- _shoreline_ambience.configure(
- _player,
- _test_world.get_saltwater_shoreline_mesh(),
- )
- return true
-
-
-func _refresh_active_world_bindings() -> void:
- if is_instance_valid(_shop_interaction):
- if _shop_interaction.local_player_range_changed.is_connected(
- _on_shop_range_changed
- ):
- _shop_interaction.local_player_range_changed.disconnect(
- _on_shop_range_changed
- )
- if is_instance_valid(_storage_interaction):
- if _storage_interaction.local_player_range_changed.is_connected(
- _on_storage_range_changed
- ):
- _storage_interaction.local_player_range_changed.disconnect(
- _on_storage_range_changed
- )
- if is_instance_valid(_decor_shop_interaction):
- if _decor_shop_interaction.local_player_range_changed.is_connected(
- _on_decor_shop_range_changed
- ):
- _decor_shop_interaction.local_player_range_changed.disconnect(
- _on_decor_shop_range_changed
- )
- _shop_interaction = _test_world.get_fishing_shop()
- _decor_shop_interaction = _test_world.get_decor_shop()
- _storage_interaction = _test_world.get_player_storage()
- _network_sale.set_shop_interaction(_shop_interaction)
- _network_shop.set_shop_interaction(_shop_interaction)
- _network_shop.set_decor_shop_interaction(_decor_shop_interaction)
- if _dedicated_runtime:
- return
- if _shop_interaction != null:
- _shop_interaction.setup_local_player(_player)
- _shop_interaction.local_player_range_changed.connect(
- _on_shop_range_changed
- )
- if _storage_interaction != null:
- _storage_interaction.setup_local_player(_player)
- _storage_interaction.local_player_range_changed.connect(
- _on_storage_range_changed
- )
- if _decor_shop_interaction != null:
- _decor_shop_interaction.setup_local_player(_player)
- _decor_shop_interaction.local_player_range_changed.connect(
- _on_decor_shop_range_changed
- )
- _game_ui.set_world_interactions(
- _shop_interaction,
- _storage_interaction,
- _decor_shop_interaction,
- )
- _on_shop_range_changed(
- _shop_interaction != null
- and _shop_interaction.is_local_player_in_range()
- )
- _on_storage_range_changed(
- _storage_interaction != null
- and _storage_interaction.is_local_player_in_range()
- )
- _on_decor_shop_range_changed(
- _decor_shop_interaction != null
- and _decor_shop_interaction.is_local_player_in_range()
- )
-
-
func _on_water_recovery_starting() -> void:
- _local_recovery_attempt_id = (
- "recovery:%s"
- % Crypto.new().generate_random_bytes(16).hex_encode()
- )
- _network_session.request_recovery_presentation(
- true, _local_recovery_attempt_id
- )
_game_ui.close_player_menu_for_water_recovery()
_game_ui.get_pause_menu().close_for_water_recovery()
_game_ui.get_fishing_shop().close_for_water_recovery()
- _game_ui.get_decor_shop().close_for_water_recovery()
- _game_ui.get_player_storage().close_for_water_recovery()
-
-
-func _on_water_recovery_finished() -> void:
- if _local_recovery_attempt_id.is_empty():
- return
- _network_session.request_recovery_presentation(
- false, _local_recovery_attempt_id
- )
- _local_recovery_attempt_id = ""
-
-
-func _on_remote_recovery_presentation_changed(
- peer_id: int,
- active: bool,
- _attempt_id: String,
-) -> void:
- var avatar: PlayerType = _player_spawn_service.get_avatar(peer_id)
- if avatar == null or avatar == _player:
- return
- avatar.set_water_recovery_active(active)
- avatar.set_remote_recovery_presentation(active)
func _on_active_hotbar_item_changed(
_slot_index: int,
- kind: int,
- identity: StringName,
+ item_id: StringName,
) -> void:
if _fishing_spot.state != FishingSpotType.FishingState.READY:
return
- var item_id: StringName = (
- identity
- if kind == PlayerHotbarType.AssignmentKind.ITEM
- else StringName()
- )
var item = item_catalog.get_item_by_id(item_id)
var active_is_rod: bool = (
item != null
- and item.is_available()
and item.category == ItemDataType.Category.ROD
and _player.bag.owns_item(item_id)
)
- var active_is_art_kit: bool = (
- item_id == ArtShopStockType.ART_KIT_ITEM_ID
- and item != null
- and item.is_available()
- and _player.bag.owns_item(item_id)
- )
- var active_is_catching_net: bool = (
- item_id == FishingShopStockType.CRAB_NET_ID
- and item != null
- and item.is_available()
- and _player.bag.owns_item(item_id)
- )
- var active_is_shovel: bool = (
- item_id == FishingShopStockType.STANDARD_SHOVEL_ID
- and item != null
- and item.is_available()
- and _player.bag.owns_item(item_id)
- )
- var active_is_decor: bool = (
- DecorCatalogType.has_product(item_id)
- and item != null
- and item.is_available()
- and _player.bag.owns_item(item_id)
- )
- _player.set_active_fishing_rod(
- item as FishingRodDataType if active_is_rod else null,
- true,
- )
- _player.set_active_art_kit(
- item.icon if item != null else null,
- active_is_art_kit,
- )
- _player.set_active_catching_net(active_is_catching_net)
- _player.set_active_shovel(active_is_shovel)
- _player.set_active_decor_package(active_is_decor)
- _game_ui.set_surface_drawing_hotbar_selected(active_is_art_kit)
- _network_item_use.submit_local_equipped(item_id, active_is_rod or (
- item != null and _player.bag.owns_item(item_id)
- ))
+ _player.set_active_item_is_rod(active_is_rod)
_fishing_spot.refresh_active_item_status()
func _refresh_active_hotbar_item() -> void:
- var kind: int = (
- _player.hotbar.get_selected_assignment_kind()
- )
- var identity: StringName = StringName()
- if kind == PlayerHotbarType.AssignmentKind.ITEM:
- identity = _player.hotbar.get_selected_item_id()
- elif kind == PlayerHotbarType.AssignmentKind.FISH:
- identity = _player.hotbar.get_selected_fish_catch_id()
_on_active_hotbar_item_changed(
_player.hotbar.get_selected_slot(),
- kind,
- identity,
+ _player.hotbar.get_selected_item_id()
)
@@ -2738,19 +371,15 @@ func _on_quit_requested() -> void:
_quit_in_progress = true
_settings_manager.save_if_dirty()
if _gameplay_started:
- _save_manager.save_world_time_checkpoint()
- _network_session.disconnect_session("Application closing.")
+ _save_manager.save_if_dirty()
_title_music_requested = false
_replace_title_music_transition()
_finish_quit()
func _show_title_music(restart_from_beginning: bool = false) -> void:
- _new_game_music.stop()
_title_music_requested = true
_replace_title_music_transition()
- if _title_music.stream == null:
- return
if restart_from_beginning:
_title_music.stop()
_title_music.volume_db = title_music_volume_db
@@ -2758,19 +387,6 @@ func _show_title_music(restart_from_beginning: bool = false) -> void:
_title_music.play(0.0)
-func _start_new_game_music() -> void:
- if _new_game_music.stream == null:
- return
- _title_music_requested = false
- _replace_title_music_transition()
- _title_music.stop()
- _title_music.volume_db = title_music_volume_db
- _dusk_music.stop()
- _new_game_music.stop()
- _new_game_music.volume_db = new_game_music_volume_db
- _new_game_music.play(0.0)
-
-
func _fade_out_title_music(on_complete: Callable = Callable()) -> void:
_title_music_requested = false
var generation: int = _replace_title_music_transition()
@@ -2828,150 +444,26 @@ func _finish_quit() -> void:
func _exit_tree() -> void:
- if _dedicated_runtime and _network_session != null:
- _network_session.disconnect_session("Dedicated server stopping.")
if _title_music_tween != null:
_title_music_tween.kill()
_title_music_tween = null
if is_instance_valid(_title_music):
_title_music.stop()
_title_music.stream = null
- if is_instance_valid(_new_game_music):
- _new_game_music.stop()
- _new_game_music.stream = null
- if is_instance_valid(_dusk_music):
- _dusk_music.stop()
- _dusk_music.stream = null
func _on_shop_range_changed(in_range: bool) -> void:
- _game_ui.set_shop_npc_player_in_range(
- in_range
- or (
- _decor_shop_interaction != null
- and _decor_shop_interaction.is_local_player_in_range()
- )
- )
if not in_range:
_game_ui.get_fishing_shop().close_for_range_exit()
_game_ui.set_shop_prompt_visible(_can_show_shop_prompt())
-func _on_storage_range_changed(in_range: bool) -> void:
- if not in_range:
- _game_ui.get_player_storage().close_for_range_exit()
- var prompt := _get_home_prompt()
- _game_ui.set_storage_prompt_visible(
- not prompt.is_empty(),
- prompt.get("anchor", Vector3.ZERO),
- str(prompt.get("label", "enter RV")),
- )
- _game_ui.set_rv_storage_button_visible(
- _can_show_rv_storage_button()
- )
-
-
-func _on_decor_shop_range_changed(in_range: bool) -> void:
- _game_ui.set_shop_npc_player_in_range(
- in_range
- or (
- _shop_interaction != null
- and _shop_interaction.is_local_player_in_range()
- )
- )
- if not in_range:
- _game_ui.get_decor_shop().close_for_range_exit()
- _game_ui.set_shop_prompt_visible(
- _can_show_shop_prompt() or _can_show_decor_shop_prompt()
- )
-
-
-func _on_local_home_space_changed(
- space_id: StringName,
- _owner_fingerprint: String,
-) -> void:
- var inside_rv: bool = String(space_id).begins_with("rv:")
- _fishing_spot.set_fishing_environment_enabled(not inside_rv)
- _game_ui.set_rv_interior_camera_mode(
- inside_rv
- )
- _game_ui.set_rv_storage_button_visible(
- _can_show_rv_storage_button()
- )
-
-
-func _on_rv_storage_requested() -> void:
- if not _can_show_rv_storage_button():
- return
- _game_ui.get_player_storage().open_storage_from_home(true)
-
-
func _can_show_shop_prompt() -> bool:
return (
_gameplay_started
and _shop_interaction != null
and _shop_interaction.is_local_player_in_range()
and not _game_ui.get_fishing_shop().visible
- and not _game_ui.get_decor_shop().visible
and not _water_recovery.is_recovery_active()
and _fishing_spot.can_open_fishing_shop()
)
-
-
-func _can_show_decor_shop_prompt() -> bool:
- return (
- _gameplay_started
- and _decor_shop_interaction != null
- and _decor_shop_interaction.is_local_player_in_range()
- and not _game_ui.get_decor_shop().visible
- and not _game_ui.get_fishing_shop().visible
- and not _water_recovery.is_recovery_active()
- and _fishing_spot.can_open_fishing_shop()
- )
-
-
-func _can_show_storage_prompt() -> bool:
- return not _get_home_prompt().is_empty()
-
-
-func _can_show_rv_storage_button() -> bool:
- return (
- _gameplay_started
- and not _network_home.get_local_home_owner_fingerprint().is_empty()
- and _network_home.can_local_decorate()
- and not _water_recovery.is_recovery_active()
- and _fishing_spot.can_open_fishing_shop()
- )
-
-
-func _get_home_prompt() -> Dictionary:
- if (
- not _gameplay_started
- or _game_ui.get_player_storage().visible
- or _game_ui.get_fishing_shop().visible
- or _game_ui.get_decor_shop().visible
- or _water_recovery.is_recovery_active()
- or not _fishing_spot.can_open_fishing_shop()
- ):
- return {}
- var owner_fingerprint: String = (
- _network_home.get_local_home_owner_fingerprint()
- )
- if not owner_fingerprint.is_empty():
- if _home_world.is_avatar_near_interior_exit(
- _player, owner_fingerprint
- ):
- return {
- "label": "leave RV",
- "anchor": _home_world.get_interior_prompt_position(
- owner_fingerprint
- ),
- }
- return {}
- var nearby_owner: String = _home_world.find_nearby_exterior_owner(_player)
- if nearby_owner.is_empty():
- return {}
- return {
- "label": "enter RV",
- "anchor": _home_world.get_exterior_prompt_position(nearby_owner),
- }
diff --git a/main/main.tscn b/main/main.tscn
index 778a1d4..192178b 100644
--- a/main/main.tscn
+++ b/main/main.tscn
@@ -1,402 +1,64 @@
-[gd_scene format=3 uid="uid://w6lvqm3bfhei"]
+[gd_scene load_steps=17 format=3]
[ext_resource type="PackedScene" path="res://world/test_world.tscn" id="1_world"]
[ext_resource type="PackedScene" path="res://player/player.tscn" id="2_player"]
-[ext_resource type="Script" uid="uid://c7u6jpr3f33t3" path="res://main/main.gd" id="3_main"]
+[ext_resource type="Script" path="res://main/main.gd" id="3_main"]
[ext_resource type="PackedScene" path="res://fishing/fishing_spot.tscn" id="4_spot"]
[ext_resource type="PackedScene" path="res://ui/game_ui.tscn" id="5_ui"]
-[ext_resource type="Resource" path="res://fish/pools/fish_catalog.tres" id="6_pool"]
+[ext_resource type="Resource" path="res://fish/pools/test_water_pool.tres" id="6_pool"]
[ext_resource type="Resource" path="res://economy/buyers/pelicans.tres" id="7_pelicans"]
-[ext_resource type="Script" uid="uid://dvv0ublaoj4gr" path="res://world/water_recovery_controller.gd" id="8_recovery"]
-[ext_resource type="Script" uid="uid://dfr5m2x77d8ed" path="res://save/player_save_manager.gd" id="9_save"]
-[ext_resource type="Script" uid="uid://dwv3nvfke5xtu" path="res://settings/player_settings_manager.gd" id="10_settings"]
+[ext_resource type="Script" path="res://world/water_recovery_controller.gd" id="8_recovery"]
+[ext_resource type="Script" path="res://save/player_save_manager.gd" id="9_save"]
+[ext_resource type="Script" path="res://settings/player_settings_manager.gd" id="10_settings"]
[ext_resource type="Resource" path="res://items/catalog/item_catalog.tres" id="11_items"]
[ext_resource type="Resource" path="res://economy/buyers/main_fishing_shop.tres" id="12_main_shop"]
-[ext_resource type="Script" uid="uid://bdtb05vkiicd" path="res://ui/ui_pixelation_presenter.gd" id="14_pixelation"]
+[ext_resource type="AudioStream" path="res://audio/music/title/as_in_four_wolves.ogg" id="13_title_music"]
+[ext_resource type="Script" path="res://ui/ui_pixelation_presenter.gd" id="14_pixelation"]
[ext_resource type="PackedScene" path="res://ui/pixelation_reset_overlay.tscn" id="15_reset"]
[ext_resource type="PackedScene" path="res://main/world_pixelation_postprocess.tscn" id="16_world_pixelation"]
-[ext_resource type="Script" uid="uid://u62a32ktf0y5" path="res://network/network_session.gd" id="17_network_session"]
-[ext_resource type="Script" uid="uid://db8ua5psoql0w" path="res://network/network_profile_preferences.gd" id="18_network_profile"]
-[ext_resource type="Script" uid="uid://b7j0er0u1nwy7" path="res://network/saved_server_store.gd" id="19_saved_servers"]
-[ext_resource type="Script" uid="uid://8w70d0nrf4a7" path="res://network/player_spawn_service.gd" id="20_spawn_service"]
-[ext_resource type="Script" uid="uid://b4v56htdn1y2l" path="res://network/network_fishing_service.gd" id="21_network_fishing"]
-[ext_resource type="Script" uid="uid://sqqy3anotq6n" path="res://network/network_sale_service.gd" id="22_network_sale"]
-[ext_resource type="Script" uid="uid://4ci4dbhyfc11" path="res://network/network_shop_service.gd" id="23_network_shop"]
-[ext_resource type="Script" uid="uid://dx21uu253nmpw" path="res://network/network_item_use_service.gd" id="24_network_item"]
-[ext_resource type="Script" uid="uid://b10jayoog5dc2" path="res://network/network_chat_service.gd" id="25_network_chat"]
-[ext_resource type="Script" uid="uid://t235k67v0dno" path="res://network/network_mail_service.gd" id="26_network_mail"]
-[ext_resource type="Script" uid="uid://bheplri3k6ch7" path="res://progression/player_asset_reservation_service.gd" id="27_reservations"]
-[ext_resource type="Script" uid="uid://ci07gm3e7r8p6" path="res://progression/player_appearance_store.gd" id="28_appearance"]
-[ext_resource type="Script" uid="uid://dc80t6nmksvm2" path="res://network/network_profile_service.gd" id="29_profile_service"]
-[ext_resource type="Script" uid="uid://cqlgolkbx2riq" path="res://network/player_identity_store.gd" id="30_player_identity"]
-[ext_resource type="Script" uid="uid://dtyoh0v77x52f" path="res://network/host_identity_store.gd" id="31_host_identity"]
-[ext_resource type="Script" uid="uid://dossvvvh4yuid" path="res://network/known_player_store.gd" id="32_known_players"]
-[ext_resource type="Script" uid="uid://cp2kam2btjkb0" path="res://network/server_trust_store.gd" id="33_server_trust"]
-[ext_resource type="Script" uid="uid://c3fnebvjotixg" path="res://network/player_relationship_store.gd" id="34_relationships"]
-[ext_resource type="Script" uid="uid://dmv6dqua5yl5a" path="res://network/host_ban_store.gd" id="35_bans"]
-[ext_resource type="Script" uid="uid://bkidkp7ggtc3q" path="res://network/network_player_list_service.gd" id="36_player_list"]
-[ext_resource type="Script" uid="uid://dpiv6wttry0j4" path="res://network/player_data_root.gd" id="37_data_root"]
-[ext_resource type="Script" uid="uid://bu5q4a8jfvxkf" path="res://network/identity_backup_service.gd" id="38_identity_backup"]
-[ext_resource type="Script" uid="uid://djmxy6jibi5y3" path="res://ui/interface_font_controller.gd" id="39_fonts"]
-[ext_resource type="Texture2D" uid="uid://doju2lbj2ghwa" path="res://world/generation/chunks/assets/chunk_0000_grass_lite.png" id="40_title_grass"]
-[ext_resource type="Texture2D" uid="uid://f8a0cs0s7ler" path="res://art/patterns/pattern_tealdot.png" id="41_menu_pattern"]
-[ext_resource type="Shader" uid="uid://byw3iub66qvog" path="res://ui/player_menu_pattern.gdshader" id="42_menu_pattern"]
-[ext_resource type="Texture2D" uid="uid://b6xws1d2dnbnn" path="res://art/patterns/pattern_moneyfish.png" id="43_shop_pattern"]
-[ext_resource type="Script" uid="uid://m4n0bn38f34i" path="res://network/network_fish_showcase_service.gd" id="45_fish_showcase"]
-[ext_resource type="Script" uid="uid://bkiepgu8j5r6t" path="res://network/network_surface_drawing_service.gd" id="46_surface_drawing"]
-[ext_resource type="Script" uid="uid://5lceyulaglpf" path="res://world/world_time_service.gd" id="47_world_time"]
-[ext_resource type="Script" uid="uid://svirs8vbypad" path="res://world/world_time_visual_controller.gd" id="48_world_time_visuals"]
-[ext_resource type="Script" uid="uid://blig76aes25j" path="res://network/network_world_time_service.gd" id="49_network_world_time"]
-[ext_resource type="Script" uid="uid://v4c54isq6w74" path="res://world/world_weather_service.gd" id="50_world_weather"]
-[ext_resource type="Script" uid="uid://llj46fx0oco7" path="res://network/network_world_weather_service.gd" id="51_network_world_weather"]
-[ext_resource type="Script" uid="uid://daqtje84gqgu0" path="res://jobs/player_job_service.gd" id="52_player_jobs"]
-[ext_resource type="Script" uid="uid://drlyytr2o3uw8" path="res://network/network_job_service.gd" id="53_network_jobs"]
-[ext_resource type="Script" uid="uid://b0c3o2vy76fg3" path="res://world/shoreline_ambience.gd" id="54_shoreline_ambience"]
-[ext_resource type="Script" uid="uid://c80x8jkdnx0xy" path="res://settings/controller_mapping_manager.gd" id="56_controller_mapping"]
-[ext_resource type="Script" path="res://settings/keyboard_mouse_mapping_manager.gd" id="keyboard_mouse_mapping"]
-[ext_resource type="Script" uid="uid://d0jv8n2nfqia0" path="res://network/discovery_client.gd" id="57_discovery"]
-[ext_resource type="Resource" path="res://gathering/catalog/gatherable_catalog.tres" id="58_gatherable_catalog"]
-[ext_resource type="Script" path="res://network/network_world_spawn_service.gd" id="59_world_spawns"]
-[ext_resource type="Script" path="res://gathering/gathering_controller.gd" id="60_gathering_controller"]
-[ext_resource type="Script" path="res://homes/player_home_state.gd" id="61_home_state"]
-[ext_resource type="Script" path="res://homes/home_world_service.gd" id="62_home_world"]
-[ext_resource type="Script" path="res://network/network_home_service.gd" id="63_network_home"]
-[ext_resource type="Script" path="res://homes/home_decor_controller.gd" id="64_home_decor_controller"]
-[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_grass_native"]
-shader = ExtResource("42_menu_pattern")
-shader_parameter/pattern_texture = ExtResource("40_title_grass")
-shader_parameter/source_tile_size = Vector2(128, 128)
-shader_parameter/display_scale = 1.0
-shader_parameter/scroll_velocity_pixels = Vector2(-7.5, -7.5)
-
-[sub_resource type="ShaderMaterial" id="ShaderMaterial_player_menu_pattern"]
-shader = ExtResource("42_menu_pattern")
-shader_parameter/pattern_texture = ExtResource("41_menu_pattern")
-shader_parameter/source_tile_size = Vector2(128, 128)
-shader_parameter/display_scale = 0.85
-shader_parameter/scroll_velocity_pixels = Vector2(-7, -5)
-
-[sub_resource type="ShaderMaterial" id="ShaderMaterial_shop_pattern"]
-shader = ExtResource("42_menu_pattern")
-shader_parameter/pattern_texture = ExtResource("43_shop_pattern")
-shader_parameter/source_tile_size = Vector2(64, 64)
-shader_parameter/display_scale = 1.75
-shader_parameter/scroll_velocity_pixels = Vector2(-7, -5)
-
-[sub_resource type="ConcavePolygonShape3D" id="ConcavePolygonShape3D_hifdc"]
-data = PackedVector3Array(-13.598646, -21.527378, 3.4402227, -13.598646, -21.04931, 3.716229, -13.64679, -21.345833, 3.1714065, -13.64679, -21.345833, 3.1714065, -13.598646, -21.04931, 3.716229, -13.598646, -21.04931, 3.1642165, -13.598646, -21.601322, 3.716229, -13.598646, -21.04931, 3.716229, -13.598646, -21.527378, 3.4402227, -13.598646, -21.04931, 3.1642165, -13.598646, -21.04931, 3.716229, -13.598646, -20.773304, 3.238161, -13.604641, -21.54901, 3.9200878, -13.598646, -21.04931, 3.716229, -13.598646, -21.601322, 3.716229, -13.598646, -20.773304, 3.238161, -13.598646, -21.04931, 3.716229, -13.598646, -20.571243, 3.4402227, -13.598646, -21.325317, 4.194297, -13.598646, -21.04931, 3.716229, -13.604641, -21.54901, 3.9200878, -13.598646, -20.571243, 3.4402227, -13.598646, -21.04931, 3.716229, -13.598646, -20.4973, 3.716229, -13.602205, -21.078318, 4.268241, -13.598646, -21.04931, 3.716229, -13.598646, -21.325317, 4.194297, -13.598646, -20.4973, 3.716229, -13.598646, -21.04931, 3.716229, -13.598646, -20.571243, 3.992235, -13.598646, -20.773304, 4.194297, -13.598646, -21.04931, 3.716229, -13.602205, -21.078318, 4.268241, -13.598646, -20.571243, 3.992235, -13.598646, -21.04931, 3.716229, -13.598646, -20.773304, 4.194297, -19.042458, -21.325317, 3.2381601, -19.042458, -21.04931, 3.716228, -19.042458, -21.527378, 3.4402218, -19.042458, -21.527378, 3.4402218, -19.042458, -21.04931, 3.716228, -19.042458, -21.601322, 3.716228, -19.042458, -21.04931, 3.1642156, -19.042458, -21.04931, 3.716228, -19.042458, -21.325317, 3.2381601, -19.042458, -21.601322, 3.716228, -19.042458, -21.04931, 3.716228, -19.042458, -21.527378, 3.992234, -19.042458, -20.773304, 3.2381601, -19.042458, -21.04931, 3.716228, -19.042458, -21.04931, 3.1642156, -19.042458, -21.527378, 3.992234, -19.042458, -21.04931, 3.716228, -19.042458, -21.325317, 4.194296, -18.784327, -20.60149, 3.4511306, -19.042458, -21.04931, 3.716228, -19.042458, -20.773304, 3.2381601, -19.042458, -21.325317, 4.194296, -19.042458, -21.04931, 3.716228, -19.042458, -21.04931, 4.26824, -19.042458, -20.4973, 3.716228, -19.042458, -21.04931, 3.716228, -18.784327, -20.60149, 3.4511306, -19.042458, -21.04931, 4.26824, -19.042458, -21.04931, 3.716228, -19.042458, -20.773304, 4.194296, -19.042458, -20.571243, 3.992234, -19.042458, -21.04931, 3.716228, -19.042458, -20.4973, 3.716228, -19.042458, -20.773304, 4.194296, -19.042458, -21.04931, 3.716228, -19.042458, -20.571243, 3.992234, -14.534702, -21.32513, 3.238533, -17.467253, -21.04931, 3.1645257, -17.467253, -21.32513, 3.2384703, -17.467253, -21.32513, 3.2384703, -19.084044, -21.586075, 3.4063175, -17.467253, -21.52707, 3.440408, -17.467253, -21.32513, 3.2384703, -19.084044, -21.35922, 3.1794631, -19.084044, -21.586075, 3.4063175, -17.467253, -21.04931, 3.1645257, -19.084044, -21.35922, 3.1794631, -17.467253, -21.32513, 3.2384703, -17.467253, -21.32513, 3.2384703, -17.467253, -21.52707, 3.440408, -14.516907, -21.527006, 3.4404085, -17.467253, -21.32513, 3.2384703, -14.516907, -21.527006, 3.4404085, -14.534702, -21.32513, 3.238533, -17.467253, -21.04931, 3.1645257, -19.084044, -21.04931, 3.0964072, -19.084044, -21.35922, 3.1794631, -13.605204, -21.379738, 3.1126475, -14.534702, -21.32513, 3.238533, -14.516907, -21.527006, 3.4404085, -13.605204, -21.379738, 3.1126475, -14.516907, -21.527006, 3.4404085, -13.55706, -21.586075, 3.4063187, -13.55706, -21.04931, 3.0964081, -14.534702, -21.32513, 3.238533, -13.605204, -21.379738, 3.1126475, -13.55706, -21.04931, 3.0964081, -14.519905, -21.04931, 3.1645882, -14.534702, -21.32513, 3.238533, -14.534702, -21.32513, 3.238533, -14.519905, -21.04931, 3.1645882, -17.467253, -21.04931, 3.1645257, -13.55706, -20.7394, 3.179464, -14.519905, -21.04931, 3.1645882, -13.55706, -21.04931, 3.0964081, -17.467253, -20.77349, 3.2384703, -19.084044, -21.04931, 3.0964072, -17.467253, -21.04931, 3.1645257, -17.467253, -20.77349, 3.2384703, -17.467253, -21.04931, 3.1645257, -14.519905, -21.04931, 3.1645882, -17.467253, -20.77349, 3.2384703, -19.084044, -20.7394, 3.1794631, -19.084044, -21.04931, 3.0964072, -17.467253, -20.571552, 3.440408, -19.084044, -20.7394, 3.1794631, -17.467253, -20.77349, 3.2384703, -17.467253, -20.571552, 3.440408, -18.82591, -20.542793, 3.4172263, -19.084044, -20.7394, 3.1794631, -17.467253, -20.77349, 3.2384703, -14.519905, -21.04931, 3.1645882, -14.509039, -20.77349, 3.238533, -17.467253, -20.571552, 3.440408, -17.467253, -20.77349, 3.2384703, -14.509039, -20.77349, 3.238533, -13.55706, -20.7394, 3.179464, -14.509039, -20.77349, 3.238533, -14.519905, -21.04931, 3.1645882, -17.467253, -20.571552, 3.440408, -14.509039, -20.77349, 3.238533, -14.509039, -20.571615, 3.4404085, -13.55706, -20.512547, 3.4063187, -14.509039, -20.77349, 3.238533, -13.55706, -20.7394, 3.179464, -13.55706, -20.512547, 3.4063187, -14.509039, -20.571615, 3.4404085, -14.509039, -20.77349, 3.238533, -17.467253, -21.52707, 3.440408, -14.509039, -21.60095, 3.7162287, -14.516907, -21.527006, 3.4404085, -13.55706, -21.586075, 3.4063187, -14.516907, -21.527006, 3.4404085, -14.509039, -21.60095, 3.7162287, -13.55706, -21.586075, 3.4063187, -14.509039, -21.60095, 3.7162287, -13.55706, -21.669132, 3.716229, -13.55706, -21.669132, 3.716229, -14.509039, -21.60095, 3.7162287, -14.511475, -21.527006, 3.992049, -13.55706, -21.669132, 3.716229, -14.511475, -21.527006, 3.992049, -13.563242, -21.607708, 3.9539921, -17.467253, -21.601013, 3.7162282, -14.511475, -21.527006, 3.992049, -14.509039, -21.60095, 3.7162287, -17.467253, -21.52707, 3.440408, -17.467253, -21.601013, 3.7162282, -14.509039, -21.60095, 3.7162287, -17.467253, -21.601013, 3.7162282, -17.467253, -21.52707, 3.9920485, -14.511475, -21.527006, 3.992049, -17.467253, -21.601013, 3.7162282, -19.084044, -21.586075, 4.0261383, -17.467253, -21.52707, 3.9920485, -17.467253, -21.601013, 3.7162282, -19.084044, -21.669132, 3.716228, -19.084044, -21.586075, 4.0261383, -17.467253, -21.52707, 3.440408, -19.084044, -21.669132, 3.716228, -17.467253, -21.601013, 3.7162282, -17.467253, -21.52707, 3.440408, -19.084044, -21.586075, 3.4063175, -19.084044, -21.669132, 3.716228, -17.467253, -21.52707, 3.9920485, -14.509039, -21.32513, 4.1939244, -14.511475, -21.527006, 3.992049, -13.563242, -21.607708, 3.9539921, -14.511475, -21.527006, 3.992049, -14.509039, -21.32513, 4.1939244, -13.563242, -21.607708, 3.9539921, -14.509039, -21.32513, 4.1939244, -13.557062, -21.35922, 4.2529936, -13.557062, -21.35922, 4.2529936, -14.509039, -21.32513, 4.1939244, -14.510912, -21.04931, 4.267869, -13.557062, -21.35922, 4.2529936, -14.510912, -21.04931, 4.267869, -13.560621, -21.078318, 4.3360496, -17.467253, -21.52707, 3.9920485, -17.467253, -21.32513, 4.193986, -14.509039, -21.32513, 4.1939244, -17.467253, -21.32513, 4.193986, -14.510912, -21.04931, 4.267869, -14.509039, -21.32513, 4.1939244, -17.467253, -21.52707, 3.9920485, -19.084044, -21.35922, 4.2529926, -17.467253, -21.32513, 4.193986, -17.467253, -21.52707, 3.9920485, -19.084044, -21.586075, 4.0261383, -19.084044, -21.35922, 4.2529926, -17.467253, -21.32513, 4.193986, -19.084044, -21.35922, 4.2529926, -19.084044, -21.04931, 4.3360486, -17.467253, -21.32513, 4.193986, -19.084044, -21.04931, 4.3360486, -17.467253, -21.04931, 4.2679305, -17.467253, -21.32513, 4.193986, -17.467253, -21.04931, 4.2679305, -14.510912, -21.04931, 4.267869, -17.467253, -21.04931, 4.2679305, -19.084044, -21.04931, 4.3360486, -19.084044, -20.7394, 4.2529926, -13.560621, -21.078318, 4.3360496, -14.510912, -21.04931, 4.267869, -14.510912, -20.795122, 4.1217775, -17.467253, -21.04931, 4.2679305, -14.510912, -20.795122, 4.1217775, -14.510912, -21.04931, 4.267869, -13.560621, -21.078318, 4.3360496, -14.510912, -20.795122, 4.1217775, -13.557062, -20.7394, 4.2529936, -17.467253, -21.04931, 4.2679305, -19.084044, -20.7394, 4.2529926, -17.467253, -20.77349, 4.193986, -17.467253, -21.04931, 4.2679305, -17.467253, -20.77349, 4.193986, -14.510912, -20.795122, 4.1217775, -17.467253, -20.77349, 4.193986, -19.084044, -20.7394, 4.2529926, -19.084044, -20.512547, 4.0261383, -17.467253, -20.77349, 4.193986, -19.084044, -20.512547, 4.0261383, -17.467253, -20.571552, 3.9920485, -17.467253, -20.77349, 4.193986, -17.467253, -20.571552, 3.9920485, -14.509039, -20.571615, 3.992049, -17.467253, -20.77349, 4.193986, -14.509039, -20.571615, 3.992049, -14.510912, -20.795122, 4.1217775, -13.557062, -20.7394, 4.2529936, -14.510912, -20.795122, 4.1217775, -14.509039, -20.571615, 3.992049, -13.557062, -20.7394, 4.2529936, -14.509039, -20.571615, 3.992049, -13.55706, -20.512547, 4.0261393, -17.467253, -20.571552, 3.9920485, -14.509039, -20.497732, 3.7162287, -14.509039, -20.571615, 3.992049, -13.55706, -20.512547, 4.0261393, -14.509039, -20.571615, 3.992049, -14.509039, -20.497732, 3.7162287, -13.55706, -20.512547, 4.0261393, -14.509039, -20.497732, 3.7162287, -13.55706, -20.42949, 3.716229, -13.55706, -20.42949, 3.716229, -14.509039, -20.497732, 3.7162287, -14.509039, -20.571615, 3.4404085, -13.55706, -20.42949, 3.716229, -14.509039, -20.571615, 3.4404085, -13.55706, -20.512547, 3.4063187, -17.467253, -20.497608, 3.7162282, -14.509039, -20.571615, 3.4404085, -14.509039, -20.497732, 3.7162287, -17.467253, -20.571552, 3.9920485, -17.467253, -20.497608, 3.7162282, -14.509039, -20.497732, 3.7162287, -17.467253, -20.497608, 3.7162282, -17.467253, -20.571552, 3.440408, -14.509039, -20.571615, 3.4404085, -17.467253, -20.497608, 3.7162282, -18.82591, -20.542793, 3.4172263, -17.467253, -20.571552, 3.440408, -17.467253, -20.497608, 3.7162282, -19.084044, -20.42949, 3.716228, -18.82591, -20.542793, 3.4172263, -17.467253, -20.571552, 3.9920485, -19.084044, -20.42949, 3.716228, -17.467253, -20.497608, 3.7162282, -17.467253, -20.571552, 3.9920485, -19.084044, -20.512547, 4.0261383, -19.084044, -20.42949, 3.716228, -19.084044, -21.35922, 3.1794631, -19.084044, -21.586075, 3.4063175, -19.084044, -21.586075, 3.4063175, -19.084044, -21.586075, 3.4063175, -19.084044, -21.586075, 3.4063175, -19.084044, -21.669132, 3.716228, -19.084044, -21.35922, 3.1794631, -19.084044, -21.35922, 3.1794631, -19.084044, -21.586075, 3.4063175, -19.084044, -21.04931, 3.0964072, -19.084044, -21.35922, 3.1794631, -19.084044, -21.35922, 3.1794631, -19.084044, -21.04931, 3.0964072, -19.084044, -21.04931, 3.0964072, -19.084044, -21.35922, 3.1794631, -19.084044, -21.586075, 3.4063175, -19.084044, -21.669132, 3.716228, -19.084044, -21.669132, 3.716228, -19.084044, -21.669132, 3.716228, -19.084044, -21.669132, 3.716228, -19.084044, -21.586075, 4.0261383, -19.084044, -21.669132, 3.716228, -19.084044, -21.586075, 4.0261383, -19.084044, -21.586075, 4.0261383, -19.084044, -21.586075, 4.0261383, -19.084044, -21.586075, 4.0261383, -19.084044, -21.35922, 4.2529926, -19.084044, -21.586075, 4.0261383, -19.084044, -21.35922, 4.2529926, -19.084044, -21.35922, 4.2529926, -19.084044, -21.35922, 4.2529926, -19.084044, -21.35922, 4.2529926, -19.084044, -21.04931, 4.3360486, -19.084044, -21.35922, 4.2529926, -19.084044, -21.04931, 4.3360486, -19.084044, -21.04931, 4.3360486, -19.084044, -21.04931, 4.3360486, -19.084044, -21.04931, 4.3360486, -19.084044, -20.7394, 4.2529926, -19.084044, -21.04931, 4.3360486, -19.084044, -20.7394, 4.2529926, -19.084044, -20.7394, 4.2529926, -19.084044, -20.7394, 4.2529926, -19.084044, -20.7394, 4.2529926, -19.084044, -20.512547, 4.0261383, -19.084044, -20.7394, 4.2529926, -19.084044, -20.512547, 4.0261383, -19.084044, -20.512547, 4.0261383, -19.084044, -20.512547, 4.0261383, -19.084044, -20.512547, 4.0261383, -19.084044, -20.42949, 3.716228, -19.084044, -20.512547, 4.0261383, -19.084044, -20.42949, 3.716228, -19.084044, -20.42949, 3.716228, -19.084044, -20.42949, 3.716228, -19.084044, -20.42949, 3.716228, -18.82591, -20.542793, 3.4172263, -19.084044, -20.42949, 3.716228, -18.82591, -20.542793, 3.4172263, -18.82591, -20.542793, 3.4172263, -18.82591, -20.542793, 3.4172263, -19.084044, -20.7394, 3.1794631, -19.084044, -20.7394, 3.1794631, -18.82591, -20.542793, 3.4172263, -18.82591, -20.542793, 3.4172263, -19.084044, -20.7394, 3.1794631, -19.084044, -20.7394, 3.1794631, -19.084044, -21.04931, 3.0964072, -19.084044, -21.04931, 3.0964072, -19.084044, -20.7394, 3.1794631, -19.084044, -20.7394, 3.1794631, -19.084044, -21.04931, 3.0964072, -13.55706, -21.586075, 3.4063187, -13.605204, -21.345833, 3.1714065, -13.605204, -21.379738, 3.1126475, -13.605204, -21.379738, 3.1126475, -13.605204, -21.345833, 3.1714065, -13.55706, -21.04931, 3.1642165, -13.55706, -21.586075, 3.4063187, -13.55706, -21.527378, 3.4402227, -13.605204, -21.345833, 3.1714065, -13.605204, -21.379738, 3.1126475, -13.55706, -21.04931, 3.1642165, -13.55706, -21.04931, 3.0964081, -13.55706, -21.669132, 3.716229, -13.55706, -21.527378, 3.4402227, -13.55706, -21.586075, 3.4063187, -13.55706, -21.04931, 3.0964081, -13.55706, -21.04931, 3.1642165, -13.55706, -20.773304, 3.238161, -13.55706, -21.669132, 3.716229, -13.55706, -21.601322, 3.716229, -13.55706, -21.527378, 3.4402227, -13.55706, -21.04931, 3.0964081, -13.55706, -20.773304, 3.238161, -13.55706, -20.7394, 3.179464, -13.563242, -21.607708, 3.9539921, -13.55706, -21.601322, 3.716229, -13.55706, -21.669132, 3.716229, -13.55706, -20.7394, 3.179464, -13.55706, -20.773304, 3.238161, -13.55706, -20.571243, 3.4402227, -13.563242, -21.607708, 3.9539921, -13.563242, -21.54901, 3.9200878, -13.55706, -21.601322, 3.716229, -13.55706, -20.7394, 3.179464, -13.55706, -20.571243, 3.4402227, -13.55706, -20.512547, 3.4063187, -13.557062, -21.35922, 4.2529936, -13.563242, -21.54901, 3.9200878, -13.563242, -21.607708, 3.9539921, -13.55706, -20.512547, 3.4063187, -13.55706, -20.571243, 3.4402227, -13.55706, -20.4973, 3.716229, -13.557062, -21.35922, 4.2529936, -13.55706, -21.325317, 4.194297, -13.563242, -21.54901, 3.9200878, -13.55706, -20.512547, 3.4063187, -13.55706, -20.4973, 3.716229, -13.55706, -20.42949, 3.716229, -13.560621, -21.078318, 4.3360496, -13.55706, -21.325317, 4.194297, -13.557062, -21.35922, 4.2529936, -13.55706, -20.42949, 3.716229, -13.55706, -20.4973, 3.716229, -13.55706, -20.571243, 3.9922352, -13.560621, -21.078318, 4.3360496, -13.560621, -21.078318, 4.268241, -13.55706, -21.325317, 4.194297, -13.55706, -20.42949, 3.716229, -13.55706, -20.571243, 3.9922352, -13.55706, -20.512547, 4.0261393, -13.557062, -20.7394, 4.2529936, -13.560621, -21.078318, 4.268241, -13.560621, -21.078318, 4.3360496, -13.55706, -20.512547, 4.0261393, -13.55706, -20.571243, 3.9922352, -13.55706, -20.773304, 4.194297, -13.557062, -20.7394, 4.2529936, -13.55706, -20.773304, 4.194297, -13.560621, -21.078318, 4.268241, -13.55706, -20.512547, 4.0261393, -13.55706, -20.773304, 4.194297, -13.557062, -20.7394, 4.2529936, -19.084044, -21.35922, 3.1794631, -19.084044, -21.527378, 3.4402218, -19.084044, -21.586075, 3.4063175, -19.084044, -21.586075, 3.4063175, -19.084044, -21.527378, 3.4402218, -19.084044, -21.601322, 3.716228, -19.084044, -21.35922, 3.1794631, -19.084044, -21.325317, 3.2381601, -19.084044, -21.527378, 3.4402218, -19.084044, -21.586075, 3.4063175, -19.084044, -21.601322, 3.716228, -19.084044, -21.669132, 3.716228, -19.084044, -21.04931, 3.0964072, -19.084044, -21.325317, 3.2381601, -19.084044, -21.35922, 3.1794631, -19.084044, -21.669132, 3.716228, -19.084044, -21.601322, 3.716228, -19.084044, -21.527378, 3.992234, -19.084044, -21.04931, 3.0964072, -19.084044, -21.04931, 3.1642156, -19.084044, -21.325317, 3.2381601, -19.084044, -21.669132, 3.716228, -19.084044, -21.527378, 3.992234, -19.084044, -21.586075, 4.0261383, -19.084044, -20.7394, 3.1794631, -19.084044, -21.04931, 3.1642156, -19.084044, -21.04931, 3.0964072, -19.084044, -21.586075, 4.0261383, -19.084044, -21.527378, 3.992234, -19.084044, -21.325317, 4.194296, -19.084044, -20.7394, 3.1794631, -19.084044, -20.773304, 3.2381601, -19.084044, -21.04931, 3.1642156, -19.084044, -21.586075, 4.0261383, -19.084044, -21.325317, 4.194296, -19.084044, -21.35922, 4.2529926, -18.82591, -20.542793, 3.4172263, -19.084044, -20.773304, 3.2381601, -19.084044, -20.7394, 3.1794631, -19.084044, -21.35922, 4.2529926, -19.084044, -21.325317, 4.194296, -19.084044, -21.04931, 4.26824, -18.82591, -20.542793, 3.4172263, -18.82591, -20.60149, 3.4511306, -19.084044, -20.773304, 3.2381601, -19.084044, -21.35922, 4.2529926, -19.084044, -21.04931, 4.26824, -19.084044, -21.04931, 4.3360486, -19.084044, -20.42949, 3.716228, -18.82591, -20.60149, 3.4511306, -18.82591, -20.542793, 3.4172263, -19.084044, -21.04931, 4.3360486, -19.084044, -21.04931, 4.26824, -19.084044, -20.773304, 4.194296, -19.084044, -20.42949, 3.716228, -19.084044, -20.4973, 3.716228, -18.82591, -20.60149, 3.4511306, -19.084044, -21.04931, 4.3360486, -19.084044, -20.773304, 4.194296, -19.084044, -20.7394, 4.2529926, -19.084044, -20.512547, 4.0261383, -19.084044, -20.4973, 3.716228, -19.084044, -20.42949, 3.716228, -19.084044, -20.7394, 4.2529926, -19.084044, -20.773304, 4.194296, -19.084044, -20.571243, 3.992234, -19.084044, -20.512547, 4.0261383, -19.084044, -20.571243, 3.992234, -19.084044, -20.4973, 3.716228, -19.084044, -20.7394, 4.2529926, -19.084044, -20.571243, 3.992234, -19.084044, -20.512547, 4.0261383, -13.55706, -21.527378, 3.4402227, -13.64679, -21.345833, 3.1714065, -13.605204, -21.345833, 3.1714065, -13.55706, -21.527378, 3.4402227, -13.598646, -21.527378, 3.4402227, -13.64679, -21.345833, 3.1714065, -13.55706, -21.601322, 3.716229, -13.598646, -21.527378, 3.4402227, -13.55706, -21.527378, 3.4402227, -13.55706, -21.601322, 3.716229, -13.598646, -21.601322, 3.716229, -13.598646, -21.527378, 3.4402227, -13.563242, -21.54901, 3.9200878, -13.598646, -21.601322, 3.716229, -13.55706, -21.601322, 3.716229, -13.563242, -21.54901, 3.9200878, -13.604641, -21.54901, 3.9200878, -13.598646, -21.601322, 3.716229, -13.55706, -21.325317, 4.194297, -13.604641, -21.54901, 3.9200878, -13.563242, -21.54901, 3.9200878, -13.55706, -21.325317, 4.194297, -13.598646, -21.325317, 4.194297, -13.604641, -21.54901, 3.9200878, -13.560621, -21.078318, 4.268241, -13.598646, -21.325317, 4.194297, -13.55706, -21.325317, 4.194297, -13.560621, -21.078318, 4.268241, -13.602205, -21.078318, 4.268241, -13.598646, -21.325317, 4.194297, -13.55706, -20.773304, 4.194297, -13.602205, -21.078318, 4.268241, -13.560621, -21.078318, 4.268241, -13.55706, -20.773304, 4.194297, -13.598646, -20.773304, 4.194297, -13.602205, -21.078318, 4.268241, -13.55706, -20.571243, 3.9922352, -13.598646, -20.773304, 4.194297, -13.55706, -20.773304, 4.194297, -13.55706, -20.571243, 3.9922352, -13.598646, -20.571243, 3.992235, -13.598646, -20.773304, 4.194297, -13.55706, -20.4973, 3.716229, -13.598646, -20.571243, 3.992235, -13.55706, -20.571243, 3.9922352, -13.55706, -20.4973, 3.716229, -13.598646, -20.4973, 3.716229, -13.598646, -20.571243, 3.992235, -13.55706, -20.571243, 3.4402227, -13.598646, -20.4973, 3.716229, -13.55706, -20.4973, 3.716229, -13.55706, -20.571243, 3.4402227, -13.598646, -20.571243, 3.4402227, -13.598646, -20.4973, 3.716229, -13.55706, -20.773304, 3.238161, -13.598646, -20.571243, 3.4402227, -13.55706, -20.571243, 3.4402227, -13.55706, -20.773304, 3.238161, -13.598646, -20.773304, 3.238161, -13.598646, -20.571243, 3.4402227, -13.55706, -21.04931, 3.1642165, -13.598646, -20.773304, 3.238161, -13.55706, -20.773304, 3.238161, -13.55706, -21.04931, 3.1642165, -13.598646, -21.04931, 3.1642165, -13.598646, -20.773304, 3.238161, -13.605204, -21.345833, 3.1714065, -13.598646, -21.04931, 3.1642165, -13.55706, -21.04931, 3.1642165, -13.605204, -21.345833, 3.1714065, -13.64679, -21.345833, 3.1714065, -13.598646, -21.04931, 3.1642165, -19.084044, -21.325317, 3.2381601, -19.042458, -21.527378, 3.4402218, -19.084044, -21.527378, 3.4402218, -19.084044, -21.325317, 3.2381601, -19.042458, -21.325317, 3.2381601, -19.042458, -21.527378, 3.4402218, -19.084044, -21.527378, 3.4402218, -19.042458, -21.601322, 3.716228, -19.084044, -21.601322, 3.716228, -19.084044, -21.527378, 3.4402218, -19.042458, -21.527378, 3.4402218, -19.042458, -21.601322, 3.716228, -19.084044, -21.601322, 3.716228, -19.042458, -21.527378, 3.992234, -19.084044, -21.527378, 3.992234, -19.084044, -21.601322, 3.716228, -19.042458, -21.601322, 3.716228, -19.042458, -21.527378, 3.992234, -19.084044, -21.527378, 3.992234, -19.042458, -21.325317, 4.194296, -19.084044, -21.325317, 4.194296, -19.084044, -21.527378, 3.992234, -19.042458, -21.527378, 3.992234, -19.042458, -21.325317, 4.194296, -19.084044, -21.325317, 4.194296, -19.042458, -21.04931, 4.26824, -19.084044, -21.04931, 4.26824, -19.084044, -21.325317, 4.194296, -19.042458, -21.325317, 4.194296, -19.042458, -21.04931, 4.26824, -19.084044, -21.04931, 4.26824, -19.042458, -21.04931, 4.26824, -19.042458, -20.773304, 4.194296, -19.084044, -21.04931, 4.26824, -19.042458, -20.773304, 4.194296, -19.084044, -20.773304, 4.194296, -19.084044, -20.773304, 4.194296, -19.042458, -20.571243, 3.992234, -19.084044, -20.571243, 3.992234, -19.084044, -20.773304, 4.194296, -19.042458, -20.773304, 4.194296, -19.042458, -20.571243, 3.992234, -19.084044, -20.571243, 3.992234, -19.042458, -20.4973, 3.716228, -19.084044, -20.4973, 3.716228, -19.084044, -20.571243, 3.992234, -19.042458, -20.571243, 3.992234, -19.042458, -20.4973, 3.716228, -19.084044, -20.4973, 3.716228, -18.784327, -20.60149, 3.4511306, -18.82591, -20.60149, 3.4511306, -19.084044, -20.4973, 3.716228, -19.042458, -20.4973, 3.716228, -18.784327, -20.60149, 3.4511306, -18.82591, -20.60149, 3.4511306, -19.042458, -20.773304, 3.2381601, -19.084044, -20.773304, 3.2381601, -18.82591, -20.60149, 3.4511306, -18.784327, -20.60149, 3.4511306, -19.042458, -20.773304, 3.2381601, -19.084044, -20.773304, 3.2381601, -19.042458, -21.04931, 3.1642156, -19.084044, -21.04931, 3.1642156, -19.084044, -20.773304, 3.2381601, -19.042458, -20.773304, 3.2381601, -19.042458, -21.04931, 3.1642156, -19.084044, -21.04931, 3.1642156, -19.042458, -21.04931, 3.1642156, -19.042458, -21.325317, 3.2381601, -19.084044, -21.04931, 3.1642156, -19.042458, -21.325317, 3.2381601, -19.084044, -21.325317, 3.2381601, -0.49230337, 3.2260702, -2.5408025, -0.2174604, 3.7041378, -2.5661154, -0.75557137, 3.4076157, -2.4682097, -0.75557137, 3.4076157, -2.4682097, -0.2174604, 3.7041378, -2.5661154, -0.7671461, 3.7041378, -2.5154896, -0.2174604, 3.1521254, -2.5661154, -0.2174604, 3.7041378, -2.5661154, -0.49230337, 3.2260702, -2.5408025, -0.7671461, 3.7041378, -2.5154896, -0.2174604, 3.7041378, -2.5661154, -0.6935134, 3.980144, -2.5222712, -0.01391077, 3.2044384, -2.5788422, -0.2174604, 3.7041378, -2.5661154, -0.2174604, 3.1521254, -2.5661154, -0.6935134, 3.980144, -2.5222712, -0.2174604, 3.7041378, -2.5661154, -0.49230337, 4.1822057, -2.5408025, 0.2585926, 3.4281316, -2.6099591, -0.2174604, 3.7041378, -2.5661154, -0.01391077, 3.2044384, -2.5788422, -0.49230337, 4.1822057, -2.5408025, -0.2174604, 3.7041378, -2.5661154, -0.2174604, 4.2561502, -2.5661154, 0.33255196, 3.6751301, -2.6131968, -0.2174604, 3.7041378, -2.5661154, 0.2585926, 3.4281316, -2.6099591, -0.2174604, 4.2561502, -2.5661154, -0.2174604, 3.7041378, -2.5661154, 0.057382584, 4.1822057, -2.5914283, 0.2585926, 3.980144, -2.6099591, -0.2174604, 3.7041378, -2.5661154, 0.33255196, 3.6751301, -2.6131968, 0.057382584, 4.1822057, -2.5914283, -0.2174604, 3.7041378, -2.5661154, 0.2585926, 3.980144, -2.6099591, -0.19425559, 3.4281316, 2.8985977, 0.2817974, 3.7041378, 2.8547535, 0.0069544315, 3.2260702, 2.8800664, 0.0069544315, 3.2260702, 2.8800664, 0.2817974, 3.7041378, 2.8547535, 0.2817974, 3.1521254, 2.8547535, -0.26788855, 3.7041378, 2.9053793, 0.2817974, 3.7041378, 2.8547535, -0.19425559, 3.4281316, 2.8985977, 0.2817974, 3.1521254, 2.8547535, 0.2817974, 3.7041378, 2.8547535, 0.55664027, 3.2260702, 2.8294406, -0.19425559, 3.980144, 2.8985977, 0.2817974, 3.7041378, 2.8547535, -0.26788855, 3.7041378, 2.9053793, 0.55664027, 3.2260702, 2.8294406, 0.2817974, 3.7041378, 2.8547535, 0.75785017, 3.4281316, 2.8109093, -0.0058562756, 4.1519585, 2.6220222, 0.2817974, 3.7041378, 2.8547535, -0.19425559, 3.980144, 2.8985977, 0.75785017, 3.4281316, 2.8109093, 0.2817974, 3.7041378, 2.8547535, 0.8314831, 3.7041378, 2.8041277, 0.2817974, 4.2561502, 2.8547535, 0.2817974, 3.7041378, 2.8547535, -0.0058562756, 4.1519585, 2.6220222, 0.8314831, 3.7041378, 2.8041277, 0.2817974, 3.7041378, 2.8547535, 0.75785017, 3.980144, 2.8109093, 0.55664027, 4.1822057, 2.8294406, 0.2817974, 3.7041378, 2.8547535, 0.2817974, 4.2561502, 2.8547535, 0.75785017, 3.980144, 2.8109093, 0.2817974, 3.7041378, 2.8547535, 0.55664027, 4.1822057, 2.8294406, -0.6072962, 3.4283175, -1.5901942, -0.41204357, 3.7041378, 1.3367851, -0.33841062, 3.4283175, 1.3300035, -0.33841062, 3.4283175, 1.3300035, -0.022993088, 3.1673732, 2.9245868, -0.1373241, 3.2263799, 1.3114836, -0.33841062, 3.4283175, 1.3300035, -0.24889135, 3.3942275, 2.9453917, -0.022993088, 3.1673732, 2.9245868, -0.41204357, 3.7041378, 1.3367851, -0.24889135, 3.3942275, 2.9453917, -0.33841062, 3.4283175, 1.3300035, -0.33841062, 3.4283175, 1.3300035, -0.1373241, 3.2263799, 1.3114836, -0.40790343, 3.2264419, -1.6264291, -0.33841062, 3.4283175, 1.3300035, -0.40790343, 3.2264419, -1.6264291, -0.6072962, 3.4283175, -1.5901942, -0.41204357, 3.7041378, 1.3367851, -0.33159733, 3.7041378, 2.9530087, -0.24889135, 3.3942275, 2.9453917, -0.81789684, 3.3737113, -2.5042315, -0.6072962, 3.4283175, -1.5901942, -0.40790343, 3.2264419, -1.6264291, -0.81789684, 3.3737113, -2.5042315, -0.40790343, 3.2264419, -1.6264291, -0.5298786, 3.1673732, -2.5791035, -0.83848286, 3.7041378, -2.5506816, -0.6072962, 3.4283175, -1.5901942, -0.81789684, 3.3737113, -2.5042315, -0.83848286, 3.7041378, -2.5506816, -0.6822865, 3.7041378, -1.5981488, -0.6072962, 3.4283175, -1.5901942, -0.6072962, 3.4283175, -1.5901942, -0.6822865, 3.7041378, -1.5981488, -0.41204357, 3.7041378, 1.3367851, -0.7557769, 4.014048, -2.5582986, -0.6822865, 3.7041378, -1.5981488, -0.83848286, 3.7041378, -2.5506816, -0.33841062, 3.979958, 1.3300035, -0.33159733, 3.7041378, 2.9530087, -0.41204357, 3.7041378, 1.3367851, -0.33841062, 3.979958, 1.3300035, -0.41204357, 3.7041378, 1.3367851, -0.6822865, 3.7041378, -1.5981488, -0.33841062, 3.979958, 1.3300035, -0.24889135, 4.014048, 2.9453917, -0.33159733, 3.7041378, 2.9530087, -0.1373241, 4.1818957, 1.3114836, -0.24889135, 4.014048, 2.9453917, -0.33841062, 3.979958, 1.3300035, -0.1373241, 4.1818957, 1.3114836, -0.035803795, 4.210655, 2.666542, -0.24889135, 4.014048, 2.9453917, -0.33841062, 3.979958, 1.3300035, -0.6822865, 3.7041378, -1.5981488, -0.6096499, 3.979958, -1.6157494, -0.1373241, 4.1818957, 1.3114836, -0.33841062, 3.979958, 1.3300035, -0.6096499, 3.979958, -1.6157494, -0.7557769, 4.014048, -2.5582986, -0.6096499, 3.979958, -1.6157494, -0.6822865, 3.7041378, -1.5981488, -0.1373241, 4.1818957, 1.3114836, -0.6096499, 3.979958, -1.6157494, -0.40862513, 4.1818337, -1.6342635, -0.5298786, 4.2409024, -2.5791035, -0.6096499, 3.979958, -1.6157494, -0.7557769, 4.014048, -2.5582986, -0.5298786, 4.2409024, -2.5791035, -0.40862513, 4.1818337, -1.6342635, -0.6096499, 3.979958, -1.6157494, -0.1373241, 3.2263799, 1.3114836, -0.13396716, 3.1524973, -1.6595592, -0.40790343, 3.2264419, -1.6264291, -0.5298786, 3.1673732, -2.5791035, -0.40790343, 3.2264419, -1.6264291, -0.13396716, 3.1524973, -1.6595592, -0.5298786, 3.1673732, -2.5791035, -0.13396716, 3.1524973, -1.6595592, -0.22127414, 3.0843172, -2.6075258, -0.22127414, 3.0843172, -2.6075258, -0.13396716, 3.1524973, -1.6595592, 0.14091396, 3.2264419, -1.68243, -0.22127414, 3.0843172, -2.6075258, 0.14091396, 3.2264419, -1.68243, 0.016053915, 3.1457415, -2.6231756, 0.13733363, 3.1524353, 1.2861879, 0.14091396, 3.2264419, -1.68243, -0.13396716, 3.1524973, -1.6595592, -0.1373241, 3.2263799, 1.3114836, 0.13733363, 3.1524353, 1.2861879, -0.13396716, 3.1524973, -1.6595592, 0.13733363, 3.1524353, 1.2861879, 0.4119916, 3.2263799, 1.2608922, 0.14091396, 3.2264419, -1.68243, 0.13733363, 3.1524353, 1.2861879, 0.5942155, 3.1673732, 2.867742, 0.4119916, 3.2263799, 1.2608922, 0.13733363, 3.1524353, 1.2861879, 0.28561115, 3.0843172, 2.8961644, 0.5942155, 3.1673732, 2.867742, -0.1373241, 3.2263799, 1.3114836, 0.28561115, 3.0843172, 2.8961644, 0.13733363, 3.1524353, 1.2861879, -0.1373241, 3.2263799, 1.3114836, -0.022993088, 3.1673732, 2.9245868, 0.28561115, 3.0843172, 2.8961644, 0.4119916, 3.2263799, 1.2608922, 0.34171534, 3.4283175, -1.7033694, 0.14091396, 3.2264419, -1.68243, 0.016053915, 3.1457415, -2.6231756, 0.14091396, 3.2264419, -1.68243, 0.34171534, 3.4283175, -1.7033694, 0.016053915, 3.1457415, -2.6231756, 0.34171534, 3.4283175, -1.7033694, 0.31322837, 3.3942275, -2.656753, 0.31322837, 3.3942275, -2.656753, 0.34171534, 3.4283175, -1.7033694, 0.4155202, 3.7041378, -1.7082856, 0.31322837, 3.3942275, -2.656753, 0.4155202, 3.7041378, -1.7082856, 0.39626074, 3.6751301, -2.6608262, 0.4119916, 3.2263799, 1.2608922, 0.613078, 3.4283175, 1.2423723, 0.34171534, 3.4283175, -1.7033694, 0.613078, 3.4283175, 1.2423723, 0.4155202, 3.7041378, -1.7082856, 0.34171534, 3.4283175, -1.7033694, 0.4119916, 3.2263799, 1.2608922, 0.8201138, 3.3942275, 2.8469372, 0.613078, 3.4283175, 1.2423723, 0.4119916, 3.2263799, 1.2608922, 0.5942155, 3.1673732, 2.867742, 0.8201138, 3.3942275, 2.8469372, 0.613078, 3.4283175, 1.2423723, 0.8201138, 3.3942275, 2.8469372, 0.90281975, 3.7041378, 2.8393202, 0.613078, 3.4283175, 1.2423723, 0.90281975, 3.7041378, 2.8393202, 0.68671095, 3.7041378, 1.2355907, 0.613078, 3.4283175, 1.2423723, 0.68671095, 3.7041378, 1.2355907, 0.4155202, 3.7041378, -1.7082856, 0.68671095, 3.7041378, 1.2355907, 0.90281975, 3.7041378, 2.8393202, 0.8201138, 4.014048, 2.8469372, 0.39626074, 3.6751301, -2.6608262, 0.4155202, 3.7041378, -1.7082856, 0.2700441, 3.9583263, -1.6948872, 0.68671095, 3.7041378, 1.2355907, 0.2700441, 3.9583263, -1.6948872, 0.4155202, 3.7041378, -1.7082856, 0.39626074, 3.6751301, -2.6608262, 0.2700441, 3.9583263, -1.6948872, 0.31322837, 4.014048, -2.656753, 0.68671095, 3.7041378, 1.2355907, 0.8201138, 4.014048, 2.8469372, 0.613078, 3.979958, 1.2423723, 0.68671095, 3.7041378, 1.2355907, 0.613078, 3.979958, 1.2423723, 0.2700441, 3.9583263, -1.6948872, 0.613078, 3.979958, 1.2423723, 0.8201138, 4.014048, 2.8469372, 0.5942155, 4.2409024, 2.867742, 0.613078, 3.979958, 1.2423723, 0.5942155, 4.2409024, 2.867742, 0.4119916, 4.1818957, 1.2608922, 0.613078, 3.979958, 1.2423723, 0.4119916, 4.1818957, 1.2608922, 0.14069057, 4.1818337, -1.684855, 0.613078, 3.979958, 1.2423723, 0.14069057, 4.1818337, -1.684855, 0.2700441, 3.9583263, -1.6948872, 0.31322837, 4.014048, -2.656753, 0.2700441, 3.9583263, -1.6948872, 0.14069057, 4.1818337, -1.684855, 0.31322837, 4.014048, -2.656753, 0.14069057, 4.1818337, -1.684855, 0.0873301, 4.2409024, -2.6359482, 0.4119916, 4.1818957, 1.2608922, -0.13396716, 4.2557163, -1.6595592, 0.14069057, 4.1818337, -1.684855, 0.0873301, 4.2409024, -2.6359482, 0.14069057, 4.1818337, -1.684855, -0.13396716, 4.2557163, -1.6595592, 0.0873301, 4.2409024, -2.6359482, -0.13396716, 4.2557163, -1.6595592, -0.22127414, 4.3239584, -2.6075258, -0.22127414, 4.3239584, -2.6075258, -0.13396716, 4.2557163, -1.6595592, -0.40862513, 4.1818337, -1.6342635, -0.22127414, 4.3239584, -2.6075258, -0.40862513, 4.1818337, -1.6342635, -0.5298786, 4.2409024, -2.5791035, 0.13733363, 4.2558403, 1.2861879, -0.40862513, 4.1818337, -1.6342635, -0.13396716, 4.2557163, -1.6595592, 0.4119916, 4.1818957, 1.2608922, 0.13733363, 4.2558403, 1.2861879, -0.13396716, 4.2557163, -1.6595592, 0.13733363, 4.2558403, 1.2861879, -0.1373241, 4.1818957, 1.3114836, -0.40862513, 4.1818337, -1.6342635, 0.13733363, 4.2558403, 1.2861879, -0.035803795, 4.210655, 2.666542, -0.1373241, 4.1818957, 1.3114836, 0.13733363, 4.2558403, 1.2861879, 0.28561115, 4.3239584, 2.8961644, -0.035803795, 4.210655, 2.666542, 0.4119916, 4.1818957, 1.2608922, 0.28561115, 4.3239584, 2.8961644, 0.13733363, 4.2558403, 1.2861879, 0.4119916, 4.1818957, 1.2608922, 0.5942155, 4.2409024, 2.867742, 0.28561115, 4.3239584, 2.8961644, -0.24889135, 3.3942275, 2.9453917, -0.022993088, 3.1673732, 2.9245868, -0.022993088, 3.1673732, 2.9245868, -0.022993088, 3.1673732, 2.9245868, -0.022993088, 3.1673732, 2.9245868, 0.28561115, 3.0843172, 2.8961644, -0.24889135, 3.3942275, 2.9453917, -0.24889135, 3.3942275, 2.9453917, -0.022993088, 3.1673732, 2.9245868, -0.33159733, 3.7041378, 2.9530087, -0.24889135, 3.3942275, 2.9453917, -0.24889135, 3.3942275, 2.9453917, -0.33159733, 3.7041378, 2.9530087, -0.33159733, 3.7041378, 2.9530087, -0.24889135, 3.3942275, 2.9453917, -0.022993088, 3.1673732, 2.9245868, 0.28561115, 3.0843172, 2.8961644, 0.28561115, 3.0843172, 2.8961644, 0.28561115, 3.0843172, 2.8961644, 0.28561115, 3.0843172, 2.8961644, 0.5942155, 3.1673732, 2.867742, 0.28561115, 3.0843172, 2.8961644, 0.5942155, 3.1673732, 2.867742, 0.5942155, 3.1673732, 2.867742, 0.5942155, 3.1673732, 2.867742, 0.5942155, 3.1673732, 2.867742, 0.8201138, 3.3942275, 2.8469372, 0.5942155, 3.1673732, 2.867742, 0.8201138, 3.3942275, 2.8469372, 0.8201138, 3.3942275, 2.8469372, 0.8201138, 3.3942275, 2.8469372, 0.8201138, 3.3942275, 2.8469372, 0.90281975, 3.7041378, 2.8393202, 0.8201138, 3.3942275, 2.8469372, 0.90281975, 3.7041378, 2.8393202, 0.90281975, 3.7041378, 2.8393202, 0.90281975, 3.7041378, 2.8393202, 0.90281975, 3.7041378, 2.8393202, 0.8201138, 4.014048, 2.8469372, 0.90281975, 3.7041378, 2.8393202, 0.8201138, 4.014048, 2.8469372, 0.8201138, 4.014048, 2.8469372, 0.8201138, 4.014048, 2.8469372, 0.8201138, 4.014048, 2.8469372, 0.5942155, 4.2409024, 2.867742, 0.8201138, 4.014048, 2.8469372, 0.5942155, 4.2409024, 2.867742, 0.5942155, 4.2409024, 2.867742, 0.5942155, 4.2409024, 2.867742, 0.5942155, 4.2409024, 2.867742, 0.28561115, 4.3239584, 2.8961644, 0.5942155, 4.2409024, 2.867742, 0.28561115, 4.3239584, 2.8961644, 0.28561115, 4.3239584, 2.8961644, 0.28561115, 4.3239584, 2.8961644, 0.28561115, 4.3239584, 2.8961644, -0.035803795, 4.210655, 2.666542, 0.28561115, 4.3239584, 2.8961644, -0.035803795, 4.210655, 2.666542, -0.035803795, 4.210655, 2.666542, -0.035803795, 4.210655, 2.666542, -0.24889135, 4.014048, 2.9453917, -0.24889135, 4.014048, 2.9453917, -0.035803795, 4.210655, 2.666542, -0.035803795, 4.210655, 2.666542, -0.24889135, 4.014048, 2.9453917, -0.24889135, 4.014048, 2.9453917, -0.33159733, 3.7041378, 2.9530087, -0.33159733, 3.7041378, 2.9530087, -0.24889135, 4.014048, 2.9453917, -0.24889135, 4.014048, 2.9453917, -0.33159733, 3.7041378, 2.9530087, -0.5298786, 3.1673732, -2.5791035, -0.75938535, 3.4076157, -2.5096202, -0.81789684, 3.3737113, -2.5042315, -0.81789684, 3.3737113, -2.5042315, -0.75938535, 3.4076157, -2.5096202, -0.7709601, 3.7041378, -2.5569, -0.5298786, 3.1673732, -2.5791035, -0.49611712, 3.2260702, -2.582213, -0.75938535, 3.4076157, -2.5096202, -0.81789684, 3.3737113, -2.5042315, -0.7709601, 3.7041378, -2.5569, -0.83848286, 3.7041378, -2.5506816, -0.22127414, 3.0843172, -2.6075258, -0.49611712, 3.2260702, -2.582213, -0.5298786, 3.1673732, -2.5791035, -0.83848286, 3.7041378, -2.5506816, -0.7709601, 3.7041378, -2.5569, -0.69732714, 3.980144, -2.5636816, -0.22127414, 3.0843172, -2.6075258, -0.22127414, 3.1521254, -2.6075258, -0.49611712, 3.2260702, -2.582213, -0.83848286, 3.7041378, -2.5506816, -0.69732714, 3.980144, -2.5636816, -0.7557769, 4.014048, -2.5582986, 0.016053915, 3.1457415, -2.6231756, -0.22127414, 3.1521254, -2.6075258, -0.22127414, 3.0843172, -2.6075258, -0.7557769, 4.014048, -2.5582986, -0.69732714, 3.980144, -2.5636816, -0.49611712, 4.1822057, -2.582213, 0.016053915, 3.1457415, -2.6231756, -0.017707348, 3.2044384, -2.6200662, -0.22127414, 3.1521254, -2.6075258, -0.7557769, 4.014048, -2.5582986, -0.49611712, 4.1822057, -2.582213, -0.5298786, 4.2409024, -2.5791035, 0.31322837, 3.3942275, -2.656753, -0.017707348, 3.2044384, -2.6200662, 0.016053915, 3.1457415, -2.6231756, -0.5298786, 4.2409024, -2.5791035, -0.49611712, 4.1822057, -2.582213, -0.22127414, 4.2561502, -2.6075258, 0.31322837, 3.3942275, -2.656753, 0.25477862, 3.4281316, -2.65137, -0.017707348, 3.2044384, -2.6200662, -0.5298786, 4.2409024, -2.5791035, -0.22127414, 4.2561502, -2.6075258, -0.22127414, 4.3239584, -2.6075258, 0.39626074, 3.6751301, -2.6608262, 0.25477862, 3.4281316, -2.65137, 0.31322837, 3.3942275, -2.656753, -0.22127414, 4.3239584, -2.6075258, -0.22127414, 4.2561502, -2.6075258, 0.05356884, 4.1822057, -2.6328387, 0.39626074, 3.6751301, -2.6608262, 0.32873797, 3.6751301, -2.6546073, 0.25477862, 3.4281316, -2.65137, -0.22127414, 4.3239584, -2.6075258, 0.05356884, 4.1822057, -2.6328387, 0.0873301, 4.2409024, -2.6359482, 0.31322837, 4.014048, -2.656753, 0.32873797, 3.6751301, -2.6546073, 0.39626074, 3.6751301, -2.6608262, 0.0873301, 4.2409024, -2.6359482, 0.05356884, 4.1822057, -2.6328387, 0.25477862, 3.980144, -2.65137, 0.31322837, 4.014048, -2.656753, 0.25477862, 3.980144, -2.65137, 0.32873797, 3.6751301, -2.6546073, 0.0873301, 4.2409024, -2.6359482, 0.25477862, 3.980144, -2.65137, 0.31322837, 4.014048, -2.656753, -0.24889135, 3.3942275, 2.9453917, 0.010768175, 3.2260702, 2.9214773, -0.022993088, 3.1673732, 2.9245868, -0.022993088, 3.1673732, 2.9245868, 0.010768175, 3.2260702, 2.9214773, 0.28561115, 3.1521254, 2.8961644, -0.24889135, 3.3942275, 2.9453917, -0.19044161, 3.4281316, 2.9400086, 0.010768175, 3.2260702, 2.9214773, -0.022993088, 3.1673732, 2.9245868, 0.28561115, 3.1521254, 2.8961644, 0.28561115, 3.0843172, 2.8961644, -0.33159733, 3.7041378, 2.9530087, -0.19044161, 3.4281316, 2.9400086, -0.24889135, 3.3942275, 2.9453917, 0.28561115, 3.0843172, 2.8961644, 0.28561115, 3.1521254, 2.8961644, 0.56045413, 3.2260702, 2.8708515, -0.33159733, 3.7041378, 2.9530087, -0.2640748, 3.7041378, 2.9467902, -0.19044161, 3.4281316, 2.9400086, 0.28561115, 3.0843172, 2.8961644, 0.56045413, 3.2260702, 2.8708515, 0.5942155, 3.1673732, 2.867742, -0.24889135, 4.014048, 2.9453917, -0.2640748, 3.7041378, 2.9467902, -0.33159733, 3.7041378, 2.9530087, 0.5942155, 3.1673732, 2.867742, 0.56045413, 3.2260702, 2.8708515, 0.76166415, 3.4281316, 2.8523202, -0.24889135, 4.014048, 2.9453917, -0.19044161, 3.980144, 2.9400086, -0.2640748, 3.7041378, 2.9467902, 0.5942155, 3.1673732, 2.867742, 0.76166415, 3.4281316, 2.8523202, 0.8201138, 3.3942275, 2.8469372, -0.035803795, 4.210655, 2.666542, -0.19044161, 3.980144, 2.9400086, -0.24889135, 4.014048, 2.9453917, 0.8201138, 3.3942275, 2.8469372, 0.76166415, 3.4281316, 2.8523202, 0.8352971, 3.7041378, 2.8455386, -0.035803795, 4.210655, 2.666542, -0.002042532, 4.1519585, 2.6634326, -0.19044161, 3.980144, 2.9400086, 0.8201138, 3.3942275, 2.8469372, 0.8352971, 3.7041378, 2.8455386, 0.90281975, 3.7041378, 2.8393202, 0.28561115, 4.3239584, 2.8961644, -0.002042532, 4.1519585, 2.6634326, -0.035803795, 4.210655, 2.666542, 0.90281975, 3.7041378, 2.8393202, 0.8352971, 3.7041378, 2.8455386, 0.76166415, 3.980144, 2.8523202, 0.28561115, 4.3239584, 2.8961644, 0.28561115, 4.2561502, 2.8961644, -0.002042532, 4.1519585, 2.6634326, 0.90281975, 3.7041378, 2.8393202, 0.76166415, 3.980144, 2.8523202, 0.8201138, 4.014048, 2.8469372, 0.5942155, 4.2409024, 2.867742, 0.28561115, 4.2561502, 2.8961644, 0.28561115, 4.3239584, 2.8961644, 0.8201138, 4.014048, 2.8469372, 0.76166415, 3.980144, 2.8523202, 0.56045413, 4.1822057, 2.8708515, 0.5942155, 4.2409024, 2.867742, 0.56045413, 4.1822057, 2.8708515, 0.28561115, 4.2561502, 2.8961644, 0.8201138, 4.014048, 2.8469372, 0.56045413, 4.1822057, 2.8708515, 0.5942155, 4.2409024, 2.867742, -0.49611712, 3.2260702, -2.582213, -0.75557137, 3.4076157, -2.4682097, -0.75938535, 3.4076157, -2.5096202, -0.49611712, 3.2260702, -2.582213, -0.49230337, 3.2260702, -2.5408025, -0.75557137, 3.4076157, -2.4682097, -0.22127414, 3.1521254, -2.6075258, -0.49230337, 3.2260702, -2.5408025, -0.49611712, 3.2260702, -2.582213, -0.22127414, 3.1521254, -2.6075258, -0.2174604, 3.1521254, -2.5661154, -0.49230337, 3.2260702, -2.5408025, -0.017707348, 3.2044384, -2.6200662, -0.2174604, 3.1521254, -2.5661154, -0.22127414, 3.1521254, -2.6075258, -0.017707348, 3.2044384, -2.6200662, -0.01391077, 3.2044384, -2.5788422, -0.2174604, 3.1521254, -2.5661154, 0.25477862, 3.4281316, -2.65137, -0.01391077, 3.2044384, -2.5788422, -0.017707348, 3.2044384, -2.6200662, 0.25477862, 3.4281316, -2.65137, 0.2585926, 3.4281316, -2.6099591, -0.01391077, 3.2044384, -2.5788422, 0.32873797, 3.6751301, -2.6546073, 0.2585926, 3.4281316, -2.6099591, 0.25477862, 3.4281316, -2.65137, 0.32873797, 3.6751301, -2.6546073, 0.33255196, 3.6751301, -2.6131968, 0.2585926, 3.4281316, -2.6099591, 0.25477862, 3.980144, -2.65137, 0.33255196, 3.6751301, -2.6131968, 0.32873797, 3.6751301, -2.6546073, 0.25477862, 3.980144, -2.65137, 0.2585926, 3.980144, -2.6099591, 0.33255196, 3.6751301, -2.6131968, 0.05356884, 4.1822057, -2.6328387, 0.2585926, 3.980144, -2.6099591, 0.25477862, 3.980144, -2.65137, 0.05356884, 4.1822057, -2.6328387, 0.057382584, 4.1822057, -2.5914283, 0.2585926, 3.980144, -2.6099591, -0.22127414, 4.2561502, -2.6075258, 0.057382584, 4.1822057, -2.5914283, 0.05356884, 4.1822057, -2.6328387, -0.22127414, 4.2561502, -2.6075258, -0.2174604, 4.2561502, -2.5661154, 0.057382584, 4.1822057, -2.5914283, -0.49611712, 4.1822057, -2.582213, -0.2174604, 4.2561502, -2.5661154, -0.22127414, 4.2561502, -2.6075258, -0.49611712, 4.1822057, -2.582213, -0.49230337, 4.1822057, -2.5408025, -0.2174604, 4.2561502, -2.5661154, -0.69732714, 3.980144, -2.5636816, -0.49230337, 4.1822057, -2.5408025, -0.49611712, 4.1822057, -2.582213, -0.69732714, 3.980144, -2.5636816, -0.6935134, 3.980144, -2.5222712, -0.49230337, 4.1822057, -2.5408025, -0.7709601, 3.7041378, -2.5569, -0.6935134, 3.980144, -2.5222712, -0.69732714, 3.980144, -2.5636816, -0.7709601, 3.7041378, -2.5569, -0.7671461, 3.7041378, -2.5154896, -0.6935134, 3.980144, -2.5222712, -0.75938535, 3.4076157, -2.5096202, -0.7671461, 3.7041378, -2.5154896, -0.7709601, 3.7041378, -2.5569, -0.75938535, 3.4076157, -2.5096202, -0.75557137, 3.4076157, -2.4682097, -0.7671461, 3.7041378, -2.5154896, -0.19044161, 3.4281316, 2.9400086, 0.0069544315, 3.2260702, 2.8800664, 0.010768175, 3.2260702, 2.9214773, -0.19044161, 3.4281316, 2.9400086, -0.19425559, 3.4281316, 2.8985977, 0.0069544315, 3.2260702, 2.8800664, 0.010768175, 3.2260702, 2.9214773, 0.2817974, 3.1521254, 2.8547535, 0.28561115, 3.1521254, 2.8961644, 0.010768175, 3.2260702, 2.9214773, 0.0069544315, 3.2260702, 2.8800664, 0.2817974, 3.1521254, 2.8547535, 0.28561115, 3.1521254, 2.8961644, 0.55664027, 3.2260702, 2.8294406, 0.56045413, 3.2260702, 2.8708515, 0.28561115, 3.1521254, 2.8961644, 0.2817974, 3.1521254, 2.8547535, 0.55664027, 3.2260702, 2.8294406, 0.56045413, 3.2260702, 2.8708515, 0.75785017, 3.4281316, 2.8109093, 0.76166415, 3.4281316, 2.8523202, 0.56045413, 3.2260702, 2.8708515, 0.55664027, 3.2260702, 2.8294406, 0.75785017, 3.4281316, 2.8109093, 0.76166415, 3.4281316, 2.8523202, 0.8314831, 3.7041378, 2.8041277, 0.8352971, 3.7041378, 2.8455386, 0.76166415, 3.4281316, 2.8523202, 0.75785017, 3.4281316, 2.8109093, 0.8314831, 3.7041378, 2.8041277, 0.8352971, 3.7041378, 2.8455386, 0.8314831, 3.7041378, 2.8041277, 0.75785017, 3.980144, 2.8109093, 0.8352971, 3.7041378, 2.8455386, 0.75785017, 3.980144, 2.8109093, 0.76166415, 3.980144, 2.8523202, 0.76166415, 3.980144, 2.8523202, 0.55664027, 4.1822057, 2.8294406, 0.56045413, 4.1822057, 2.8708515, 0.76166415, 3.980144, 2.8523202, 0.75785017, 3.980144, 2.8109093, 0.55664027, 4.1822057, 2.8294406, 0.56045413, 4.1822057, 2.8708515, 0.2817974, 4.2561502, 2.8547535, 0.28561115, 4.2561502, 2.8961644, 0.56045413, 4.1822057, 2.8708515, 0.55664027, 4.1822057, 2.8294406, 0.2817974, 4.2561502, 2.8547535, 0.28561115, 4.2561502, 2.8961644, -0.0058562756, 4.1519585, 2.6220222, -0.002042532, 4.1519585, 2.6634326, 0.28561115, 4.2561502, 2.8961644, 0.2817974, 4.2561502, 2.8547535, -0.0058562756, 4.1519585, 2.6220222, -0.002042532, 4.1519585, 2.6634326, -0.19425559, 3.980144, 2.8985977, -0.19044161, 3.980144, 2.9400086, -0.002042532, 4.1519585, 2.6634326, -0.0058562756, 4.1519585, 2.6220222, -0.19425559, 3.980144, 2.8985977, -0.19044161, 3.980144, 2.9400086, -0.26788855, 3.7041378, 2.9053793, -0.2640748, 3.7041378, 2.9467902, -0.19044161, 3.980144, 2.9400086, -0.19425559, 3.980144, 2.8985977, -0.26788855, 3.7041378, 2.9053793, -0.2640748, 3.7041378, 2.9467902, -0.26788855, 3.7041378, 2.9053793, -0.19425559, 3.4281316, 2.8985977, -0.2640748, 3.7041378, 2.9467902, -0.19425559, 3.4281316, 2.8985977, -0.19044161, 3.4281316, 2.9400086, -5.555945, -21.30843, 0.44781768, -3.54707, -21.129436, 0.3570392, -5.555945, -21.129436, 0.44781768, -5.555945, -21.30843, 0.44781768, -3.54707, -21.30843, 0.3570392, -3.54707, -21.129436, 0.3570392, -7.629112, -21.129436, 0.3570392, -5.555945, -21.30843, 0.44781768, -5.555945, -21.129436, 0.44781768, -7.629112, -21.129436, 0.3570392, -7.629112, -21.30843, 0.3570392, -5.555945, -21.30843, 0.44781768, -5.555945, -21.30843, 0.9271282, -3.54707, -21.30843, 0.3570392, -5.555945, -21.30843, 0.44781768, -5.555945, -21.30843, 0.9271282, -3.54707, -21.30843, 0.7632997, -3.54707, -21.30843, 0.3570392, -5.555945, -21.30843, 0.44781768, -7.629112, -21.30843, 0.91196275, -5.555945, -21.30843, 0.9271282, -5.555945, -21.30843, 0.44781768, -7.629112, -21.30843, 0.62980187, -7.629112, -21.30843, 0.91196275, -7.629112, -21.30843, 0.62980187, -5.555945, -21.30843, 0.44781768, -7.629112, -21.30843, 0.3570392, -3.54707, -21.30843, 0.7632997, -3.54707, -21.129436, 0.3570392, -3.54707, -21.30843, 0.3570392, -3.54707, -21.30843, 0.7632997, -3.54707, -21.129435, 0.7632997, -3.54707, -21.129436, 0.3570392, -7.629112, -21.129436, 0.5971217, -7.5507226, -21.183048, 0.6447537, -7.629112, -21.30843, 0.62980187, -5.555945, -21.129435, 0.80730057, -5.555945, -21.174292, 0.9271282, -6.0741305, -21.129435, 0.92328346, -5.555945, -21.129435, 0.80730057, -5.0537796, -21.129435, 0.8861177, -5.555945, -21.174292, 0.9271282, -5.555945, -21.129435, 0.80730057, -3.54707, -21.129435, 0.7632997, -5.0537796, -21.129435, 0.8861177, -5.555945, -21.129435, 0.80730057, -3.54707, -21.129436, 0.3570392, -3.54707, -21.129435, 0.7632997, -3.54707, -21.129436, 0.3570392, -5.555945, -21.129435, 0.80730057, -5.555945, -21.129436, 0.44781768, -6.0741305, -21.129435, 0.92328346, -5.555945, -21.129436, 0.44781768, -5.555945, -21.129435, 0.80730057, -5.555945, -21.129436, 0.44781768, -7.629112, -21.129436, 0.5971217, -7.629112, -21.129436, 0.3570392, -5.555945, -21.129436, 0.44781768, -7.2726197, -21.129435, 0.6462488, -7.629112, -21.129436, 0.5971217, -6.0741305, -21.129435, 0.92328346, -7.2726197, -21.129435, 0.6462488, -5.555945, -21.129436, 0.44781768, -7.5507226, -21.183048, 0.6447537, -7.629112, -21.129436, 0.5971217, -7.2726197, -21.129435, 0.6462488, -7.629112, -21.129435, 0.7325418, -7.5507226, -21.183048, 0.6447537, -7.2726197, -21.129435, 0.6462488, -7.2726197, -21.129435, 0.6462488, -6.0741305, -21.129435, 0.92328346, -7.629112, -21.129435, 0.7325418, -7.629112, -21.129435, 0.7325418, -6.0741305, -21.129435, 0.92328346, -7.629112, -21.129435, 0.91196275, -7.629112, -21.30843, 0.3570392, -7.629112, -21.129436, 0.5971217, -7.629112, -21.30843, 0.62980187, -7.629112, -21.30843, 0.3570392, -7.629112, -21.129436, 0.3570392, -7.629112, -21.129436, 0.5971217, -7.629112, -21.129435, 0.7325418, -7.629112, -21.30843, 0.91196275, -7.629112, -21.30843, 0.62980187, -7.629112, -21.129435, 0.7325418, -7.629112, -21.129435, 0.91196275, -7.629112, -21.30843, 0.91196275, -7.5507226, -21.183048, 0.6447537, -7.629112, -21.129435, 0.7325418, -7.629112, -21.30843, 0.62980187, -7.629112, -21.129435, 0.91196275, -5.555945, -21.30843, 0.9271282, -7.629112, -21.30843, 0.91196275, -5.555945, -21.30843, 0.9271282, -7.629112, -21.129435, 0.91196275, -6.0741305, -21.129435, 0.92328346, -5.555945, -21.30843, 0.9271282, -6.0741305, -21.129435, 0.92328346, -5.555945, -21.174292, 0.9271282, -5.555945, -21.174292, 0.9271282, -3.54707, -21.30843, 0.7632997, -5.555945, -21.30843, 0.9271282, -5.555945, -21.174292, 0.9271282, -3.54707, -21.129435, 0.7632997, -3.54707, -21.30843, 0.7632997, -3.54707, -21.129435, 0.7632997, -5.555945, -21.174292, 0.9271282, -5.0537796, -21.129435, 0.8861177, -3.988361, -21.129435, 2.1034038, -3.54707, -21.30843, 2.1130157, -3.988361, -21.30843, 2.1034038, -3.988361, -21.129435, 2.1034038, -3.54707, -21.129435, 2.1130157, -3.54707, -21.30843, 2.1130157, -3.988361, -21.30843, 2.1034038, -6.7189775, -21.129435, 2.110666, -3.988361, -21.129435, 2.1034038, -3.988361, -21.30843, 2.1034038, -7.629112, -21.17429, 2.1130157, -6.7189775, -21.129435, 2.110666, -7.629112, -21.17429, 2.1130157, -3.988361, -21.30843, 2.1034038, -7.629112, -21.30843, 2.1130157, -3.54707, -21.129435, 1.6317828, -3.988361, -21.129435, 2.1034038, -3.988361, -21.129435, 1.6206758, -6.7189775, -21.129435, 2.110666, -3.988361, -21.129435, 1.6206758, -3.988361, -21.129435, 2.1034038, -6.7189775, -21.129435, 2.110666, -7.629112, -21.129435, 1.7067552, -3.988361, -21.129435, 1.6206758, -7.629112, -21.129435, 1.7067552, -6.7189775, -21.129435, 2.110666, -7.629112, -21.129435, 2.0113437, -7.629112, -21.129435, 2.0113437, -6.7189775, -21.129435, 2.110666, -7.629112, -21.17429, 2.1130157, -3.988361, -21.129435, 2.1034038, -3.54707, -21.129435, 1.6317828, -3.78224, -21.129435, 1.8853219, -3.78224, -21.129435, 1.8853219, -3.54707, -21.129435, 1.6317828, -3.54707, -21.129435, 1.8257284, -3.54707, -21.129435, 1.8257284, -3.5872264, -21.197786, 1.8791275, -3.78224, -21.129435, 1.8853219, -3.5872264, -21.197786, 1.8791275, -3.54707, -21.129435, 1.959867, -3.78224, -21.129435, 1.8853219, -3.988361, -21.129435, 2.1034038, -3.78224, -21.129435, 1.8853219, -3.54707, -21.129435, 1.959867, -3.988361, -21.129435, 2.1034038, -3.54707, -21.129435, 1.959867, -3.54707, -21.129435, 2.1130157, -3.988361, -21.30843, 1.6206758, -3.54707, -21.129435, 1.6317828, -3.988361, -21.129435, 1.6206758, -3.988361, -21.30843, 1.6206758, -3.54707, -21.30843, 1.6317829, -3.54707, -21.129435, 1.6317828, -7.629112, -21.129435, 1.7067552, -3.988361, -21.30843, 1.6206758, -3.988361, -21.129435, 1.6206758, -7.629112, -21.129435, 1.7067552, -7.629112, -21.30843, 1.7067553, -3.988361, -21.30843, 1.6206758, -3.54707, -21.30843, 2.1130157, -3.988361, -21.30843, 1.6206758, -3.988361, -21.30843, 2.1034038, -3.988361, -21.30843, 1.6206758, -3.54707, -21.30843, 2.1130157, -3.54707, -21.30843, 1.8528552, -3.988361, -21.30843, 1.6206758, -3.54707, -21.30843, 1.8528552, -3.54707, -21.30843, 1.6317829, -7.629112, -21.30843, 1.7067553, -3.988361, -21.30843, 2.1034038, -3.988361, -21.30843, 1.6206758, -7.629112, -21.30843, 1.7067553, -7.629112, -21.30843, 2.1130157, -3.988361, -21.30843, 2.1034038, -3.54707, -21.129435, 1.959867, -3.5872264, -21.197786, 1.8791275, -3.54707, -21.30843, 1.8528552, -3.5872264, -21.197786, 1.8791275, -3.54707, -21.129435, 1.8257284, -3.54707, -21.30843, 1.8528552, -3.54707, -21.30843, 2.1130157, -3.54707, -21.129435, 1.959867, -3.54707, -21.30843, 1.8528552, -3.54707, -21.30843, 2.1130157, -3.54707, -21.129435, 2.1130157, -3.54707, -21.129435, 1.959867, -3.54707, -21.129435, 1.8257284, -3.54707, -21.30843, 1.6317829, -3.54707, -21.30843, 1.8528552, -3.54707, -21.129435, 1.8257284, -3.54707, -21.129435, 1.6317828, -3.54707, -21.30843, 1.6317829, -7.629112, -21.129435, 1.7067552, -7.629112, -21.30843, 2.1130157, -7.629112, -21.30843, 1.7067553, -7.629112, -21.30843, 2.1130157, -7.629112, -21.129435, 1.7067552, -7.629112, -21.129435, 2.0113437, -7.629112, -21.30843, 2.1130157, -7.629112, -21.129435, 2.0113437, -7.629112, -21.17429, 2.1130157, -3.9170194, -21.320818, -0.44629717, -3.8958735, -21.335342, -0.95978284, -3.8958735, -21.34047, -0.43775296, -3.9170194, -21.320818, -0.44629717, -3.9170194, -21.31612, -0.95081186, -3.8958735, -21.335342, -0.95978284, -3.9170194, -21.31612, -0.95081186, -3.8958735, -21.210388, -0.979434, -3.8958735, -21.335342, -0.95978284, -3.9170194, -21.31612, -0.95081186, -3.9170194, -21.215515, -0.9587152, -3.8958735, -21.210388, -0.979434, -3.8958735, -21.34047, -0.43775296, -3.9170194, -21.35478, -0.9687538, -3.9170194, -21.36012, -0.4289956, -3.8958735, -21.34047, -0.43775296, -3.8958735, -21.335342, -0.95978284, -3.9170194, -21.35478, -0.9687538, -3.8958735, -21.335342, -0.95978284, -3.9170194, -21.205263, -1.0001528, -3.9170194, -21.35478, -0.9687538, -3.8958735, -21.335342, -0.95978284, -3.8958735, -21.210388, -0.979434, -3.9170194, -21.205263, -1.0001528, -3.9170194, -21.36012, -0.4289956, -3.9385924, -21.335342, -0.95978284, -3.9385924, -21.34047, -0.43775296, -3.9170194, -21.36012, -0.4289956, -3.9170194, -21.35478, -0.9687538, -3.9385924, -21.335342, -0.95978284, -3.9170194, -21.35478, -0.9687538, -3.9385924, -21.210388, -0.979434, -3.9385924, -21.335342, -0.95978284, -3.9170194, -21.35478, -0.9687538, -3.9170194, -21.205263, -1.0001528, -3.9385924, -21.210388, -0.979434, -3.9385924, -21.34047, -0.43775296, -3.9170194, -21.31612, -0.95081186, -3.9170194, -21.320818, -0.44629717, -3.9385924, -21.34047, -0.43775296, -3.9385924, -21.335342, -0.95978284, -3.9170194, -21.31612, -0.95081186, -3.9385924, -21.335342, -0.95978284, -3.9170194, -21.215515, -0.9587152, -3.9170194, -21.31612, -0.95081186, -3.9385924, -21.335342, -0.95978284, -3.9385924, -21.210388, -0.979434, -3.9170194, -21.215515, -0.9587152, -3.9170194, -21.215515, -0.9587152, -3.8958735, -21.105513, -0.94718075, -3.8958735, -21.210388, -0.979434, -3.9170194, -21.215515, -0.9587152, -3.9170194, -21.126446, -0.9426954, -3.8958735, -21.105513, -0.94718075, -3.9170194, -21.126446, -0.9426954, -3.8958735, -21.10359, -0.44181156, -3.8958735, -21.105513, -0.94718075, -3.9170194, -21.126446, -0.9426954, -3.9170194, -21.12431, -0.44693804, -3.8958735, -21.10359, -0.44181156, -3.9170194, -21.12431, -0.44693804, -3.8958735, -21.219788, -0.39460683, -3.8958735, -21.10359, -0.44181156, -3.9170194, -21.12431, -0.44693804, -3.9170194, -21.22406, -0.4155395, -3.8958735, -21.219788, -0.39460683, -3.8958735, -21.210388, -0.979434, -3.9170194, -21.084795, -0.95188, -3.9170194, -21.205263, -1.0001528, -3.8958735, -21.210388, -0.979434, -3.8958735, -21.105513, -0.94718075, -3.9170194, -21.084795, -0.95188, -3.8958735, -21.105513, -0.94718075, -3.9170194, -21.082659, -0.43668532, -3.9170194, -21.084795, -0.95188, -3.8958735, -21.105513, -0.94718075, -3.8958735, -21.10359, -0.44181156, -3.9170194, -21.082659, -0.43668532, -3.8958735, -21.10359, -0.44181156, -3.9170194, -21.215515, -0.3736744, -3.9170194, -21.082659, -0.43668532, -3.8958735, -21.10359, -0.44181156, -3.8958735, -21.219788, -0.39460683, -3.9170194, -21.215515, -0.3736744, -3.9170194, -21.205263, -1.0001528, -3.9385924, -21.105513, -0.94718075, -3.9385924, -21.210388, -0.979434, -3.9170194, -21.205263, -1.0001528, -3.9170194, -21.084795, -0.95188, -3.9385924, -21.105513, -0.94718075, -3.9385924, -21.210388, -0.979434, -3.9170194, -21.126446, -0.9426954, -3.9170194, -21.215515, -0.9587152, -3.9385924, -21.210388, -0.979434, -3.9385924, -21.105513, -0.94718075, -3.9170194, -21.126446, -0.9426954, -3.9385924, -21.105513, -0.94718075, -3.9170194, -21.12431, -0.44693804, -3.9170194, -21.126446, -0.9426954, -3.9385924, -21.105513, -0.94718075, -3.9385924, -21.10359, -0.44181156, -3.9170194, -21.12431, -0.44693804, -3.9385924, -21.10359, -0.44181156, -3.9170194, -21.22406, -0.4155395, -3.9170194, -21.12431, -0.44693804, -3.9385924, -21.10359, -0.44181156, -3.9385924, -21.219788, -0.39460683, -3.9170194, -21.22406, -0.4155395, -3.9170194, -21.084795, -0.95188, -3.9385924, -21.10359, -0.44181156, -3.9385924, -21.105513, -0.94718075, -3.9170194, -21.084795, -0.95188, -3.9170194, -21.082659, -0.43668532, -3.9385924, -21.10359, -0.44181156, -3.9170194, -21.082659, -0.43668532, -3.9385924, -21.219788, -0.39460683, -3.9385924, -21.10359, -0.44181156, -3.9170194, -21.082659, -0.43668532, -3.9170194, -21.215515, -0.3736744, -3.9385924, -21.219788, -0.39460683, -3.9170194, -21.22406, -0.4155395, -3.8958735, -21.34047, -0.43775296, -3.8958735, -21.219788, -0.39460683, -3.9170194, -21.22406, -0.4155395, -3.9170194, -21.320818, -0.44629717, -3.8958735, -21.34047, -0.43775296, -3.8958735, -21.219788, -0.39460683, -3.9170194, -21.36012, -0.4289956, -3.9170194, -21.215515, -0.3736744, -3.8958735, -21.219788, -0.39460683, -3.8958735, -21.34047, -0.43775296, -3.9170194, -21.36012, -0.4289956, -3.9170194, -21.215515, -0.3736744, -3.9385924, -21.34047, -0.43775296, -3.9385924, -21.219788, -0.39460683, -3.9170194, -21.215515, -0.3736744, -3.9170194, -21.36012, -0.4289956, -3.9385924, -21.34047, -0.43775296, -3.9385924, -21.219788, -0.39460683, -3.9170194, -21.320818, -0.44629717, -3.9170194, -21.22406, -0.4155395, -3.9385924, -21.219788, -0.39460683, -3.9385924, -21.34047, -0.43775296, -3.9170194, -21.320818, -0.44629717, -7.464643, -21.40476, 5.450674, -7.3512235, -21.359264, 5.450674, -7.3512235, -21.40476, 5.450674, -7.464643, -21.40476, 5.450674, -7.464643, -21.359264, 5.450674, -7.3512235, -21.359264, 5.450674, -7.464643, -21.359264, 5.450674, -7.3512235, -21.359266, -1.0281336, -7.3512235, -21.359264, 5.450674, -7.464643, -21.359264, 5.450674, -7.464643, -21.359266, -1.0281336, -7.3512235, -21.359266, -1.0281336, -7.464643, -21.359266, -1.0281336, -7.3512235, -21.404762, -1.0281336, -7.3512235, -21.359266, -1.0281336, -7.464643, -21.359266, -1.0281336, -7.464643, -21.404762, -1.0281336, -7.3512235, -21.404762, -1.0281336, -7.464643, -21.404762, -1.0281336, -7.3512235, -21.40476, 5.450674, -7.3512235, -21.404762, -1.0281336, -7.464643, -21.404762, -1.0281336, -7.464643, -21.40476, 5.450674, -7.3512235, -21.40476, 5.450674, -7.3512235, -21.40476, 5.450674, -7.3512235, -21.359266, -1.0281336, -7.3512235, -21.404762, -1.0281336, -7.3512235, -21.40476, 5.450674, -7.3512235, -21.359264, 5.450674, -7.3512235, -21.359266, -1.0281336, -7.464643, -21.404762, -1.0281336, -7.464643, -21.359264, 5.450674, -7.464643, -21.40476, 5.450674, -7.464643, -21.404762, -1.0281336, -7.464643, -21.359266, -1.0281336, -7.464643, -21.359264, 5.450674, -6.9564967, -21.30843, -0.21347666, -3.4046016, -21.129436, -0.29165316, -6.9564967, -21.129436, -0.2134769, -6.9564967, -21.30843, -0.21347666, -3.4046016, -21.30843, -0.29165292, -3.4046016, -21.129436, -0.29165316, -7.7713675, -21.129436, -0.21689463, -6.9564967, -21.30843, -0.21347666, -6.9564967, -21.129436, -0.2134769, -7.7713675, -21.129436, -0.21689463, -7.7713675, -21.30843, -0.21689439, -6.9564967, -21.30843, -0.21347666, -6.9564967, -21.30843, 0.28014445, -3.4046016, -21.30843, -0.29165292, -6.9564967, -21.30843, -0.21347666, -6.9564967, -21.30843, 0.28014445, -3.4046016, -21.30843, 0.1893661, -3.4046016, -21.30843, -0.29165292, -7.7713675, -21.30843, -0.21689439, -6.9564967, -21.30843, 0.28014445, -6.9564967, -21.30843, -0.21347666, -7.7713675, -21.30843, -0.21689439, -7.7713675, -21.30843, 0.1893661, -6.9564967, -21.30843, 0.28014445, -7.7713675, -21.30843, -0.21689439, -7.7713675, -21.129436, 0.18936586, -7.7713675, -21.30843, 0.1893661, -7.7713675, -21.30843, -0.21689439, -7.7713675, -21.129436, -0.21689463, -7.7713675, -21.129436, 0.18936586, -6.9564967, -21.129436, 0.15668559, -6.9564967, -21.174292, 0.28014445, -7.1602683, -21.129436, 0.25750327, -6.9564967, -21.129436, 0.15668559, -6.0685763, -21.129436, 0.25750327, -6.9564967, -21.174292, 0.28014445, -6.9564967, -21.129436, 0.15668559, -4.0705953, -21.129436, 0.20624006, -6.0685763, -21.129436, 0.25750327, -7.1602683, -21.129436, 0.25750327, -6.9564967, -21.129436, -0.2134769, -6.9564967, -21.129436, 0.15668559, -7.1602683, -21.129436, 0.25750327, -7.7713675, -21.129436, -0.21689463, -6.9564967, -21.129436, -0.2134769, -7.7713675, -21.129436, -0.21689463, -7.1602683, -21.129436, 0.25750327, -7.7713675, -21.129436, 0.18936586, -3.4046016, -21.129436, -0.29165316, -6.9564967, -21.129436, 0.15668559, -6.9564967, -21.129436, -0.2134769, -6.9564967, -21.129436, 0.15668559, -3.4046016, -21.129436, -0.29165316, -4.0705953, -21.129436, 0.20624006, -4.0705953, -21.129436, 0.20624006, -3.4046016, -21.129436, -0.29165316, -3.4046016, -21.129436, 0.06911111, -4.0705953, -21.129436, 0.20624006, -3.4046016, -21.129436, 0.06911111, -3.4046016, -21.174292, 0.18936586, -7.7713675, -21.129436, 0.18936586, -6.9564967, -21.30843, 0.28014445, -7.7713675, -21.30843, 0.1893661, -6.9564967, -21.30843, 0.28014445, -7.7713675, -21.129436, 0.18936586, -7.1602683, -21.129436, 0.25750327, -6.9564967, -21.30843, 0.28014445, -7.1602683, -21.129436, 0.25750327, -6.9564967, -21.174292, 0.28014445, -6.9564967, -21.174292, 0.28014445, -3.4046016, -21.30843, 0.1893661, -6.9564967, -21.30843, 0.28014445, -6.9564967, -21.174292, 0.28014445, -4.0705953, -21.129436, 0.20624006, -3.4046016, -21.30843, 0.1893661, -3.4046016, -21.30843, 0.1893661, -4.0705953, -21.129436, 0.20624006, -3.4046016, -21.174292, 0.18936586, -4.0705953, -21.129436, 0.20624006, -6.9564967, -21.174292, 0.28014445, -6.0685763, -21.129436, 0.25750327, -3.4046016, -21.174292, 0.18936586, -3.4046016, -21.30843, -0.29165292, -3.4046016, -21.30843, 0.1893661, -3.4046016, -21.30843, -0.29165292, -3.4046016, -21.174292, 0.18936586, -3.4046016, -21.129436, 0.06911111, -3.4046016, -21.30843, -0.29165292, -3.4046016, -21.129436, 0.06911111, -3.4046016, -21.129436, -0.29165316, -7.7320657, -21.3975, -1.0330465, -7.351437, -19.082968, -0.9484632, -7.4360213, -21.3975, -1.0330465, -7.7320657, -21.3975, -1.0330465, -7.81665, -19.082968, -0.9484632, -7.351437, -19.082968, -0.9484632, -7.81665, -19.082968, -0.9484632, -7.351437, -19.082968, -1.4134626, -7.351437, -19.082968, -0.9484632, -7.81665, -19.082968, -0.9484632, -7.81665, -19.082968, -1.4134626, -7.351437, -19.082968, -1.4134626, -7.81665, -19.082968, -1.4134626, -7.4360213, -21.3975, -1.3288774, -7.351437, -19.082968, -1.4134626, -7.81665, -19.082968, -1.4134626, -7.7320657, -21.3975, -1.3288774, -7.4360213, -21.3975, -1.3288774, -7.7320657, -21.3975, -1.3288774, -7.4360213, -21.3975, -1.0330465, -7.4360213, -21.3975, -1.3288774, -7.7320657, -21.3975, -1.3288774, -7.7320657, -21.3975, -1.0330465, -7.4360213, -21.3975, -1.0330465, -7.4360213, -21.3975, -1.0330465, -7.351437, -19.082968, -1.4134626, -7.4360213, -21.3975, -1.3288774, -7.4360213, -21.3975, -1.0330465, -7.351437, -19.082968, -0.9484632, -7.351437, -19.082968, -1.4134626, -7.7320657, -21.3975, -1.3288774, -7.81665, -19.082968, -0.9484632, -7.7320657, -21.3975, -1.0330465, -7.7320657, -21.3975, -1.3288774, -7.81665, -19.082968, -1.4134626, -7.81665, -19.082968, -0.9484632, -3.988361, -21.129436, -0.4571905, -3.54707, -21.30843, -0.4477923, -3.988361, -21.30843, -0.45719028, -3.988361, -21.129436, -0.4571905, -3.54707, -21.129436, -0.44779253, -3.54707, -21.30843, -0.4477923, -3.988361, -21.30843, -0.45719028, -6.7189775, -21.129436, -0.4501419, -3.988361, -21.129436, -0.4571905, -3.988361, -21.30843, -0.45719028, -7.629112, -21.174292, -0.44779253, -6.7189775, -21.129436, -0.4501419, -7.629112, -21.174292, -0.44779253, -3.988361, -21.30843, -0.45719028, -7.629112, -21.30843, -0.4477923, -3.54707, -21.129436, -0.92881155, -3.988361, -21.129436, -0.4571905, -3.988361, -21.129436, -0.9401324, -6.7189775, -21.129436, -0.4501419, -3.988361, -21.129436, -0.9401324, -3.988361, -21.129436, -0.4571905, -6.7189775, -21.129436, -0.4501419, -7.629112, -21.129436, -0.8540528, -3.988361, -21.129436, -0.9401324, -7.629112, -21.129436, -0.8540528, -6.7189775, -21.129436, -0.4501419, -7.629112, -21.129436, -0.5492506, -7.629112, -21.129436, -0.5492506, -6.7189775, -21.129436, -0.4501419, -7.629112, -21.174292, -0.44779253, -3.988361, -21.129436, -0.4571905, -3.54707, -21.129436, -0.92881155, -3.78224, -21.129436, -0.6754861, -3.78224, -21.129436, -0.6754861, -3.54707, -21.129436, -0.92881155, -3.54707, -21.129436, -0.7350795, -3.54707, -21.129436, -0.7350795, -3.5872264, -21.197786, -0.68168044, -3.78224, -21.129436, -0.6754861, -3.5872264, -21.197786, -0.68168044, -3.54707, -21.129436, -0.6009412, -3.78224, -21.129436, -0.6754861, -3.988361, -21.129436, -0.4571905, -3.78224, -21.129436, -0.6754861, -3.54707, -21.129436, -0.6009412, -3.988361, -21.129436, -0.4571905, -3.54707, -21.129436, -0.6009412, -3.54707, -21.129436, -0.44779253, -3.988361, -21.30843, -0.94013214, -3.54707, -21.129436, -0.92881155, -3.988361, -21.129436, -0.9401324, -3.988361, -21.30843, -0.94013214, -3.54707, -21.30843, -0.9288113, -3.54707, -21.129436, -0.92881155, -7.629112, -21.129436, -0.8540528, -3.988361, -21.30843, -0.94013214, -3.988361, -21.129436, -0.9401324, -7.629112, -21.129436, -0.8540528, -7.629112, -21.30843, -0.85405254, -3.988361, -21.30843, -0.94013214, -3.54707, -21.30843, -0.4477923, -3.988361, -21.30843, -0.94013214, -3.988361, -21.30843, -0.45719028, -3.988361, -21.30843, -0.94013214, -3.54707, -21.30843, -0.4477923, -3.54707, -21.30843, -0.7079525, -3.988361, -21.30843, -0.94013214, -3.54707, -21.30843, -0.7079525, -3.54707, -21.30843, -0.9288113, -7.629112, -21.30843, -0.85405254, -3.988361, -21.30843, -0.45719028, -3.988361, -21.30843, -0.94013214, -7.629112, -21.30843, -0.85405254, -7.629112, -21.30843, -0.4477923, -3.988361, -21.30843, -0.45719028, -3.54707, -21.129436, -0.6009412, -3.5872264, -21.197786, -0.68168044, -3.54707, -21.30843, -0.7079525, -3.5872264, -21.197786, -0.68168044, -3.54707, -21.129436, -0.7350795, -3.54707, -21.30843, -0.7079525, -3.54707, -21.30843, -0.4477923, -3.54707, -21.129436, -0.6009412, -3.54707, -21.30843, -0.7079525, -3.54707, -21.30843, -0.4477923, -3.54707, -21.129436, -0.44779253, -3.54707, -21.129436, -0.6009412, -3.54707, -21.129436, -0.7350795, -3.54707, -21.30843, -0.9288113, -3.54707, -21.30843, -0.7079525, -3.54707, -21.129436, -0.7350795, -3.54707, -21.129436, -0.92881155, -3.54707, -21.30843, -0.9288113, -7.629112, -21.129436, -0.8540528, -7.629112, -21.30843, -0.4477923, -7.629112, -21.30843, -0.85405254, -7.629112, -21.30843, -0.4477923, -7.629112, -21.129436, -0.8540528, -7.629112, -21.129436, -0.5492506, -7.629112, -21.30843, -0.4477923, -7.629112, -21.129436, -0.5492506, -7.629112, -21.174292, -0.44779253, -3.8766494, -21.129435, 2.6811824, -3.4046016, -21.308428, 2.6905806, -3.8766494, -21.308428, 2.6811824, -3.8766494, -21.129435, 2.6811824, -3.4046016, -21.129435, 2.6905804, -3.4046016, -21.308428, 2.6905806, -3.8766494, -21.308428, 2.6811824, -6.797795, -21.129435, 2.688231, -3.8766494, -21.129435, 2.6811824, -3.8766494, -21.308428, 2.6811824, -7.7713675, -21.17429, 2.6905804, -6.797795, -21.129435, 2.688231, -7.7713675, -21.17429, 2.6905804, -3.8766494, -21.308428, 2.6811824, -7.7713675, -21.308428, 2.6905806, -3.4046016, -21.129435, 2.2095613, -3.8766494, -21.129435, 2.6811824, -3.8766494, -21.129435, 2.1984541, -6.797795, -21.129435, 2.688231, -3.8766494, -21.129435, 2.1984541, -3.8766494, -21.129435, 2.6811824, -6.797795, -21.129435, 2.688231, -7.7713675, -21.129435, 2.28432, -3.8766494, -21.129435, 2.1984541, -7.7713675, -21.129435, 2.28432, -6.797795, -21.129435, 2.688231, -7.7713675, -21.129435, 2.5891223, -7.7713675, -21.129435, 2.5891223, -6.797795, -21.129435, 2.688231, -7.7713675, -21.17429, 2.6905804, -3.8766494, -21.129435, 2.6811824, -3.4046016, -21.129435, 2.2095613, -3.656218, -21.129435, 2.4631002, -3.656218, -21.129435, 2.4631002, -3.4046016, -21.129435, 2.2095613, -3.4046016, -21.129435, 2.4032931, -3.4046016, -21.129435, 2.4032931, -3.4477482, -21.197786, 2.4566922, -3.656218, -21.129435, 2.4631002, -3.4477482, -21.197786, 2.4566922, -3.4046016, -21.129435, 2.5376453, -3.656218, -21.129435, 2.4631002, -3.8766494, -21.129435, 2.6811824, -3.656218, -21.129435, 2.4631002, -3.4046016, -21.129435, 2.5376453, -3.8766494, -21.129435, 2.6811824, -3.4046016, -21.129435, 2.5376453, -3.4046016, -21.129435, 2.6905804, -3.8766494, -21.30843, 2.1984544, -3.4046016, -21.129435, 2.2095613, -3.8766494, -21.129435, 2.1984541, -3.8766494, -21.30843, 2.1984544, -3.4046016, -21.30843, 2.2095613, -3.4046016, -21.129435, 2.2095613, -7.7713675, -21.129435, 2.28432, -3.8766494, -21.30843, 2.1984544, -3.8766494, -21.129435, 2.1984541, -7.7713675, -21.129435, 2.28432, -7.7713675, -21.30843, 2.28432, -3.8766494, -21.30843, 2.1984544, -3.4046016, -21.308428, 2.6905806, -3.8766494, -21.30843, 2.1984544, -3.8766494, -21.308428, 2.6811824, -3.8766494, -21.30843, 2.1984544, -3.4046016, -21.308428, 2.6905806, -3.4046016, -21.308428, 2.4306335, -3.8766494, -21.30843, 2.1984544, -3.4046016, -21.308428, 2.4306335, -3.4046016, -21.30843, 2.2095613, -7.7713675, -21.30843, 2.28432, -3.8766494, -21.308428, 2.6811824, -3.8766494, -21.30843, 2.1984544, -7.7713675, -21.30843, 2.28432, -7.7713675, -21.308428, 2.6905806, -3.8766494, -21.308428, 2.6811824, -3.4046016, -21.129435, 2.5376453, -3.4477482, -21.197786, 2.4566922, -3.4046016, -21.308428, 2.4306335, -3.4477482, -21.197786, 2.4566922, -3.4046016, -21.129435, 2.4032931, -3.4046016, -21.308428, 2.4306335, -3.4046016, -21.308428, 2.6905806, -3.4046016, -21.129435, 2.5376453, -3.4046016, -21.308428, 2.4306335, -3.4046016, -21.308428, 2.6905806, -3.4046016, -21.129435, 2.6905804, -3.4046016, -21.129435, 2.5376453, -3.4046016, -21.129435, 2.4032931, -3.4046016, -21.30843, 2.2095613, -3.4046016, -21.308428, 2.4306335, -3.4046016, -21.129435, 2.4032931, -3.4046016, -21.129435, 2.2095613, -3.4046016, -21.30843, 2.2095613, -7.7713675, -21.129435, 2.28432, -7.7713675, -21.308428, 2.6905806, -7.7713675, -21.30843, 2.28432, -7.7713675, -21.308428, 2.6905806, -7.7713675, -21.129435, 2.28432, -7.7713675, -21.129435, 2.5891223, -7.7713675, -21.308428, 2.6905806, -7.7713675, -21.129435, 2.5891223, -7.7713675, -21.17429, 2.6905804, -5.5538087, -21.308428, 2.944333, -3.4046016, -21.129435, 2.853768, -5.5538087, -21.129435, 2.944333, -5.5538087, -21.308428, 2.944333, -3.4046016, -21.308428, 2.8537683, -3.4046016, -21.129435, 2.853768, -7.7713675, -21.129435, 2.853768, -5.5538087, -21.308428, 2.944333, -5.5538087, -21.129435, 2.944333, -7.7713675, -21.129435, 2.853768, -7.7713675, -21.308428, 2.8537683, -5.5538087, -21.308428, 2.944333, -5.5538087, -21.308428, 3.4236436, -3.4046016, -21.308428, 2.8537683, -5.5538087, -21.308428, 2.944333, -5.5538087, -21.308428, 3.4236436, -3.4046016, -21.308428, 3.2600288, -3.4046016, -21.308428, 2.8537683, -5.5538087, -21.308428, 2.944333, -7.7713675, -21.308428, 3.4084783, -5.5538087, -21.308428, 3.4236436, -5.5538087, -21.308428, 2.944333, -7.7713675, -21.308428, 3.126531, -7.7713675, -21.308428, 3.4084783, -7.7713675, -21.308428, 3.126531, -5.5538087, -21.308428, 2.944333, -7.7713675, -21.308428, 2.8537683, -3.4046016, -21.308428, 3.2600288, -3.4046016, -21.129435, 2.853768, -3.4046016, -21.308428, 2.8537683, -3.4046016, -21.308428, 3.2600288, -3.4046016, -21.129435, 3.2600288, -3.4046016, -21.129435, 2.853768, -7.7713675, -21.129435, 3.0938506, -7.687638, -21.183048, 3.1414826, -7.7713675, -21.308428, 3.126531, -5.5538087, -21.129435, 3.3038158, -5.5538087, -21.17429, 3.4236436, -6.108092, -21.129435, 3.4197989, -5.5538087, -21.129435, 3.3038158, -5.0164003, -21.129435, 3.3828466, -5.5538087, -21.17429, 3.4236436, -5.5538087, -21.129435, 3.3038158, -3.4046016, -21.129435, 3.2600288, -5.0164003, -21.129435, 3.3828466, -5.5538087, -21.129435, 3.3038158, -3.4046016, -21.129435, 2.853768, -3.4046016, -21.129435, 3.2600288, -3.4046016, -21.129435, 2.853768, -5.5538087, -21.129435, 3.3038158, -5.5538087, -21.129435, 2.944333, -6.108092, -21.129435, 3.4197989, -5.5538087, -21.129435, 2.944333, -5.5538087, -21.129435, 3.3038158, -5.5538087, -21.129435, 2.944333, -7.7713675, -21.129435, 3.0938506, -7.7713675, -21.129435, 2.853768, -5.5538087, -21.129435, 2.944333, -7.3903117, -21.129435, 3.142764, -7.7713675, -21.129435, 3.0938506, -6.108092, -21.129435, 3.4197989, -7.3903117, -21.129435, 3.142764, -5.5538087, -21.129435, 2.944333, -7.687638, -21.183048, 3.1414826, -7.7713675, -21.129435, 3.0938506, -7.3903117, -21.129435, 3.142764, -7.7713675, -21.129435, 3.2290573, -7.687638, -21.183048, 3.1414826, -7.3903117, -21.129435, 3.142764, -7.3903117, -21.129435, 3.142764, -6.108092, -21.129435, 3.4197989, -7.7713675, -21.129435, 3.2290573, -7.7713675, -21.129435, 3.2290573, -6.108092, -21.129435, 3.4197989, -7.7713675, -21.129435, 3.4084783, -7.7713675, -21.308428, 2.8537683, -7.7713675, -21.129435, 3.0938506, -7.7713675, -21.308428, 3.126531, -7.7713675, -21.308428, 2.8537683, -7.7713675, -21.129435, 2.853768, -7.7713675, -21.129435, 3.0938506, -7.7713675, -21.129435, 3.2290573, -7.7713675, -21.308428, 3.4084783, -7.7713675, -21.308428, 3.126531, -7.7713675, -21.129435, 3.2290573, -7.7713675, -21.129435, 3.4084783, -7.7713675, -21.308428, 3.4084783, -7.687638, -21.183048, 3.1414826, -7.7713675, -21.129435, 3.2290573, -7.7713675, -21.308428, 3.126531, -7.7713675, -21.129435, 3.4084783, -5.5538087, -21.308428, 3.4236436, -7.7713675, -21.308428, 3.4084783, -5.5538087, -21.308428, 3.4236436, -7.7713675, -21.129435, 3.4084783, -6.108092, -21.129435, 3.4197989, -5.5538087, -21.308428, 3.4236436, -6.108092, -21.129435, 3.4197989, -5.5538087, -21.17429, 3.4236436, -5.5538087, -21.17429, 3.4236436, -3.4046016, -21.308428, 3.2600288, -5.5538087, -21.308428, 3.4236436, -5.5538087, -21.17429, 3.4236436, -3.4046016, -21.129435, 3.2600288, -3.4046016, -21.308428, 3.2600288, -3.4046016, -21.129435, 3.2600288, -5.5538087, -21.17429, 3.4236436, -5.0164003, -21.129435, 3.3828466, -3.988361, -21.129435, 4.050549, -3.54707, -21.308428, 4.0599475, -3.988361, -21.308428, 4.050549, -3.988361, -21.129435, 4.050549, -3.54707, -21.129435, 4.0599475, -3.54707, -21.308428, 4.0599475, -3.988361, -21.308428, 4.050549, -6.7189775, -21.129435, 4.057598, -3.988361, -21.129435, 4.050549, -3.988361, -21.308428, 4.050549, -7.629112, -21.17429, 4.0599475, -6.7189775, -21.129435, 4.057598, -7.629112, -21.17429, 4.0599475, -3.988361, -21.308428, 4.050549, -7.629112, -21.308428, 4.0599475, -3.54707, -21.129435, 3.5789282, -3.988361, -21.129435, 4.050549, -3.988361, -21.129435, 3.567821, -6.7189775, -21.129435, 4.057598, -3.988361, -21.129435, 3.567821, -3.988361, -21.129435, 4.050549, -6.7189775, -21.129435, 4.057598, -7.629112, -21.129435, 3.653687, -3.988361, -21.129435, 3.567821, -7.629112, -21.129435, 3.653687, -6.7189775, -21.129435, 4.057598, -7.629112, -21.129435, 3.958489, -7.629112, -21.129435, 3.958489, -6.7189775, -21.129435, 4.057598, -7.629112, -21.17429, 4.0599475, -3.988361, -21.129435, 4.050549, -3.54707, -21.129435, 3.5789282, -3.78224, -21.129435, 3.8322535, -3.78224, -21.129435, 3.8322535, -3.54707, -21.129435, 3.5789282, -3.54707, -21.129435, 3.7726603, -3.54707, -21.129435, 3.7726603, -3.5872264, -21.197784, 3.8260593, -3.78224, -21.129435, 3.8322535, -3.5872264, -21.197784, 3.8260593, -3.54707, -21.129435, 3.9070125, -3.78224, -21.129435, 3.8322535, -3.988361, -21.129435, 4.050549, -3.78224, -21.129435, 3.8322535, -3.54707, -21.129435, 3.9070125, -3.988361, -21.129435, 4.050549, -3.54707, -21.129435, 3.9070125, -3.54707, -21.129435, 4.0599475, -3.988361, -21.308428, 3.567821, -3.54707, -21.129435, 3.5789282, -3.988361, -21.129435, 3.567821, -3.988361, -21.308428, 3.567821, -3.54707, -21.308428, 3.5789282, -3.54707, -21.129435, 3.5789282, -7.629112, -21.129435, 3.653687, -3.988361, -21.308428, 3.567821, -3.988361, -21.129435, 3.567821, -7.629112, -21.129435, 3.653687, -7.629112, -21.308428, 3.653687, -3.988361, -21.308428, 3.567821, -3.54707, -21.308428, 4.0599475, -3.988361, -21.308428, 3.567821, -3.988361, -21.308428, 4.050549, -3.988361, -21.308428, 3.567821, -3.54707, -21.308428, 4.0599475, -3.54707, -21.308428, 3.799787, -3.988361, -21.308428, 3.567821, -3.54707, -21.308428, 3.799787, -3.54707, -21.308428, 3.5789282, -7.629112, -21.308428, 3.653687, -3.988361, -21.308428, 4.050549, -3.988361, -21.308428, 3.567821, -7.629112, -21.308428, 3.653687, -7.629112, -21.308428, 4.0599475, -3.988361, -21.308428, 4.050549, -3.54707, -21.129435, 3.9070125, -3.5872264, -21.197784, 3.8260593, -3.54707, -21.308428, 3.799787, -3.5872264, -21.197784, 3.8260593, -3.54707, -21.129435, 3.7726603, -3.54707, -21.308428, 3.799787, -3.54707, -21.308428, 4.0599475, -3.54707, -21.129435, 3.9070125, -3.54707, -21.308428, 3.799787, -3.54707, -21.308428, 4.0599475, -3.54707, -21.129435, 4.0599475, -3.54707, -21.129435, 3.9070125, -3.54707, -21.129435, 3.7726603, -3.54707, -21.308428, 3.5789282, -3.54707, -21.308428, 3.799787, -3.54707, -21.129435, 3.7726603, -3.54707, -21.129435, 3.5789282, -3.54707, -21.308428, 3.5789282, -7.629112, -21.129435, 3.653687, -7.629112, -21.308428, 4.0599475, -7.629112, -21.308428, 3.653687, -7.629112, -21.308428, 4.0599475, -7.629112, -21.129435, 3.653687, -7.629112, -21.129435, 3.958489, -7.629112, -21.308428, 4.0599475, -7.629112, -21.129435, 3.958489, -7.629112, -21.17429, 4.0599475, -3.950554, -21.3975, -1.0022886, -3.6320815, -19.082968, -1.0409505, -3.5934205, -21.3975, -1.0022886, -3.950554, -21.3975, -1.0022886, -3.911893, -19.082968, -1.0409505, -3.6320815, -19.082968, -1.0409505, -3.911893, -19.082968, -1.0409505, -3.6320815, -19.082968, -1.3209753, -3.6320815, -19.082968, -1.0409505, -3.911893, -19.082968, -1.0409505, -3.911893, -19.082968, -1.3209753, -3.6320815, -19.082968, -1.3209753, -3.911893, -19.082968, -1.3209753, -3.5934205, -21.3975, -1.3596354, -3.6320815, -19.082968, -1.3209753, -3.911893, -19.082968, -1.3209753, -3.950554, -21.3975, -1.3596354, -3.5934205, -21.3975, -1.3596354, -3.950554, -21.3975, -1.3596354, -3.5934205, -21.3975, -1.0022886, -3.5934205, -21.3975, -1.3596354, -3.950554, -21.3975, -1.3596354, -3.950554, -21.3975, -1.0022886, -3.5934205, -21.3975, -1.0022886, -3.5934205, -21.3975, -1.0022886, -3.6320815, -19.082968, -1.3209753, -3.5934205, -21.3975, -1.3596354, -3.5934205, -21.3975, -1.0022886, -3.6320815, -19.082968, -1.0409505, -3.6320815, -19.082968, -1.3209753, -3.950554, -21.3975, -1.3596354, -3.911893, -19.082968, -1.0409505, -3.950554, -21.3975, -1.0022886, -3.950554, -21.3975, -1.3596354, -3.911893, -19.082968, -1.3209753, -3.911893, -19.082968, -1.0409505, -7.8010573, -21.397497, 5.803108, -7.4439244, -19.082966, 5.8031073, -7.4439244, -21.397497, 5.803108, -7.8010573, -21.397497, 5.803108, -7.8010573, -19.082966, 5.8031073, -7.4439244, -19.082966, 5.8031073, -7.8010573, -21.397497, 5.4459753, -7.4439244, -21.397497, 5.803108, -7.4439244, -21.397497, 5.4459753, -7.8010573, -21.397497, 5.4459753, -7.8010573, -21.397497, 5.803108, -7.4439244, -21.397497, 5.803108, -7.4439244, -21.397497, 5.803108, -7.4439244, -19.082966, 5.4459743, -7.4439244, -21.397497, 5.4459753, -7.4439244, -21.397497, 5.803108, -7.4439244, -19.082966, 5.8031073, -7.4439244, -19.082966, 5.4459743, -7.8010573, -19.661598, 5.4459743, -7.7117743, -19.082966, 5.4459743, -7.8010573, -19.082966, 5.5352573, -7.8010573, -21.397497, 5.803108, -7.8010573, -19.661598, 5.4459743, -7.8010573, -19.082966, 5.5352573, -7.8010573, -19.661598, 5.4459743, -7.8010573, -21.397497, 5.803108, -7.8010573, -21.397497, 5.4459753, -7.8010573, -21.397497, 5.803108, -7.8010573, -19.082966, 5.5352573, -7.8010573, -19.082966, 5.8031073, -7.8010573, -19.082966, 5.5352573, -7.4439244, -19.082966, 5.8031073, -7.8010573, -19.082966, 5.8031073, -7.4439244, -19.082966, 5.8031073, -7.8010573, -19.082966, 5.5352573, -7.7117743, -19.082966, 5.4459743, -7.4439244, -19.082966, 5.8031073, -7.7117743, -19.082966, 5.4459743, -7.4439244, -19.082966, 5.4459743, -7.8010573, -21.397497, 5.4459753, -7.7117743, -19.082966, 5.4459743, -7.8010573, -19.661598, 5.4459743, -7.7117743, -19.082966, 5.4459743, -7.8010573, -21.397497, 5.4459753, -7.4439244, -21.397497, 5.4459753, -7.7117743, -19.082966, 5.4459743, -7.4439244, -21.397497, 5.4459753, -7.4439244, -19.082966, 5.4459743, -5.555945, -21.308428, 4.955558, -3.54707, -21.129435, 4.8647785, -5.555945, -21.129435, 4.9555573, -5.555945, -21.308428, 4.955558, -3.54707, -21.308428, 4.864779, -3.54707, -21.129435, 4.8647785, -7.629112, -21.129435, 4.8647785, -5.555945, -21.308428, 4.955558, -5.555945, -21.129435, 4.9555573, -7.629112, -21.129435, 4.8647785, -7.629112, -21.308428, 4.864779, -5.555945, -21.308428, 4.955558, -5.555945, -21.308428, 5.434868, -3.54707, -21.308428, 4.864779, -5.555945, -21.308428, 4.955558, -5.555945, -21.308428, 5.434868, -3.54707, -21.308428, 5.2710395, -3.54707, -21.308428, 4.864779, -5.555945, -21.308428, 4.955558, -7.629112, -21.308428, 5.4197025, -5.555945, -21.308428, 5.434868, -5.555945, -21.308428, 4.955558, -7.629112, -21.308428, 5.137542, -7.629112, -21.308428, 5.4197025, -7.629112, -21.308428, 5.137542, -5.555945, -21.308428, 4.955558, -7.629112, -21.308428, 4.864779, -3.54707, -21.308428, 5.2710395, -3.54707, -21.129435, 4.8647785, -3.54707, -21.308428, 4.864779, -3.54707, -21.308428, 5.2710395, -3.54707, -21.129435, 5.271039, -3.54707, -21.129435, 4.8647785, -7.629112, -21.129435, 5.1048613, -7.5507226, -21.183046, 5.1524935, -7.629112, -21.308428, 5.137542, -5.555945, -21.129435, 5.31504, -5.555945, -21.17429, 5.434868, -6.0741305, -21.129435, 5.431023, -5.555945, -21.129435, 5.31504, -5.0537796, -21.129435, 5.3940706, -5.555945, -21.17429, 5.434868, -5.555945, -21.129435, 5.31504, -3.54707, -21.129435, 5.271039, -5.0537796, -21.129435, 5.3940706, -5.555945, -21.129435, 5.31504, -3.54707, -21.129435, 4.8647785, -3.54707, -21.129435, 5.271039, -3.54707, -21.129435, 4.8647785, -5.555945, -21.129435, 5.31504, -5.555945, -21.129435, 4.9555573, -6.0741305, -21.129435, 5.431023, -5.555945, -21.129435, 4.9555573, -5.555945, -21.129435, 5.31504, -5.555945, -21.129435, 4.9555573, -7.629112, -21.129435, 5.1048613, -7.629112, -21.129435, 4.8647785, -5.555945, -21.129435, 4.9555573, -7.2726197, -21.129435, 5.1539884, -7.629112, -21.129435, 5.1048613, -6.0741305, -21.129435, 5.431023, -7.2726197, -21.129435, 5.1539884, -5.555945, -21.129435, 4.9555573, -7.5507226, -21.183046, 5.1524935, -7.629112, -21.129435, 5.1048613, -7.2726197, -21.129435, 5.1539884, -7.629112, -21.129435, 5.240281, -7.5507226, -21.183046, 5.1524935, -7.2726197, -21.129435, 5.1539884, -7.2726197, -21.129435, 5.1539884, -6.0741305, -21.129435, 5.431023, -7.629112, -21.129435, 5.240281, -7.629112, -21.129435, 5.240281, -6.0741305, -21.129435, 5.431023, -7.629112, -21.129435, 5.4197025, -7.629112, -21.308428, 4.864779, -7.629112, -21.129435, 5.1048613, -7.629112, -21.308428, 5.137542, -7.629112, -21.308428, 4.864779, -7.629112, -21.129435, 4.8647785, -7.629112, -21.129435, 5.1048613, -7.629112, -21.129435, 5.240281, -7.629112, -21.308428, 5.4197025, -7.629112, -21.308428, 5.137542, -7.629112, -21.129435, 5.240281, -7.629112, -21.129435, 5.4197025, -7.629112, -21.308428, 5.4197025, -7.5507226, -21.183046, 5.1524935, -7.629112, -21.129435, 5.240281, -7.629112, -21.308428, 5.137542, -7.629112, -21.129435, 5.4197025, -5.555945, -21.308428, 5.434868, -7.629112, -21.308428, 5.4197025, -5.555945, -21.308428, 5.434868, -7.629112, -21.129435, 5.4197025, -6.0741305, -21.129435, 5.431023, -5.555945, -21.308428, 5.434868, -6.0741305, -21.129435, 5.431023, -5.555945, -21.17429, 5.434868, -5.555945, -21.17429, 5.434868, -3.54707, -21.308428, 5.2710395, -5.555945, -21.308428, 5.434868, -5.555945, -21.17429, 5.434868, -3.54707, -21.129435, 5.271039, -3.54707, -21.308428, 5.2710395, -3.54707, -21.129435, 5.271039, -5.555945, -21.17429, 5.434868, -5.0537796, -21.129435, 5.3940706, -6.926807, -21.30843, 1.0420431, -3.4520202, -21.129435, 0.96386683, -6.926807, -21.129435, 1.0420431, -6.926807, -21.30843, 1.0420431, -3.4520202, -21.30843, 0.96386683, -3.4520202, -21.129435, 0.96386683, -7.723949, -21.129435, 1.0386255, -6.926807, -21.30843, 1.0420431, -6.926807, -21.129435, 1.0420431, -7.723949, -21.129435, 1.0386255, -7.723949, -21.30843, 1.0386255, -6.926807, -21.30843, 1.0420431, -6.926807, -21.30843, 1.5358781, -3.4520202, -21.30843, 0.96386683, -6.926807, -21.30843, 1.0420431, -6.926807, -21.30843, 1.5358781, -3.4520202, -21.30843, 1.444886, -3.4520202, -21.30843, 0.96386683, -7.723949, -21.30843, 1.0386255, -6.926807, -21.30843, 1.5358781, -6.926807, -21.30843, 1.0420431, -7.723949, -21.30843, 1.0386255, -7.723949, -21.30843, 1.444886, -6.926807, -21.30843, 1.5358781, -7.723949, -21.30843, 1.0386255, -7.723949, -21.129435, 1.444886, -7.723949, -21.30843, 1.444886, -7.723949, -21.30843, 1.0386255, -7.723949, -21.129435, 1.0386255, -7.723949, -21.129435, 1.444886, -6.926807, -21.129435, 1.4124193, -6.926807, -21.17429, 1.5358781, -7.1260924, -21.129435, 1.5130231, -6.926807, -21.129435, 1.4124193, -6.0581107, -21.129435, 1.5130231, -6.926807, -21.17429, 1.5358781, -6.926807, -21.129435, 1.4124193, -4.1037025, -21.129435, 1.4619737, -6.0581107, -21.129435, 1.5130231, -7.1260924, -21.129435, 1.5130231, -6.926807, -21.129435, 1.0420431, -6.926807, -21.129435, 1.4124193, -7.1260924, -21.129435, 1.5130231, -7.723949, -21.129435, 1.0386255, -6.926807, -21.129435, 1.0420431, -7.723949, -21.129435, 1.0386255, -7.1260924, -21.129435, 1.5130231, -7.723949, -21.129435, 1.444886, -3.4520202, -21.129435, 0.96386683, -6.926807, -21.129435, 1.4124193, -6.926807, -21.129435, 1.0420431, -6.926807, -21.129435, 1.4124193, -3.4520202, -21.129435, 0.96386683, -4.1037025, -21.129435, 1.4619737, -4.1037025, -21.129435, 1.4619737, -3.4520202, -21.129435, 0.96386683, -3.4520202, -21.129435, 1.3246312, -4.1037025, -21.129435, 1.4619737, -3.4520202, -21.129435, 1.3246312, -3.4520202, -21.17429, 1.444886, -7.723949, -21.129435, 1.444886, -6.926807, -21.30843, 1.5358781, -7.723949, -21.30843, 1.444886, -6.926807, -21.30843, 1.5358781, -7.723949, -21.129435, 1.444886, -7.1260924, -21.129435, 1.5130231, -6.926807, -21.30843, 1.5358781, -7.1260924, -21.129435, 1.5130231, -6.926807, -21.17429, 1.5358781, -6.926807, -21.17429, 1.5358781, -3.4520202, -21.30843, 1.444886, -6.926807, -21.30843, 1.5358781, -6.926807, -21.17429, 1.5358781, -4.1037025, -21.129435, 1.4619737, -3.4520202, -21.30843, 1.444886, -3.4520202, -21.30843, 1.444886, -4.1037025, -21.129435, 1.4619737, -3.4520202, -21.17429, 1.444886, -4.1037025, -21.129435, 1.4619737, -6.926807, -21.17429, 1.5358781, -6.0581107, -21.129435, 1.5130231, -3.4520202, -21.17429, 1.444886, -3.4520202, -21.30843, 0.96386683, -3.4520202, -21.30843, 1.444886, -3.4520202, -21.30843, 0.96386683, -3.4520202, -21.17429, 1.444886, -3.4520202, -21.129435, 1.3246312, -3.4520202, -21.30843, 0.96386683, -3.4520202, -21.129435, 1.3246312, -3.4520202, -21.129435, 0.96386683, -6.9564967, -21.308428, 4.294263, -3.4046016, -21.129435, 4.2160864, -6.9564967, -21.129435, 4.294263, -6.9564967, -21.308428, 4.294263, -3.4046016, -21.308428, 4.2160864, -3.4046016, -21.129435, 4.2160864, -7.7713675, -21.129435, 4.290845, -6.9564967, -21.308428, 4.294263, -6.9564967, -21.129435, 4.294263, -7.7713675, -21.129435, 4.290845, -7.7713675, -21.308428, 4.2908454, -6.9564967, -21.308428, 4.294263, -6.9564967, -21.308428, 4.787884, -3.4046016, -21.308428, 4.2160864, -6.9564967, -21.308428, 4.294263, -6.9564967, -21.308428, 4.787884, -3.4046016, -21.308428, 4.6971054, -3.4046016, -21.308428, 4.2160864, -7.7713675, -21.308428, 4.2908454, -6.9564967, -21.308428, 4.787884, -6.9564967, -21.308428, 4.294263, -7.7713675, -21.308428, 4.2908454, -7.7713675, -21.308428, 4.6971054, -6.9564967, -21.308428, 4.787884, -7.7713675, -21.308428, 4.2908454, -7.7713675, -21.129435, 4.6971054, -7.7713675, -21.308428, 4.6971054, -7.7713675, -21.308428, 4.2908454, -7.7713675, -21.129435, 4.290845, -7.7713675, -21.129435, 4.6971054, -6.9564967, -21.129435, 4.6646385, -6.9564967, -21.17429, 4.7878838, -7.1602683, -21.129435, 4.7652426, -6.9564967, -21.129435, 4.6646385, -6.0685763, -21.129435, 4.7652426, -6.9564967, -21.17429, 4.7878838, -6.9564967, -21.129435, 4.6646385, -4.0705953, -21.129435, 4.7141933, -6.0685763, -21.129435, 4.7652426, -7.1602683, -21.129435, 4.7652426, -6.9564967, -21.129435, 4.294263, -6.9564967, -21.129435, 4.6646385, -7.1602683, -21.129435, 4.7652426, -7.7713675, -21.129435, 4.290845, -6.9564967, -21.129435, 4.294263, -7.7713675, -21.129435, 4.290845, -7.1602683, -21.129435, 4.7652426, -7.7713675, -21.129435, 4.6971054, -3.4046016, -21.129435, 4.2160864, -6.9564967, -21.129435, 4.6646385, -6.9564967, -21.129435, 4.294263, -6.9564967, -21.129435, 4.6646385, -3.4046016, -21.129435, 4.2160864, -4.0705953, -21.129435, 4.7141933, -4.0705953, -21.129435, 4.7141933, -3.4046016, -21.129435, 4.2160864, -3.4046016, -21.129435, 4.576851, -4.0705953, -21.129435, 4.7141933, -3.4046016, -21.129435, 4.576851, -3.4046016, -21.17429, 4.6971054, -7.7713675, -21.129435, 4.6971054, -6.9564967, -21.308428, 4.787884, -7.7713675, -21.308428, 4.6971054, -6.9564967, -21.308428, 4.787884, -7.7713675, -21.129435, 4.6971054, -7.1602683, -21.129435, 4.7652426, -6.9564967, -21.308428, 4.787884, -7.1602683, -21.129435, 4.7652426, -6.9564967, -21.17429, 4.7878838, -6.9564967, -21.17429, 4.7878838, -3.4046016, -21.308428, 4.6971054, -6.9564967, -21.308428, 4.787884, -6.9564967, -21.17429, 4.7878838, -4.0705953, -21.129435, 4.7141933, -3.4046016, -21.308428, 4.6971054, -3.4046016, -21.308428, 4.6971054, -4.0705953, -21.129435, 4.7141933, -3.4046016, -21.17429, 4.6971054, -4.0705953, -21.129435, 4.7141933, -6.9564967, -21.17429, 4.7878838, -6.0685763, -21.129435, 4.7652426, -3.4046016, -21.17429, 4.6971054, -3.4046016, -21.308428, 4.2160864, -3.4046016, -21.308428, 4.6971054, -3.4046016, -21.308428, 4.2160864, -3.4046016, -21.17429, 4.6971054, -3.4046016, -21.129435, 4.576851, -3.4046016, -21.308428, 4.2160864, -3.4046016, -21.129435, 4.576851, -3.4046016, -21.129435, 4.2160864, -3.8851933, -21.397497, 5.744369, -3.5280604, -19.082966, 5.744368, -3.5280604, -21.397497, 5.744369, -3.8851933, -21.397497, 5.744369, -3.8851933, -19.351458, 5.7443686, -3.5280604, -19.082966, 5.744368, -3.8851933, -19.351458, 5.7443686, -3.5280604, -19.082966, 5.387021, -3.5280604, -19.082966, 5.744368, -3.8851933, -19.351458, 5.7443686, -3.8851933, -19.082966, 5.387021, -3.5280604, -19.082966, 5.387021, -3.8851933, -19.082966, 5.387021, -3.5280604, -21.397497, 5.387022, -3.5280604, -19.082966, 5.387021, -3.8851933, -19.082966, 5.387021, -3.8851933, -21.397497, 5.387022, -3.5280604, -21.397497, 5.387022, -3.8851933, -21.397497, 5.387022, -3.5280604, -21.397497, 5.744369, -3.5280604, -21.397497, 5.387022, -3.8851933, -21.397497, 5.387022, -3.8851933, -21.397497, 5.744369, -3.5280604, -21.397497, 5.744369, -3.5280604, -21.397497, 5.744369, -3.5280604, -19.082966, 5.387021, -3.5280604, -21.397497, 5.387022, -3.5280604, -21.397497, 5.744369, -3.5280604, -19.082966, 5.744368, -3.5280604, -19.082966, 5.387021, -3.8851933, -21.397497, 5.387022, -3.8851933, -19.351458, 5.7443686, -3.8851933, -21.397497, 5.744369, -3.8851933, -21.397497, 5.387022, -3.8851933, -19.082966, 5.387021, -3.8851933, -19.351458, 5.7443686, -3.8144932, -21.40476, 5.450674, -3.7010732, -21.359264, 5.450674, -3.7010732, -21.40476, 5.450674, -3.8144932, -21.40476, 5.450674, -3.8144932, -21.359264, 5.450674, -3.7010732, -21.359264, 5.450674, -3.8144932, -21.359264, 5.450674, -3.7010732, -21.359266, -1.0281336, -3.7010732, -21.359264, 5.450674, -3.8144932, -21.359264, 5.450674, -3.8144932, -21.359266, -1.0281336, -3.7010732, -21.359266, -1.0281336, -3.8144932, -21.359266, -1.0281336, -3.7010732, -21.404762, -1.0281336, -3.7010732, -21.359266, -1.0281336, -3.8144932, -21.359266, -1.0281336, -3.8144932, -21.404762, -1.0281336, -3.7010732, -21.404762, -1.0281336, -3.8144932, -21.404762, -1.0281336, -3.7010732, -21.40476, 5.450674, -3.7010732, -21.404762, -1.0281336, -3.8144932, -21.404762, -1.0281336, -3.8144932, -21.40476, 5.450674, -3.7010732, -21.40476, 5.450674, -3.7010732, -21.40476, 5.450674, -3.7010732, -21.359266, -1.0281336, -3.7010732, -21.404762, -1.0281336, -3.7010732, -21.40476, 5.450674, -3.7010732, -21.359264, 5.450674, -3.7010732, -21.359266, -1.0281336, -3.8144932, -21.404762, -1.0281336, -3.8144932, -21.359264, 5.450674, -3.8144932, -21.40476, 5.450674, -3.8144932, -21.404762, -1.0281336, -3.8144932, -21.359266, -1.0281336, -3.8144932, -21.359264, 5.450674, -7.4172244, -21.333633, 0.22268713, -7.393088, -21.35008, -0.25406027, -7.393088, -21.355635, 0.23059022, -7.4172244, -21.333633, 0.22268713, -7.4172244, -21.328295, -0.24551606, -7.393088, -21.35008, -0.25406027, -7.4172244, -21.328295, -0.24551606, -7.393088, -21.208893, -0.27221608, -7.393088, -21.35008, -0.25406027, -7.4172244, -21.328295, -0.24551606, -7.4172244, -21.21466, -0.25277877, -7.393088, -21.208893, -0.27221608, -7.393088, -21.355635, 0.23059022, -7.4172244, -21.371868, -0.26239038, -7.4172244, -21.377848, 0.2384932, -7.393088, -21.355635, 0.23059022, -7.393088, -21.35008, -0.25406027, -7.4172244, -21.371868, -0.26239038, -7.393088, -21.35008, -0.25406027, -7.4172244, -21.20334, -0.29122615, -7.4172244, -21.371868, -0.26239038, -7.393088, -21.35008, -0.25406027, -7.393088, -21.208893, -0.27221608, -7.4172244, -21.20334, -0.29122615, -7.4172244, -21.377848, 0.2384932, -7.441361, -21.35008, -0.25406027, -7.441361, -21.355635, 0.23059022, -7.4172244, -21.377848, 0.2384932, -7.4172244, -21.371868, -0.26239038, -7.441361, -21.35008, -0.25406027, -7.4172244, -21.371868, -0.26239038, -7.441361, -21.208893, -0.27221608, -7.441361, -21.35008, -0.25406027, -7.4172244, -21.371868, -0.26239038, -7.4172244, -21.20334, -0.29122615, -7.441361, -21.208893, -0.27221608, -7.441361, -21.355635, 0.23059022, -7.4172244, -21.328295, -0.24551606, -7.4172244, -21.333633, 0.22268713, -7.441361, -21.355635, 0.23059022, -7.441361, -21.35008, -0.25406027, -7.4172244, -21.328295, -0.24551606, -7.441361, -21.35008, -0.25406027, -7.4172244, -21.21466, -0.25277877, -7.4172244, -21.328295, -0.24551606, -7.441361, -21.35008, -0.25406027, -7.441361, -21.208893, -0.27221608, -7.4172244, -21.21466, -0.25277877, -7.4172244, -21.21466, -0.25277877, -7.393088, -21.090775, -0.24231243, -7.393088, -21.208893, -0.27221608, -7.4172244, -21.21466, -0.25277877, -7.4172244, -21.114271, -0.23804045, -7.393088, -21.090775, -0.24231243, -7.4172244, -21.114271, -0.23804045, -7.393088, -21.088425, 0.22674549, -7.393088, -21.090775, -0.24231243, -7.4172244, -21.114271, -0.23804045, -7.4172244, -21.111708, 0.22204626, -7.393088, -21.088425, 0.22674549, -7.4172244, -21.111708, 0.22204626, -7.393088, -21.219574, 0.2705326, -7.393088, -21.088425, 0.22674549, -7.4172244, -21.111708, 0.22204626, -7.4172244, -21.224485, 0.2510954, -7.393088, -21.219574, 0.2705326, -7.393088, -21.208893, -0.27221608, -7.4172244, -21.06728, -0.24658465, -7.4172244, -21.20334, -0.29122615, -7.393088, -21.208893, -0.27221608, -7.393088, -21.090775, -0.24231243, -7.4172244, -21.06728, -0.24658465, -7.393088, -21.090775, -0.24231243, -7.4172244, -21.06493, 0.23144436, -7.4172244, -21.06728, -0.24658465, -7.393088, -21.090775, -0.24231243, -7.393088, -21.088425, 0.22674549, -7.4172244, -21.06493, 0.23144436, -7.393088, -21.088425, 0.22674549, -7.4172244, -21.214874, 0.28996992, -7.4172244, -21.06493, 0.23144436, -7.393088, -21.088425, 0.22674549, -7.393088, -21.219574, 0.2705326, -7.4172244, -21.214874, 0.28996992, -7.4172244, -21.20334, -0.29122615, -7.441361, -21.090775, -0.24231243, -7.441361, -21.208893, -0.27221608, -7.4172244, -21.20334, -0.29122615, -7.4172244, -21.06728, -0.24658465, -7.441361, -21.090775, -0.24231243, -7.441361, -21.208893, -0.27221608, -7.4172244, -21.114271, -0.23804045, -7.4172244, -21.21466, -0.25277877, -7.441361, -21.208893, -0.27221608, -7.441361, -21.090775, -0.24231243, -7.4172244, -21.114271, -0.23804045, -7.441361, -21.090775, -0.24231243, -7.4172244, -21.111708, 0.22204626, -7.4172244, -21.114271, -0.23804045, -7.441361, -21.090775, -0.24231243, -7.441361, -21.088425, 0.22674549, -7.4172244, -21.111708, 0.22204626, -7.441361, -21.088425, 0.22674549, -7.4172244, -21.224485, 0.2510954, -7.4172244, -21.111708, 0.22204626, -7.441361, -21.088425, 0.22674549, -7.441361, -21.219574, 0.2705326, -7.4172244, -21.224485, 0.2510954, -7.4172244, -21.06728, -0.24658465, -7.441361, -21.088425, 0.22674549, -7.441361, -21.090775, -0.24231243, -7.4172244, -21.06728, -0.24658465, -7.4172244, -21.06493, 0.23144436, -7.441361, -21.088425, 0.22674549, -7.4172244, -21.06493, 0.23144436, -7.441361, -21.219574, 0.2705326, -7.441361, -21.088425, 0.22674549, -7.4172244, -21.06493, 0.23144436, -7.4172244, -21.214874, 0.28996992, -7.441361, -21.219574, 0.2705326, -7.4172244, -21.224485, 0.2510954, -7.393088, -21.355635, 0.23059022, -7.393088, -21.219574, 0.2705326, -7.4172244, -21.224485, 0.2510954, -7.4172244, -21.333633, 0.22268713, -7.393088, -21.355635, 0.23059022, -7.393088, -21.219574, 0.2705326, -7.4172244, -21.377848, 0.2384932, -7.4172244, -21.214874, 0.28996992, -7.393088, -21.219574, 0.2705326, -7.393088, -21.355635, 0.23059022, -7.4172244, -21.377848, 0.2384932, -7.4172244, -21.214874, 0.28996992, -7.441361, -21.355635, 0.23059022, -7.441361, -21.219574, 0.2705326, -7.4172244, -21.214874, 0.28996992, -7.4172244, -21.377848, 0.2384932, -7.441361, -21.355635, 0.23059022, -7.441361, -21.219574, 0.2705326, -7.4172244, -21.333633, 0.22268713, -7.4172244, -21.224485, 0.2510954, -7.441361, -21.219574, 0.2705326, -7.441361, -21.355635, 0.23059022, -7.4172244, -21.333633, 0.22268713, -3.8775039, -21.320818, 0.20923042, -3.8561444, -21.335342, -0.30446887, -3.8561444, -21.34047, 0.21777427, -3.8775039, -21.320818, 0.20923042, -3.8775039, -21.316118, -0.29528427, -3.8561444, -21.335342, -0.30446887, -3.8775039, -21.316118, -0.29528427, -3.8561444, -21.210388, -0.32390642, -3.8561444, -21.335342, -0.30446887, -3.8775039, -21.316118, -0.29528427, -3.8775039, -21.215515, -0.3031876, -3.8561444, -21.210388, -0.32390642, -3.8561444, -21.34047, 0.21777427, -3.8775039, -21.35478, -0.31322622, -3.8775039, -21.36012, 0.22631812, -3.8561444, -21.34047, 0.21777427, -3.8561444, -21.335342, -0.30446887, -3.8775039, -21.35478, -0.31322622, -3.8561444, -21.335342, -0.30446887, -3.8775039, -21.205263, -0.34462523, -3.8775039, -21.35478, -0.31322622, -3.8561444, -21.335342, -0.30446887, -3.8561444, -21.210388, -0.32390642, -3.8775039, -21.205263, -0.34462523, -3.8775039, -21.36012, 0.22631812, -3.8988638, -21.335342, -0.30446887, -3.8988638, -21.34047, 0.21777427, -3.8775039, -21.36012, 0.22631812, -3.8775039, -21.35478, -0.31322622, -3.8988638, -21.335342, -0.30446887, -3.8775039, -21.35478, -0.31322622, -3.8988638, -21.210388, -0.32390642, -3.8988638, -21.335342, -0.30446887, -3.8775039, -21.35478, -0.31322622, -3.8775039, -21.205263, -0.34462523, -3.8988638, -21.210388, -0.32390642, -3.8988638, -21.34047, 0.21777427, -3.8775039, -21.316118, -0.29528427, -3.8775039, -21.320818, 0.20923042, -3.8988638, -21.34047, 0.21777427, -3.8988638, -21.335342, -0.30446887, -3.8775039, -21.316118, -0.29528427, -3.8988638, -21.335342, -0.30446887, -3.8775039, -21.215515, -0.3031876, -3.8775039, -21.316118, -0.29528427, -3.8988638, -21.335342, -0.30446887, -3.8988638, -21.210388, -0.32390642, -3.8775039, -21.215515, -0.3031876, -3.8775039, -21.215515, -0.3031876, -3.8561444, -21.105513, -0.29165316, -3.8561444, -21.210388, -0.32390642, -3.8775039, -21.215515, -0.3031876, -3.8775039, -21.126446, -0.2871678, -3.8561444, -21.105513, -0.29165316, -3.8775039, -21.126446, -0.2871678, -3.8561444, -21.10359, 0.2135024, -3.8561444, -21.105513, -0.29165316, -3.8775039, -21.126446, -0.2871678, -3.8775039, -21.12431, 0.20858967, -3.8561444, -21.10359, 0.2135024, -3.8775039, -21.12431, 0.20858967, -3.8561444, -21.219788, 0.26070714, -3.8561444, -21.10359, 0.2135024, -3.8775039, -21.12431, 0.20858967, -3.8775039, -21.224058, 0.23977482, -3.8561444, -21.219788, 0.26070714, -3.8561444, -21.210388, -0.32390642, -3.8775039, -21.084795, -0.2963524, -3.8775039, -21.205263, -0.34462523, -3.8561444, -21.210388, -0.32390642, -3.8561444, -21.105513, -0.29165316, -3.8775039, -21.084795, -0.2963524, -3.8561444, -21.105513, -0.29165316, -3.8775039, -21.082659, 0.21862864, -3.8775039, -21.084795, -0.2963524, -3.8561444, -21.105513, -0.29165316, -3.8561444, -21.10359, 0.2135024, -3.8775039, -21.082659, 0.21862864, -3.8561444, -21.10359, 0.2135024, -3.8775039, -21.215515, 0.2816397, -3.8775039, -21.082659, 0.21862864, -3.8561444, -21.10359, 0.2135024, -3.8561444, -21.219788, 0.26070714, -3.8775039, -21.215515, 0.2816397, -3.8775039, -21.205263, -0.34462523, -3.8988638, -21.105513, -0.29165316, -3.8988638, -21.210388, -0.32390642, -3.8775039, -21.205263, -0.34462523, -3.8775039, -21.084795, -0.2963524, -3.8988638, -21.105513, -0.29165316, -3.8988638, -21.210388, -0.32390642, -3.8775039, -21.126446, -0.2871678, -3.8775039, -21.215515, -0.3031876, -3.8988638, -21.210388, -0.32390642, -3.8988638, -21.105513, -0.29165316, -3.8775039, -21.126446, -0.2871678, -3.8988638, -21.105513, -0.29165316, -3.8775039, -21.12431, 0.20858967, -3.8775039, -21.126446, -0.2871678, -3.8988638, -21.105513, -0.29165316, -3.8988638, -21.10359, 0.2135024, -3.8775039, -21.12431, 0.20858967, -3.8988638, -21.10359, 0.2135024, -3.8775039, -21.224058, 0.23977482, -3.8775039, -21.12431, 0.20858967, -3.8988638, -21.10359, 0.2135024, -3.8988638, -21.219788, 0.26070714, -3.8775039, -21.224058, 0.23977482, -3.8775039, -21.084795, -0.2963524, -3.8988638, -21.10359, 0.2135024, -3.8988638, -21.105513, -0.29165316, -3.8775039, -21.084795, -0.2963524, -3.8775039, -21.082659, 0.21862864, -3.8988638, -21.10359, 0.2135024, -3.8775039, -21.082659, 0.21862864, -3.8988638, -21.219788, 0.26070714, -3.8988638, -21.10359, 0.2135024, -3.8775039, -21.082659, 0.21862864, -3.8775039, -21.215515, 0.2816397, -3.8988638, -21.219788, 0.26070714, -3.8775039, -21.224058, 0.23977482, -3.8561444, -21.34047, 0.21777427, -3.8561444, -21.219788, 0.26070714, -3.8775039, -21.224058, 0.23977482, -3.8775039, -21.320818, 0.20923042, -3.8561444, -21.34047, 0.21777427, -3.8561444, -21.219788, 0.26070714, -3.8775039, -21.36012, 0.22631812, -3.8775039, -21.215515, 0.2816397, -3.8561444, -21.219788, 0.26070714, -3.8561444, -21.34047, 0.21777427, -3.8775039, -21.36012, 0.22631812, -3.8775039, -21.215515, 0.2816397, -3.8988638, -21.34047, 0.21777427, -3.8988638, -21.219788, 0.26070714, -3.8775039, -21.215515, 0.2816397, -3.8775039, -21.36012, 0.22631812, -3.8988638, -21.34047, 0.21777427, -3.8988638, -21.219788, 0.26070714, -3.8775039, -21.320818, 0.20923042, -3.8775039, -21.224058, 0.23977482, -3.8988638, -21.219788, 0.26070714, -3.8988638, -21.34047, 0.21777427, -3.8775039, -21.320818, 0.20923042, -7.34631, -21.333633, 0.9091861, -7.3221745, -21.35008, 0.3158151, -7.3221745, -21.355635, 0.9190115, -7.34631, -21.333633, 0.9091861, -7.34631, -21.328295, 0.3260677, -7.3221745, -21.35008, 0.3158151, -7.34631, -21.328295, 0.3260677, -7.3221745, -21.208893, 0.2931739, -7.3221745, -21.35008, 0.3158151, -7.34631, -21.328295, 0.3260677, -7.34631, -21.21466, 0.3170967, -7.3221745, -21.208893, 0.2931739, -7.3221745, -21.355635, 0.9190115, -7.34631, -21.371868, 0.30534887, -7.34631, -21.377848, 0.92883694, -7.3221745, -21.355635, 0.9190115, -7.3221745, -21.35008, 0.3158151, -7.34631, -21.371868, 0.30534887, -7.3221745, -21.35008, 0.3158151, -7.34631, -21.20334, 0.2692511, -7.34631, -21.371868, 0.30534887, -7.3221745, -21.35008, 0.3158151, -7.3221745, -21.208893, 0.2931739, -7.34631, -21.20334, 0.2692511, -7.34631, -21.377848, 0.92883694, -7.3704467, -21.35008, 0.3158151, -7.3704467, -21.355635, 0.9190115, -7.34631, -21.377848, 0.92883694, -7.34631, -21.371868, 0.30534887, -7.3704467, -21.35008, 0.3158151, -7.34631, -21.371868, 0.30534887, -7.3704467, -21.208893, 0.2931739, -7.3704467, -21.35008, 0.3158151, -7.34631, -21.371868, 0.30534887, -7.34631, -21.20334, 0.2692511, -7.3704467, -21.208893, 0.2931739, -7.3704467, -21.355635, 0.9190115, -7.34631, -21.328295, 0.3260677, -7.34631, -21.333633, 0.9091861, -7.3704467, -21.355635, 0.9190115, -7.3704467, -21.35008, 0.3158151, -7.34631, -21.328295, 0.3260677, -7.3704467, -21.35008, 0.3158151, -7.34631, -21.21466, 0.3170967, -7.34631, -21.328295, 0.3260677, -7.3704467, -21.35008, 0.3158151, -7.3704467, -21.208893, 0.2931739, -7.34631, -21.21466, 0.3170967, -7.34631, -21.21466, 0.3170967, -7.3221745, -21.090775, 0.33033967, -7.3221745, -21.208893, 0.2931739, -7.34631, -21.21466, 0.3170967, -7.34631, -21.114271, 0.33567965, -7.3221745, -21.090775, 0.33033967, -7.34631, -21.114271, 0.33567965, -7.3221745, -21.088425, 0.91409886, -7.3221745, -21.090775, 0.33033967, -7.34631, -21.114271, 0.33567965, -7.34631, -21.111708, 0.90833163, -7.3221745, -21.088425, 0.91409886, -7.34631, -21.111708, 0.90833163, -7.3221745, -21.219574, 0.96877944, -7.3221745, -21.088425, 0.91409886, -7.34631, -21.111708, 0.90833163, -7.34631, -21.224485, 0.9444295, -7.3221745, -21.219574, 0.96877944, -7.3221745, -21.208893, 0.2931739, -7.34631, -21.06728, 0.32499957, -7.34631, -21.20334, 0.2692511, -7.3221745, -21.208893, 0.2931739, -7.3221745, -21.090775, 0.33033967, -7.34631, -21.06728, 0.32499957, -7.3221745, -21.090775, 0.33033967, -7.34631, -21.06493, 0.92007935, -7.34631, -21.06728, 0.32499957, -7.3221745, -21.090775, 0.33033967, -7.3221745, -21.088425, 0.91409886, -7.34631, -21.06493, 0.92007935, -7.3221745, -21.088425, 0.91409886, -7.34631, -21.214874, 0.99291587, -7.34631, -21.06493, 0.92007935, -7.3221745, -21.088425, 0.91409886, -7.3221745, -21.219574, 0.96877944, -7.34631, -21.214874, 0.99291587, -7.34631, -21.20334, 0.2692511, -7.3704467, -21.090775, 0.33033967, -7.3704467, -21.208893, 0.2931739, -7.34631, -21.20334, 0.2692511, -7.34631, -21.06728, 0.32499957, -7.3704467, -21.090775, 0.33033967, -7.3704467, -21.208893, 0.2931739, -7.34631, -21.114271, 0.33567965, -7.34631, -21.21466, 0.3170967, -7.3704467, -21.208893, 0.2931739, -7.3704467, -21.090775, 0.33033967, -7.34631, -21.114271, 0.33567965, -7.3704467, -21.090775, 0.33033967, -7.34631, -21.111708, 0.90833163, -7.34631, -21.114271, 0.33567965, -7.3704467, -21.090775, 0.33033967, -7.3704467, -21.088425, 0.91409886, -7.34631, -21.111708, 0.90833163, -7.3704467, -21.088425, 0.91409886, -7.34631, -21.224485, 0.9444295, -7.34631, -21.111708, 0.90833163, -7.3704467, -21.088425, 0.91409886, -7.3704467, -21.219574, 0.96877944, -7.34631, -21.224485, 0.9444295, -7.34631, -21.06728, 0.32499957, -7.3704467, -21.088425, 0.91409886, -7.3704467, -21.090775, 0.33033967, -7.34631, -21.06728, 0.32499957, -7.34631, -21.06493, 0.92007935, -7.3704467, -21.088425, 0.91409886, -7.34631, -21.06493, 0.92007935, -7.3704467, -21.219574, 0.96877944, -7.3704467, -21.088425, 0.91409886, -7.34631, -21.06493, 0.92007935, -7.34631, -21.214874, 0.99291587, -7.3704467, -21.219574, 0.96877944, -7.34631, -21.224485, 0.9444295, -7.3221745, -21.355635, 0.9190115, -7.3221745, -21.219574, 0.96877944, -7.34631, -21.224485, 0.9444295, -7.34631, -21.333633, 0.9091861, -7.3221745, -21.355635, 0.9190115, -7.3221745, -21.219574, 0.96877944, -7.34631, -21.377848, 0.92883694, -7.34631, -21.214874, 0.99291587, -7.3221745, -21.219574, 0.96877944, -7.3221745, -21.355635, 0.9190115, -7.34631, -21.377848, 0.92883694, -7.34631, -21.214874, 0.99291587, -7.3704467, -21.355635, 0.9190115, -7.3704467, -21.219574, 0.96877944, -7.34631, -21.214874, 0.99291587, -7.34631, -21.377848, 0.92883694, -7.3704467, -21.355635, 0.9190115, -7.3704467, -21.219574, 0.96877944, -7.34631, -21.333633, 0.9091861, -7.34631, -21.224485, 0.9444295, -7.3704467, -21.219574, 0.96877944, -7.3704467, -21.355635, 0.9190115, -7.34631, -21.333633, 0.9091861, -7.433244, -21.333633, 2.1138701, -7.4093213, -21.35008, 1.6759974, -7.4093213, -21.355633, 2.1211324, -7.433244, -21.333633, 2.1138701, -7.433244, -21.328293, 1.6839005, -7.4093213, -21.35008, 1.6759974, -7.4093213, -21.355633, 2.1211324, -7.433244, -21.371868, 1.6683079, -7.433244, -21.377848, 2.1283946, -7.4093213, -21.355633, 2.1211324, -7.4093213, -21.35008, 1.6759974, -7.433244, -21.371868, 1.6683079, -7.433244, -21.377848, 2.1283946, -7.457381, -21.35008, 1.6759974, -7.457381, -21.355633, 2.1211324, -7.433244, -21.377848, 2.1283946, -7.433244, -21.371868, 1.6683079, -7.457381, -21.35008, 1.6759974, -7.457381, -21.355633, 2.1211324, -7.433244, -21.328293, 1.6839005, -7.433244, -21.333633, 2.1138701, -7.457381, -21.355633, 2.1211324, -7.457381, -21.35008, 1.6759974, -7.433244, -21.328293, 1.6839005, -7.433244, -21.328293, 1.6839005, -7.4093213, -21.208893, 1.6595504, -7.4093213, -21.35008, 1.6759974, -7.433244, -21.328293, 1.6839005, -7.433244, -21.21466, 1.6770654, -7.4093213, -21.208893, 1.6595504, -7.4093213, -21.35008, 1.6759974, -7.433244, -21.203339, 1.6418219, -7.433244, -21.371868, 1.6683079, -7.4093213, -21.35008, 1.6759974, -7.4093213, -21.208893, 1.6595504, -7.433244, -21.203339, 1.6418219, -7.433244, -21.371868, 1.6683079, -7.457381, -21.208893, 1.6595504, -7.457381, -21.35008, 1.6759974, -7.433244, -21.371868, 1.6683079, -7.433244, -21.203339, 1.6418219, -7.457381, -21.208893, 1.6595504, -7.457381, -21.35008, 1.6759974, -7.433244, -21.21466, 1.6770654, -7.433244, -21.328293, 1.6839005, -7.457381, -21.35008, 1.6759974, -7.457381, -21.208893, 1.6595504, -7.433244, -21.21466, 1.6770654, -7.433244, -21.21466, 1.6770654, -7.4093213, -21.090775, 1.6868907, -7.4093213, -21.208893, 1.6595504, -7.433244, -21.21466, 1.6770654, -7.433244, -21.11427, 1.6907355, -7.4093213, -21.090775, 1.6868907, -7.4093213, -21.208893, 1.6595504, -7.433244, -21.067278, 1.6828324, -7.433244, -21.203339, 1.6418219, -7.4093213, -21.208893, 1.6595504, -7.4093213, -21.090775, 1.6868907, -7.433244, -21.067278, 1.6828324, -7.433244, -21.203339, 1.6418219, -7.457381, -21.090775, 1.6868907, -7.457381, -21.208893, 1.6595504, -7.433244, -21.203339, 1.6418219, -7.433244, -21.067278, 1.6828324, -7.457381, -21.090775, 1.6868907, -7.457381, -21.208893, 1.6595504, -7.433244, -21.11427, 1.6907355, -7.433244, -21.21466, 1.6770654, -7.457381, -21.208893, 1.6595504, -7.457381, -21.090775, 1.6868907, -7.433244, -21.11427, 1.6907355, -7.433244, -21.11427, 1.6907355, -7.4093213, -21.088425, 2.1175013, -7.4093213, -21.090775, 1.6868907, -7.433244, -21.11427, 1.6907355, -7.433244, -21.111706, 2.1132293, -7.4093213, -21.088425, 2.1175013, -7.433244, -21.111706, 2.1132293, -7.4093213, -21.219572, 2.157871, -7.4093213, -21.088425, 2.1175013, -7.433244, -21.111706, 2.1132293, -7.433244, -21.224485, 2.1399288, -7.4093213, -21.219572, 2.157871, -7.4093213, -21.090775, 1.6868907, -7.433244, -21.064928, 2.121773, -7.433244, -21.067278, 1.6828324, -7.4093213, -21.090775, 1.6868907, -7.4093213, -21.088425, 2.1175013, -7.433244, -21.064928, 2.121773, -7.4093213, -21.088425, 2.1175013, -7.433244, -21.214874, 2.1755996, -7.433244, -21.064928, 2.121773, -7.4093213, -21.088425, 2.1175013, -7.4093213, -21.219572, 2.157871, -7.433244, -21.214874, 2.1755996, -7.433244, -21.067278, 1.6828324, -7.457381, -21.088425, 2.1175013, -7.457381, -21.090775, 1.6868907, -7.433244, -21.067278, 1.6828324, -7.433244, -21.064928, 2.121773, -7.457381, -21.088425, 2.1175013, -7.433244, -21.064928, 2.121773, -7.457381, -21.219572, 2.157871, -7.457381, -21.088425, 2.1175013, -7.433244, -21.064928, 2.121773, -7.433244, -21.214874, 2.1755996, -7.457381, -21.219572, 2.157871, -7.457381, -21.090775, 1.6868907, -7.433244, -21.111706, 2.1132293, -7.433244, -21.11427, 1.6907355, -7.457381, -21.090775, 1.6868907, -7.457381, -21.088425, 2.1175013, -7.433244, -21.111706, 2.1132293, -7.457381, -21.088425, 2.1175013, -7.433244, -21.224485, 2.1399288, -7.433244, -21.111706, 2.1132293, -7.457381, -21.088425, 2.1175013, -7.457381, -21.219572, 2.157871, -7.433244, -21.224485, 2.1399288, -7.433244, -21.224485, 2.1399288, -7.4093213, -21.355633, 2.1211324, -7.4093213, -21.219572, 2.157871, -7.433244, -21.224485, 2.1399288, -7.433244, -21.333633, 2.1138701, -7.4093213, -21.355633, 2.1211324, -7.4093213, -21.219572, 2.157871, -7.433244, -21.377848, 2.1283946, -7.433244, -21.214874, 2.1755996, -7.4093213, -21.219572, 2.157871, -7.4093213, -21.355633, 2.1211324, -7.433244, -21.377848, 2.1283946, -7.433244, -21.214874, 2.1755996, -7.457381, -21.355633, 2.1211324, -7.457381, -21.219572, 2.157871, -7.433244, -21.214874, 2.1755996, -7.433244, -21.377848, 2.1283946, -7.457381, -21.355633, 2.1211324, -7.457381, -21.219572, 2.157871, -7.433244, -21.333633, 2.1138701, -7.433244, -21.224485, 2.1399288, -7.457381, -21.219572, 2.157871, -7.457381, -21.355633, 2.1211324, -7.433244, -21.333633, 2.1138701, -7.433244, -21.333633, 3.4082646, -7.4093213, -21.35008, 2.8360398, -7.4093213, -21.355633, 3.4176629, -7.433244, -21.333633, 3.4082646, -7.433244, -21.328293, 2.8460789, -7.4093213, -21.35008, 2.8360398, -7.433244, -21.328293, 2.8460789, -7.4093213, -21.208893, 2.8142529, -7.4093213, -21.35008, 2.8360398, -7.433244, -21.328293, 2.8460789, -7.433244, -21.21466, 2.837535, -7.4093213, -21.208893, 2.8142529, -7.4093213, -21.355633, 3.4176629, -7.433244, -21.371866, 2.8260007, -7.433244, -21.377848, 3.4272747, -7.4093213, -21.355633, 3.4176629, -7.4093213, -21.35008, 2.8360398, -7.433244, -21.371866, 2.8260007, -7.4093213, -21.35008, 2.8360398, -7.433244, -21.203339, 2.7911844, -7.433244, -21.371866, 2.8260007, -7.4093213, -21.35008, 2.8360398, -7.4093213, -21.208893, 2.8142529, -7.433244, -21.203339, 2.7911844, -7.433244, -21.377848, 3.4272747, -7.457381, -21.35008, 2.8360398, -7.457381, -21.355633, 3.4176629, -7.433244, -21.377848, 3.4272747, -7.433244, -21.371866, 2.8260007, -7.457381, -21.35008, 2.8360398, -7.433244, -21.371866, 2.8260007, -7.457381, -21.208893, 2.8142529, -7.457381, -21.35008, 2.8360398, -7.433244, -21.371866, 2.8260007, -7.433244, -21.203339, 2.7911844, -7.457381, -21.208893, 2.8142529, -7.457381, -21.355633, 3.4176629, -7.433244, -21.328293, 2.8460789, -7.433244, -21.333633, 3.4082646, -7.457381, -21.355633, 3.4176629, -7.457381, -21.35008, 2.8360398, -7.433244, -21.328293, 2.8460789, -7.457381, -21.35008, 2.8360398, -7.433244, -21.21466, 2.837535, -7.433244, -21.328293, 2.8460789, -7.457381, -21.35008, 2.8360398, -7.457381, -21.208893, 2.8142529, -7.433244, -21.21466, 2.837535, -7.433244, -21.21466, 2.837535, -7.4093213, -21.090775, 2.850137, -7.4093213, -21.208893, 2.8142529, -7.433244, -21.21466, 2.837535, -7.433244, -21.11427, 2.8552632, -7.4093213, -21.090775, 2.850137, -7.433244, -21.11427, 2.8552632, -7.4093213, -21.088425, 3.4129639, -7.4093213, -21.090775, 2.850137, -7.433244, -21.11427, 2.8552632, -7.433244, -21.111706, 3.4071965, -7.4093213, -21.088425, 3.4129639, -7.433244, -21.111706, 3.4071965, -7.4093213, -21.219572, 3.4655085, -7.4093213, -21.088425, 3.4129639, -7.433244, -21.111706, 3.4071965, -7.433244, -21.224485, 3.4422264, -7.4093213, -21.219572, 3.4655085, -7.4093213, -21.208893, 2.8142529, -7.433244, -21.067278, 2.8450108, -7.433244, -21.203339, 2.7911844, -7.4093213, -21.208893, 2.8142529, -7.4093213, -21.090775, 2.850137, -7.433244, -21.067278, 2.8450108, -7.4093213, -21.090775, 2.850137, -7.433244, -21.064928, 3.4187307, -7.433244, -21.067278, 2.8450108, -7.4093213, -21.090775, 2.850137, -7.4093213, -21.088425, 3.4129639, -7.433244, -21.064928, 3.4187307, -7.4093213, -21.088425, 3.4129639, -7.433244, -21.214872, 3.4887905, -7.433244, -21.064928, 3.4187307, -7.4093213, -21.088425, 3.4129639, -7.4093213, -21.219572, 3.4655085, -7.433244, -21.214872, 3.4887905, -7.433244, -21.203339, 2.7911844, -7.457381, -21.090775, 2.850137, -7.457381, -21.208893, 2.8142529, -7.433244, -21.203339, 2.7911844, -7.433244, -21.067278, 2.8450108, -7.457381, -21.090775, 2.850137, -7.457381, -21.208893, 2.8142529, -7.433244, -21.11427, 2.8552632, -7.433244, -21.21466, 2.837535, -7.457381, -21.208893, 2.8142529, -7.457381, -21.090775, 2.850137, -7.433244, -21.11427, 2.8552632, -7.457381, -21.090775, 2.850137, -7.433244, -21.111706, 3.4071965, -7.433244, -21.11427, 2.8552632, -7.457381, -21.090775, 2.850137, -7.457381, -21.088425, 3.4129639, -7.433244, -21.111706, 3.4071965, -7.457381, -21.088425, 3.4129639, -7.433244, -21.224485, 3.4422264, -7.433244, -21.111706, 3.4071965, -7.457381, -21.088425, 3.4129639, -7.457381, -21.219572, 3.4655085, -7.433244, -21.224485, 3.4422264, -7.433244, -21.067278, 2.8450108, -7.457381, -21.088425, 3.4129639, -7.457381, -21.090775, 2.850137, -7.433244, -21.067278, 2.8450108, -7.433244, -21.064928, 3.4187307, -7.457381, -21.088425, 3.4129639, -7.433244, -21.064928, 3.4187307, -7.457381, -21.219572, 3.4655085, -7.457381, -21.088425, 3.4129639, -7.433244, -21.064928, 3.4187307, -7.433244, -21.214872, 3.4887905, -7.457381, -21.219572, 3.4655085, -7.433244, -21.224485, 3.4422264, -7.4093213, -21.355633, 3.4176629, -7.4093213, -21.219572, 3.4655085, -7.433244, -21.224485, 3.4422264, -7.433244, -21.333633, 3.4082646, -7.4093213, -21.355633, 3.4176629, -7.4093213, -21.219572, 3.4655085, -7.433244, -21.377848, 3.4272747, -7.433244, -21.214872, 3.4887905, -7.4093213, -21.219572, 3.4655085, -7.4093213, -21.355633, 3.4176629, -7.433244, -21.377848, 3.4272747, -7.433244, -21.214872, 3.4887905, -7.457381, -21.355633, 3.4176629, -7.457381, -21.219572, 3.4655085, -7.433244, -21.214872, 3.4887905, -7.433244, -21.377848, 3.4272747, -7.457381, -21.355633, 3.4176629, -7.457381, -21.219572, 3.4655085, -7.433244, -21.333633, 3.4082646, -7.433244, -21.224485, 3.4422264, -7.457381, -21.219572, 3.4655085, -7.457381, -21.355633, 3.4176629, -7.433244, -21.333633, 3.4082646, -3.7589583, -21.320816, 4.036238, -3.7375984, -21.335342, 3.5225387, -3.7375984, -21.340467, 4.044782, -3.7589583, -21.320816, 4.036238, -3.7589583, -21.316118, 3.5317235, -3.7375984, -21.335342, 3.5225387, -3.7589583, -21.316118, 3.5317235, -3.7375984, -21.210388, 3.5031013, -3.7375984, -21.335342, 3.5225387, -3.7589583, -21.316118, 3.5317235, -3.7589583, -21.215513, 3.5238204, -3.7375984, -21.210388, 3.5031013, -3.7375984, -21.340467, 4.044782, -3.7589583, -21.354778, 3.5137813, -3.7589583, -21.360119, 4.0533257, -3.7375984, -21.340467, 4.044782, -3.7375984, -21.335342, 3.5225387, -3.7589583, -21.354778, 3.5137813, -3.7375984, -21.335342, 3.5225387, -3.7589583, -21.205261, 3.4823828, -3.7589583, -21.354778, 3.5137813, -3.7375984, -21.335342, 3.5225387, -3.7375984, -21.210388, 3.5031013, -3.7589583, -21.205261, 3.4823828, -3.7589583, -21.360119, 4.0533257, -3.7803178, -21.335342, 3.5225387, -3.7803178, -21.340467, 4.044782, -3.7589583, -21.360119, 4.0533257, -3.7589583, -21.354778, 3.5137813, -3.7803178, -21.335342, 3.5225387, -3.7589583, -21.354778, 3.5137813, -3.7803178, -21.210388, 3.5031013, -3.7803178, -21.335342, 3.5225387, -3.7589583, -21.354778, 3.5137813, -3.7589583, -21.205261, 3.4823828, -3.7803178, -21.210388, 3.5031013, -3.7803178, -21.340467, 4.044782, -3.7589583, -21.316118, 3.5317235, -3.7589583, -21.320816, 4.036238, -3.7803178, -21.340467, 4.044782, -3.7803178, -21.335342, 3.5225387, -3.7589583, -21.316118, 3.5317235, -3.7803178, -21.335342, 3.5225387, -3.7589583, -21.215513, 3.5238204, -3.7589583, -21.316118, 3.5317235, -3.7803178, -21.335342, 3.5225387, -3.7803178, -21.210388, 3.5031013, -3.7589583, -21.215513, 3.5238204, -3.7589583, -21.215513, 3.5238204, -3.7375984, -21.105513, 3.5353546, -3.7375984, -21.210388, 3.5031013, -3.7589583, -21.215513, 3.5238204, -3.7589583, -21.126444, 3.5398402, -3.7375984, -21.105513, 3.5353546, -3.7589583, -21.126444, 3.5398402, -3.7375984, -21.10359, 4.04051, -3.7375984, -21.105513, 3.5353546, -3.7589583, -21.126444, 3.5398402, -3.7589583, -21.124308, 4.0355973, -3.7375984, -21.10359, 4.04051, -3.7589583, -21.124308, 4.0355973, -3.7375984, -21.219786, 4.087715, -3.7375984, -21.10359, 4.04051, -3.7589583, -21.124308, 4.0355973, -3.7589583, -21.224058, 4.0667825, -3.7375984, -21.219786, 4.087715, -3.7375984, -21.210388, 3.5031013, -3.7589583, -21.084793, 3.5306554, -3.7589583, -21.205261, 3.4823828, -3.7375984, -21.210388, 3.5031013, -3.7375984, -21.105513, 3.5353546, -3.7589583, -21.084793, 3.5306554, -3.7375984, -21.105513, 3.5353546, -3.7589583, -21.082657, 4.045636, -3.7589583, -21.084793, 3.5306554, -3.7375984, -21.105513, 3.5353546, -3.7375984, -21.10359, 4.04051, -3.7589583, -21.082657, 4.045636, -3.7375984, -21.10359, 4.04051, -3.7589583, -21.215513, 4.1086473, -3.7589583, -21.082657, 4.045636, -3.7375984, -21.10359, 4.04051, -3.7375984, -21.219786, 4.087715, -3.7589583, -21.215513, 4.1086473, -3.7589583, -21.205261, 3.4823828, -3.7803178, -21.105513, 3.5353546, -3.7803178, -21.210388, 3.5031013, -3.7589583, -21.205261, 3.4823828, -3.7589583, -21.084793, 3.5306554, -3.7803178, -21.105513, 3.5353546, -3.7803178, -21.210388, 3.5031013, -3.7589583, -21.126444, 3.5398402, -3.7589583, -21.215513, 3.5238204, -3.7803178, -21.210388, 3.5031013, -3.7803178, -21.105513, 3.5353546, -3.7589583, -21.126444, 3.5398402, -3.7803178, -21.105513, 3.5353546, -3.7589583, -21.124308, 4.0355973, -3.7589583, -21.126444, 3.5398402, -3.7803178, -21.105513, 3.5353546, -3.7803178, -21.10359, 4.04051, -3.7589583, -21.124308, 4.0355973, -3.7803178, -21.10359, 4.04051, -3.7589583, -21.224058, 4.0667825, -3.7589583, -21.124308, 4.0355973, -3.7803178, -21.10359, 4.04051, -3.7803178, -21.219786, 4.087715, -3.7589583, -21.224058, 4.0667825, -3.7589583, -21.084793, 3.5306554, -3.7803178, -21.10359, 4.04051, -3.7803178, -21.105513, 3.5353546, -3.7589583, -21.084793, 3.5306554, -3.7589583, -21.082657, 4.045636, -3.7803178, -21.10359, 4.04051, -3.7589583, -21.082657, 4.045636, -3.7803178, -21.219786, 4.087715, -3.7803178, -21.10359, 4.04051, -3.7589583, -21.082657, 4.045636, -3.7589583, -21.215513, 4.1086473, -3.7803178, -21.219786, 4.087715, -3.7589583, -21.224058, 4.0667825, -3.7375984, -21.340467, 4.044782, -3.7375984, -21.219786, 4.087715, -3.7589583, -21.224058, 4.0667825, -3.7589583, -21.320816, 4.036238, -3.7375984, -21.340467, 4.044782, -3.7375984, -21.219786, 4.087715, -3.7589583, -21.360119, 4.0533257, -3.7589583, -21.215513, 4.1086473, -3.7375984, -21.219786, 4.087715, -3.7375984, -21.340467, 4.044782, -3.7589583, -21.360119, 4.0533257, -3.7589583, -21.215513, 4.1086473, -3.7803178, -21.340467, 4.044782, -3.7803178, -21.219786, 4.087715, -3.7589583, -21.215513, 4.1086473, -3.7589583, -21.360119, 4.0533257, -3.7803178, -21.340467, 4.044782, -3.7803178, -21.219786, 4.087715, -3.7589583, -21.320816, 4.036238, -3.7589583, -21.224058, 4.0667825, -3.7803178, -21.219786, 4.087715, -3.7803178, -21.340467, 4.044782, -3.7589583, -21.320816, 4.036238, -7.491342, -21.333633, 1.4549251, -7.4672065, -21.35008, 1.0367032, -7.4672065, -21.355635, 1.4619738, -7.491342, -21.333633, 1.4549251, -7.491342, -21.328293, 1.0439655, -7.4672065, -21.35008, 1.0367032, -7.4672065, -21.355635, 1.4619738, -7.491342, -21.371868, 1.0292273, -7.491342, -21.377848, 1.4688089, -7.4672065, -21.355635, 1.4619738, -7.4672065, -21.35008, 1.0367032, -7.491342, -21.371868, 1.0292273, -7.491342, -21.377848, 1.4688089, -7.5154786, -21.35008, 1.0367032, -7.5154786, -21.355635, 1.4619738, -7.491342, -21.377848, 1.4688089, -7.491342, -21.371868, 1.0292273, -7.5154786, -21.35008, 1.0367032, -7.5154786, -21.355635, 1.4619738, -7.491342, -21.328293, 1.0439655, -7.491342, -21.333633, 1.4549251, -7.5154786, -21.355635, 1.4619738, -7.5154786, -21.35008, 1.0367032, -7.491342, -21.328293, 1.0439655, -7.491342, -21.328293, 1.0439655, -7.4672065, -21.208893, 1.0206835, -7.4672065, -21.35008, 1.0367032, -7.491342, -21.328293, 1.0439655, -7.491342, -21.21466, 1.0375576, -7.4672065, -21.208893, 1.0206835, -7.4672065, -21.35008, 1.0367032, -7.491342, -21.20334, 1.0038093, -7.491342, -21.371868, 1.0292273, -7.4672065, -21.35008, 1.0367032, -7.4672065, -21.208893, 1.0206835, -7.491342, -21.20334, 1.0038093, -7.491342, -21.371868, 1.0292273, -7.5154786, -21.208893, 1.0206835, -7.5154786, -21.35008, 1.0367032, -7.491342, -21.371868, 1.0292273, -7.491342, -21.20334, 1.0038093, -7.5154786, -21.208893, 1.0206835, -7.5154786, -21.35008, 1.0367032, -7.491342, -21.21466, 1.0375576, -7.491342, -21.328293, 1.0439655, -7.5154786, -21.35008, 1.0367032, -7.5154786, -21.208893, 1.0206835, -7.491342, -21.21466, 1.0375576, -7.491342, -21.21466, 1.0375576, -7.4672065, -21.090775, 1.0469557, -7.4672065, -21.208893, 1.0206835, -7.491342, -21.21466, 1.0375576, -7.491342, -21.11427, 1.0505869, -7.4672065, -21.090775, 1.0469557, -7.4672065, -21.208893, 1.0206835, -7.491342, -21.067278, 1.0431108, -7.491342, -21.20334, 1.0038093, -7.4672065, -21.208893, 1.0206835, -7.4672065, -21.090775, 1.0469557, -7.491342, -21.067278, 1.0431108, -7.491342, -21.20334, 1.0038093, -7.5154786, -21.090775, 1.0469557, -7.5154786, -21.208893, 1.0206835, -7.491342, -21.20334, 1.0038093, -7.491342, -21.067278, 1.0431108, -7.5154786, -21.090775, 1.0469557, -7.5154786, -21.208893, 1.0206835, -7.491342, -21.11427, 1.0505869, -7.491342, -21.21466, 1.0375576, -7.5154786, -21.208893, 1.0206835, -7.5154786, -21.090775, 1.0469557, -7.491342, -21.11427, 1.0505869, -7.491342, -21.11427, 1.0505869, -7.4672065, -21.088425, 1.4585562, -7.4672065, -21.090775, 1.0469557, -7.491342, -21.11427, 1.0505869, -7.491342, -21.111706, 1.4542842, -7.4672065, -21.088425, 1.4585562, -7.4672065, -21.090775, 1.0469557, -7.491342, -21.06493, 1.4626144, -7.491342, -21.067278, 1.0431108, -7.4672065, -21.090775, 1.0469557, -7.4672065, -21.088425, 1.4585562, -7.491342, -21.06493, 1.4626144, -7.491342, -21.067278, 1.0431108, -7.5154786, -21.088425, 1.4585562, -7.5154786, -21.090775, 1.0469557, -7.491342, -21.067278, 1.0431108, -7.491342, -21.06493, 1.4626144, -7.5154786, -21.088425, 1.4585562, -7.5154786, -21.090775, 1.0469557, -7.491342, -21.111706, 1.4542842, -7.491342, -21.11427, 1.0505869, -7.5154786, -21.090775, 1.0469557, -7.5154786, -21.088425, 1.4585562, -7.491342, -21.111706, 1.4542842, -7.491342, -21.111706, 1.4542842, -7.4672065, -21.219572, 1.4970036, -7.4672065, -21.088425, 1.4585562, -7.491342, -21.111706, 1.4542842, -7.491342, -21.224485, 1.4799159, -7.4672065, -21.219572, 1.4970036, -7.4672065, -21.088425, 1.4585562, -7.491342, -21.214874, 1.5138776, -7.491342, -21.06493, 1.4626144, -7.4672065, -21.088425, 1.4585562, -7.4672065, -21.219572, 1.4970036, -7.491342, -21.214874, 1.5138776, -7.491342, -21.06493, 1.4626144, -7.5154786, -21.219572, 1.4970036, -7.5154786, -21.088425, 1.4585562, -7.491342, -21.06493, 1.4626144, -7.491342, -21.214874, 1.5138776, -7.5154786, -21.219572, 1.4970036, -7.5154786, -21.088425, 1.4585562, -7.491342, -21.224485, 1.4799159, -7.491342, -21.111706, 1.4542842, -7.5154786, -21.088425, 1.4585562, -7.5154786, -21.219572, 1.4970036, -7.491342, -21.224485, 1.4799159, -7.491342, -21.224485, 1.4799159, -7.4672065, -21.355635, 1.4619738, -7.4672065, -21.219572, 1.4970036, -7.491342, -21.224485, 1.4799159, -7.491342, -21.333633, 1.4549251, -7.4672065, -21.355635, 1.4619738, -7.4672065, -21.219572, 1.4970036, -7.491342, -21.377848, 1.4688089, -7.491342, -21.214874, 1.5138776, -7.4672065, -21.219572, 1.4970036, -7.4672065, -21.355635, 1.4619738, -7.491342, -21.377848, 1.4688089, -7.491342, -21.214874, 1.5138776, -7.5154786, -21.355635, 1.4619738, -7.5154786, -21.219572, 1.4970036, -7.491342, -21.214874, 1.5138776, -7.491342, -21.377848, 1.4688089, -7.5154786, -21.355635, 1.4619738, -7.5154786, -21.219572, 1.4970036, -7.491342, -21.333633, 1.4549251, -7.491342, -21.224485, 1.4799159, -7.5154786, -21.219572, 1.4970036, -7.5154786, -21.355635, 1.4619738, -7.491342, -21.333633, 1.4549251, -7.4881387, -21.333633, 2.66858, -7.464002, -21.35008, 2.2503583, -7.464002, -21.355633, 2.6756287, -7.4881387, -21.333633, 2.66858, -7.4881387, -21.328293, 2.2576206, -7.464002, -21.35008, 2.2503583, -7.464002, -21.355633, 2.6756287, -7.4881387, -21.371866, 2.2428825, -7.4881387, -21.377848, 2.682464, -7.464002, -21.355633, 2.6756287, -7.464002, -21.35008, 2.2503583, -7.4881387, -21.371866, 2.2428825, -7.4881387, -21.377848, 2.682464, -7.512275, -21.35008, 2.2503583, -7.512275, -21.355633, 2.6756287, -7.4881387, -21.377848, 2.682464, -7.4881387, -21.371866, 2.2428825, -7.512275, -21.35008, 2.2503583, -7.512275, -21.355633, 2.6756287, -7.4881387, -21.328293, 2.2576206, -7.4881387, -21.333633, 2.66858, -7.512275, -21.355633, 2.6756287, -7.512275, -21.35008, 2.2503583, -7.4881387, -21.328293, 2.2576206, -7.4881387, -21.328293, 2.2576206, -7.464002, -21.208893, 2.2343385, -7.464002, -21.35008, 2.2503583, -7.4881387, -21.328293, 2.2576206, -7.4881387, -21.21466, 2.2512126, -7.464002, -21.208893, 2.2343385, -7.464002, -21.35008, 2.2503583, -7.4881387, -21.203339, 2.2174644, -7.4881387, -21.371866, 2.2428825, -7.464002, -21.35008, 2.2503583, -7.464002, -21.208893, 2.2343385, -7.4881387, -21.203339, 2.2174644, -7.4881387, -21.371866, 2.2428825, -7.512275, -21.208893, 2.2343385, -7.512275, -21.35008, 2.2503583, -7.4881387, -21.371866, 2.2428825, -7.4881387, -21.203339, 2.2174644, -7.512275, -21.208893, 2.2343385, -7.512275, -21.35008, 2.2503583, -7.4881387, -21.21466, 2.2512126, -7.4881387, -21.328293, 2.2576206, -7.512275, -21.35008, 2.2503583, -7.512275, -21.208893, 2.2343385, -7.4881387, -21.21466, 2.2512126, -7.4881387, -21.21466, 2.2512126, -7.464002, -21.090775, 2.2606108, -7.464002, -21.208893, 2.2343385, -7.4881387, -21.21466, 2.2512126, -7.4881387, -21.11427, 2.264242, -7.464002, -21.090775, 2.2606108, -7.464002, -21.208893, 2.2343385, -7.4881387, -21.067278, 2.256766, -7.4881387, -21.203339, 2.2174644, -7.464002, -21.208893, 2.2343385, -7.464002, -21.090775, 2.2606108, -7.4881387, -21.067278, 2.256766, -7.4881387, -21.203339, 2.2174644, -7.512275, -21.090775, 2.2606108, -7.512275, -21.208893, 2.2343385, -7.4881387, -21.203339, 2.2174644, -7.4881387, -21.067278, 2.256766, -7.512275, -21.090775, 2.2606108, -7.512275, -21.208893, 2.2343385, -7.4881387, -21.11427, 2.264242, -7.4881387, -21.21466, 2.2512126, -7.512275, -21.208893, 2.2343385, -7.512275, -21.090775, 2.2606108, -7.4881387, -21.11427, 2.264242, -7.4881387, -21.11427, 2.264242, -7.464002, -21.088425, 2.6722112, -7.464002, -21.090775, 2.2606108, -7.4881387, -21.11427, 2.264242, -7.4881387, -21.111706, 2.6679392, -7.464002, -21.088425, 2.6722112, -7.464002, -21.090775, 2.2606108, -7.4881387, -21.064928, 2.6762695, -7.4881387, -21.067278, 2.256766, -7.464002, -21.090775, 2.2606108, -7.464002, -21.088425, 2.6722112, -7.4881387, -21.064928, 2.6762695, -7.4881387, -21.067278, 2.256766, -7.512275, -21.088425, 2.6722112, -7.512275, -21.090775, 2.2606108, -7.4881387, -21.067278, 2.256766, -7.4881387, -21.064928, 2.6762695, -7.512275, -21.088425, 2.6722112, -7.512275, -21.090775, 2.2606108, -7.4881387, -21.111706, 2.6679392, -7.4881387, -21.11427, 2.264242, -7.512275, -21.090775, 2.2606108, -7.512275, -21.088425, 2.6722112, -7.4881387, -21.111706, 2.6679392, -7.4881387, -21.111706, 2.6679392, -7.464002, -21.219572, 2.7106586, -7.464002, -21.088425, 2.6722112, -7.4881387, -21.111706, 2.6679392, -7.4881387, -21.224485, 2.6935709, -7.464002, -21.219572, 2.7106586, -7.464002, -21.088425, 2.6722112, -7.4881387, -21.214874, 2.7275326, -7.4881387, -21.064928, 2.6762695, -7.464002, -21.088425, 2.6722112, -7.464002, -21.219572, 2.7106586, -7.4881387, -21.214874, 2.7275326, -7.4881387, -21.064928, 2.6762695, -7.512275, -21.219572, 2.7106586, -7.512275, -21.088425, 2.6722112, -7.4881387, -21.064928, 2.6762695, -7.4881387, -21.214874, 2.7275326, -7.512275, -21.219572, 2.7106586, -7.512275, -21.088425, 2.6722112, -7.4881387, -21.224485, 2.6935709, -7.4881387, -21.111706, 2.6679392, -7.512275, -21.088425, 2.6722112, -7.512275, -21.219572, 2.7106586, -7.4881387, -21.224485, 2.6935709, -7.4881387, -21.224485, 2.6935709, -7.464002, -21.355633, 2.6756287, -7.464002, -21.219572, 2.7106586, -7.4881387, -21.224485, 2.6935709, -7.4881387, -21.333633, 2.66858, -7.464002, -21.355633, 2.6756287, -7.464002, -21.219572, 2.7106586, -7.4881387, -21.377848, 2.682464, -7.4881387, -21.214874, 2.7275326, -7.464002, -21.219572, 2.7106586, -7.464002, -21.355633, 2.6756287, -7.4881387, -21.377848, 2.682464, -7.4881387, -21.214874, 2.7275326, -7.512275, -21.355633, 2.6756287, -7.512275, -21.219572, 2.7106586, -7.4881387, -21.214874, 2.7275326, -7.4881387, -21.377848, 2.682464, -7.512275, -21.355633, 2.6756287, -7.512275, -21.219572, 2.7106586, -7.4881387, -21.333633, 2.66858, -7.4881387, -21.224485, 2.6935709, -7.512275, -21.219572, 2.7106586, -7.512275, -21.355633, 2.6756287, -7.4881387, -21.333633, 2.66858, -3.7905703, -21.320818, 1.4850421, -3.7692103, -21.335342, 0.9713427, -3.7692103, -21.34047, 1.4933724, -3.7905703, -21.320818, 1.4850421, -3.7905703, -21.316118, 0.98031366, -3.7692103, -21.335342, 0.9713427, -3.7905703, -21.316118, 0.98031366, -3.7692103, -21.210388, 0.95169187, -3.7692103, -21.335342, 0.9713427, -3.7905703, -21.316118, 0.98031366, -3.7905703, -21.215515, 0.9724107, -3.7692103, -21.210388, 0.95169187, -3.7692103, -21.34047, 1.4933724, -3.7905703, -21.35478, 0.9623716, -3.7905703, -21.360119, 1.5019164, -3.7692103, -21.34047, 1.4933724, -3.7692103, -21.335342, 0.9713427, -3.7905703, -21.35478, 0.9623716, -3.7692103, -21.335342, 0.9713427, -3.7905703, -21.205261, 0.93097293, -3.7905703, -21.35478, 0.9623716, -3.7692103, -21.335342, 0.9713427, -3.7692103, -21.210388, 0.95169187, -3.7905703, -21.205261, 0.93097293, -3.7905703, -21.360119, 1.5019164, -3.8119302, -21.335342, 0.9713427, -3.8119302, -21.34047, 1.4933724, -3.7905703, -21.360119, 1.5019164, -3.7905703, -21.35478, 0.9623716, -3.8119302, -21.335342, 0.9713427, -3.7905703, -21.35478, 0.9623716, -3.8119302, -21.210388, 0.95169187, -3.8119302, -21.335342, 0.9713427, -3.7905703, -21.35478, 0.9623716, -3.7905703, -21.205261, 0.93097293, -3.8119302, -21.210388, 0.95169187, -3.8119302, -21.34047, 1.4933724, -3.7905703, -21.316118, 0.98031366, -3.7905703, -21.320818, 1.4850421, -3.8119302, -21.34047, 1.4933724, -3.8119302, -21.335342, 0.9713427, -3.7905703, -21.316118, 0.98031366, -3.8119302, -21.335342, 0.9713427, -3.7905703, -21.215515, 0.9724107, -3.7905703, -21.316118, 0.98031366, -3.8119302, -21.335342, 0.9713427, -3.8119302, -21.210388, 0.95169187, -3.7905703, -21.215515, 0.9724107, -3.7905703, -21.215515, 0.9724107, -3.7692103, -21.105513, 0.9839448, -3.7692103, -21.210388, 0.95169187, -3.7905703, -21.215515, 0.9724107, -3.7905703, -21.126446, 0.9884304, -3.7692103, -21.105513, 0.9839448, -3.7905703, -21.126446, 0.9884304, -3.7692103, -21.10359, 1.489314, -3.7692103, -21.105513, 0.9839448, -3.7905703, -21.126446, 0.9884304, -3.7905703, -21.12431, 1.4841876, -3.7692103, -21.10359, 1.489314, -3.7905703, -21.12431, 1.4841876, -3.7692103, -21.219786, 1.5363052, -3.7692103, -21.10359, 1.489314, -3.7905703, -21.12431, 1.4841876, -3.7905703, -21.224058, 1.5153728, -3.7692103, -21.219786, 1.5363052, -3.7692103, -21.210388, 0.95169187, -3.7905703, -21.084793, 0.9792458, -3.7905703, -21.205261, 0.93097293, -3.7692103, -21.210388, 0.95169187, -3.7692103, -21.105513, 0.9839448, -3.7905703, -21.084793, 0.9792458, -3.7692103, -21.105513, 0.9839448, -3.7905703, -21.082657, 1.4942267, -3.7905703, -21.084793, 0.9792458, -3.7692103, -21.105513, 0.9839448, -3.7692103, -21.10359, 1.489314, -3.7905703, -21.082657, 1.4942267, -3.7692103, -21.10359, 1.489314, -3.7905703, -21.215515, 1.5574512, -3.7905703, -21.082657, 1.4942267, -3.7692103, -21.10359, 1.489314, -3.7692103, -21.219786, 1.5363052, -3.7905703, -21.215515, 1.5574512, -3.7905703, -21.205261, 0.93097293, -3.8119302, -21.105513, 0.9839448, -3.8119302, -21.210388, 0.95169187, -3.7905703, -21.205261, 0.93097293, -3.7905703, -21.084793, 0.9792458, -3.8119302, -21.105513, 0.9839448, -3.8119302, -21.210388, 0.95169187, -3.7905703, -21.126446, 0.9884304, -3.7905703, -21.215515, 0.9724107, -3.8119302, -21.210388, 0.95169187, -3.8119302, -21.105513, 0.9839448, -3.7905703, -21.126446, 0.9884304, -3.8119302, -21.105513, 0.9839448, -3.7905703, -21.12431, 1.4841876, -3.7905703, -21.126446, 0.9884304, -3.8119302, -21.105513, 0.9839448, -3.8119302, -21.10359, 1.489314, -3.7905703, -21.12431, 1.4841876, -3.8119302, -21.10359, 1.489314, -3.7905703, -21.224058, 1.5153728, -3.7905703, -21.12431, 1.4841876, -3.8119302, -21.10359, 1.489314, -3.8119302, -21.219786, 1.5363052, -3.7905703, -21.224058, 1.5153728, -3.7905703, -21.084793, 0.9792458, -3.8119302, -21.10359, 1.489314, -3.8119302, -21.105513, 0.9839448, -3.7905703, -21.084793, 0.9792458, -3.7905703, -21.082657, 1.4942267, -3.8119302, -21.10359, 1.489314, -3.7905703, -21.082657, 1.4942267, -3.8119302, -21.219786, 1.5363052, -3.8119302, -21.10359, 1.489314, -3.7905703, -21.082657, 1.4942267, -3.7905703, -21.215515, 1.5574512, -3.8119302, -21.219786, 1.5363052, -3.7905703, -21.224058, 1.5153728, -3.7692103, -21.34047, 1.4933724, -3.7692103, -21.219786, 1.5363052, -3.7905703, -21.224058, 1.5153728, -3.7905703, -21.320818, 1.4850421, -3.7692103, -21.34047, 1.4933724, -3.7692103, -21.219786, 1.5363052, -3.7905703, -21.360119, 1.5019164, -3.7905703, -21.215515, 1.5574512, -3.7692103, -21.219786, 1.5363052, -3.7692103, -21.34047, 1.4933724, -3.7905703, -21.360119, 1.5019164, -3.7905703, -21.215515, 1.5574512, -3.8119302, -21.34047, 1.4933724, -3.8119302, -21.219786, 1.5363052, -3.7905703, -21.215515, 1.5574512, -3.7905703, -21.360119, 1.5019164, -3.8119302, -21.34047, 1.4933724, -3.8119302, -21.219786, 1.5363052, -3.7905703, -21.320818, 1.4850421, -3.7905703, -21.224058, 1.5153728, -3.8119302, -21.219786, 1.5363052, -3.8119302, -21.34047, 1.4933724, -3.7905703, -21.320818, 1.4850421, -3.8458915, -21.320818, 2.7324455, -3.824532, -21.335342, 2.218746, -3.824532, -21.340467, 2.7409892, -3.8458915, -21.320818, 2.7324455, -3.8458915, -21.316118, 2.227717, -3.824532, -21.335342, 2.218746, -3.8458915, -21.316118, 2.227717, -3.824532, -21.210388, 2.199095, -3.824532, -21.335342, 2.218746, -3.8458915, -21.316118, 2.227717, -3.8458915, -21.215515, 2.2200274, -3.824532, -21.210388, 2.199095, -3.824532, -21.340467, 2.7409892, -3.8458915, -21.354778, 2.209775, -3.8458915, -21.360119, 2.7495332, -3.824532, -21.340467, 2.7409892, -3.824532, -21.335342, 2.218746, -3.8458915, -21.354778, 2.209775, -3.824532, -21.335342, 2.218746, -3.8458915, -21.205261, 2.1783762, -3.8458915, -21.354778, 2.209775, -3.824532, -21.335342, 2.218746, -3.824532, -21.210388, 2.199095, -3.8458915, -21.205261, 2.1783762, -3.8458915, -21.360119, 2.7495332, -3.8672514, -21.335342, 2.218746, -3.8672514, -21.340467, 2.7409892, -3.8458915, -21.360119, 2.7495332, -3.8458915, -21.354778, 2.209775, -3.8672514, -21.335342, 2.218746, -3.8458915, -21.354778, 2.209775, -3.8672514, -21.210388, 2.199095, -3.8672514, -21.335342, 2.218746, -3.8458915, -21.354778, 2.209775, -3.8458915, -21.205261, 2.1783762, -3.8672514, -21.210388, 2.199095, -3.8672514, -21.340467, 2.7409892, -3.8458915, -21.316118, 2.227717, -3.8458915, -21.320818, 2.7324455, -3.8672514, -21.340467, 2.7409892, -3.8672514, -21.335342, 2.218746, -3.8458915, -21.316118, 2.227717, -3.8672514, -21.335342, 2.218746, -3.8458915, -21.215515, 2.2200274, -3.8458915, -21.316118, 2.227717, -3.8672514, -21.335342, 2.218746, -3.8672514, -21.210388, 2.199095, -3.8458915, -21.215515, 2.2200274, -3.8458915, -21.215515, 2.2200274, -3.824532, -21.105513, 2.231348, -3.824532, -21.210388, 2.199095, -3.8458915, -21.215515, 2.2200274, -3.8458915, -21.126444, 2.2360473, -3.824532, -21.105513, 2.231348, -3.8458915, -21.126444, 2.2360473, -3.824532, -21.10359, 2.7367172, -3.824532, -21.105513, 2.231348, -3.8458915, -21.126444, 2.2360473, -3.8458915, -21.124308, 2.731591, -3.824532, -21.10359, 2.7367172, -3.8458915, -21.124308, 2.731591, -3.824532, -21.219786, 2.7839222, -3.824532, -21.10359, 2.7367172, -3.8458915, -21.124308, 2.731591, -3.8458915, -21.224058, 2.7629898, -3.824532, -21.219786, 2.7839222, -3.824532, -21.210388, 2.199095, -3.8458915, -21.084793, 2.2268624, -3.8458915, -21.205261, 2.1783762, -3.824532, -21.210388, 2.199095, -3.824532, -21.105513, 2.231348, -3.8458915, -21.084793, 2.2268624, -3.824532, -21.105513, 2.231348, -3.8458915, -21.082657, 2.7418437, -3.8458915, -21.084793, 2.2268624, -3.824532, -21.105513, 2.231348, -3.824532, -21.10359, 2.7367172, -3.8458915, -21.082657, 2.7418437, -3.824532, -21.10359, 2.7367172, -3.8458915, -21.215513, 2.8048546, -3.8458915, -21.082657, 2.7418437, -3.824532, -21.10359, 2.7367172, -3.824532, -21.219786, 2.7839222, -3.8458915, -21.215513, 2.8048546, -3.8458915, -21.205261, 2.1783762, -3.8672514, -21.105513, 2.231348, -3.8672514, -21.210388, 2.199095, -3.8458915, -21.205261, 2.1783762, -3.8458915, -21.084793, 2.2268624, -3.8672514, -21.105513, 2.231348, -3.8672514, -21.210388, 2.199095, -3.8458915, -21.126444, 2.2360473, -3.8458915, -21.215515, 2.2200274, -3.8672514, -21.210388, 2.199095, -3.8672514, -21.105513, 2.231348, -3.8458915, -21.126444, 2.2360473, -3.8672514, -21.105513, 2.231348, -3.8458915, -21.124308, 2.731591, -3.8458915, -21.126444, 2.2360473, -3.8672514, -21.105513, 2.231348, -3.8672514, -21.10359, 2.7367172, -3.8458915, -21.124308, 2.731591, -3.8672514, -21.10359, 2.7367172, -3.8458915, -21.224058, 2.7629898, -3.8458915, -21.124308, 2.731591, -3.8672514, -21.10359, 2.7367172, -3.8672514, -21.219786, 2.7839222, -3.8458915, -21.224058, 2.7629898, -3.8458915, -21.084793, 2.2268624, -3.8672514, -21.10359, 2.7367172, -3.8672514, -21.105513, 2.231348, -3.8458915, -21.084793, 2.2268624, -3.8458915, -21.082657, 2.7418437, -3.8672514, -21.10359, 2.7367172, -3.8458915, -21.082657, 2.7418437, -3.8672514, -21.219786, 2.7839222, -3.8672514, -21.10359, 2.7367172, -3.8458915, -21.082657, 2.7418437, -3.8458915, -21.215513, 2.8048546, -3.8672514, -21.219786, 2.7839222, -3.8458915, -21.224058, 2.7629898, -3.824532, -21.340467, 2.7409892, -3.824532, -21.219786, 2.7839222, -3.8458915, -21.224058, 2.7629898, -3.8458915, -21.320818, 2.7324455, -3.824532, -21.340467, 2.7409892, -3.824532, -21.219786, 2.7839222, -3.8458915, -21.360119, 2.7495332, -3.8458915, -21.215513, 2.8048546, -3.824532, -21.219786, 2.7839222, -3.824532, -21.340467, 2.7409892, -3.8458915, -21.360119, 2.7495332, -3.8458915, -21.215513, 2.8048546, -3.8672514, -21.340467, 2.7409892, -3.8672514, -21.219786, 2.7839222, -3.8458915, -21.215513, 2.8048546, -3.8458915, -21.360119, 2.7495332, -3.8672514, -21.340467, 2.7409892, -3.8672514, -21.219786, 2.7839222, -3.8458915, -21.320818, 2.7324455, -3.8458915, -21.224058, 2.7629898, -3.8672514, -21.219786, 2.7839222, -3.8672514, -21.340467, 2.7409892, -3.8458915, -21.320818, 2.7324455, -3.774764, -21.320818, 2.140356, -3.7534046, -21.335342, 1.6268702, -3.7534046, -21.340467, 2.1489, -3.774764, -21.320818, 2.140356, -3.774764, -21.316118, 1.6358411, -3.7534046, -21.335342, 1.6268702, -3.774764, -21.316118, 1.6358411, -3.7534046, -21.210388, 1.6072192, -3.7534046, -21.335342, 1.6268702, -3.774764, -21.316118, 1.6358411, -3.774764, -21.215515, 1.6279382, -3.7534046, -21.210388, 1.6072192, -3.7534046, -21.340467, 2.1489, -3.774764, -21.35478, 1.6176856, -3.774764, -21.360119, 2.1574438, -3.7534046, -21.340467, 2.1489, -3.7534046, -21.335342, 1.6268702, -3.774764, -21.35478, 1.6176856, -3.7534046, -21.335342, 1.6268702, -3.774764, -21.205261, 1.5865004, -3.774764, -21.35478, 1.6176856, -3.7534046, -21.335342, 1.6268702, -3.7534046, -21.210388, 1.6072192, -3.774764, -21.205261, 1.5865004, -3.774764, -21.360119, 2.1574438, -3.796124, -21.335342, 1.6268702, -3.796124, -21.340467, 2.1489, -3.774764, -21.360119, 2.1574438, -3.774764, -21.35478, 1.6176856, -3.796124, -21.335342, 1.6268702, -3.774764, -21.35478, 1.6176856, -3.796124, -21.210388, 1.6072192, -3.796124, -21.335342, 1.6268702, -3.774764, -21.35478, 1.6176856, -3.774764, -21.205261, 1.5865004, -3.796124, -21.210388, 1.6072192, -3.796124, -21.340467, 2.1489, -3.774764, -21.316118, 1.6358411, -3.774764, -21.320818, 2.140356, -3.796124, -21.340467, 2.1489, -3.796124, -21.335342, 1.6268702, -3.774764, -21.316118, 1.6358411, -3.796124, -21.335342, 1.6268702, -3.774764, -21.215515, 1.6279382, -3.774764, -21.316118, 1.6358411, -3.796124, -21.335342, 1.6268702, -3.796124, -21.210388, 1.6072192, -3.774764, -21.215515, 1.6279382, -3.774764, -21.215515, 1.6279382, -3.7534046, -21.105513, 1.6392586, -3.7534046, -21.210388, 1.6072192, -3.774764, -21.215515, 1.6279382, -3.774764, -21.126446, 1.6439579, -3.7534046, -21.105513, 1.6392586, -3.774764, -21.126446, 1.6439579, -3.7534046, -21.10359, 2.144628, -3.7534046, -21.105513, 1.6392586, -3.774764, -21.126446, 1.6439579, -3.774764, -21.12431, 2.1395016, -3.7534046, -21.10359, 2.144628, -3.774764, -21.12431, 2.1395016, -3.7534046, -21.219786, 2.1918328, -3.7534046, -21.10359, 2.144628, -3.774764, -21.12431, 2.1395016, -3.774764, -21.224058, 2.1709003, -3.7534046, -21.219786, 2.1918328, -3.7534046, -21.210388, 1.6072192, -3.774764, -21.084793, 1.6347731, -3.774764, -21.205261, 1.5865004, -3.7534046, -21.210388, 1.6072192, -3.7534046, -21.105513, 1.6392586, -3.774764, -21.084793, 1.6347731, -3.7534046, -21.105513, 1.6392586, -3.774764, -21.082657, 2.1497543, -3.774764, -21.084793, 1.6347731, -3.7534046, -21.105513, 1.6392586, -3.7534046, -21.10359, 2.144628, -3.774764, -21.082657, 2.1497543, -3.7534046, -21.10359, 2.144628, -3.774764, -21.215515, 2.2127652, -3.774764, -21.082657, 2.1497543, -3.7534046, -21.10359, 2.144628, -3.7534046, -21.219786, 2.1918328, -3.774764, -21.215515, 2.2127652, -3.774764, -21.205261, 1.5865004, -3.796124, -21.105513, 1.6392586, -3.796124, -21.210388, 1.6072192, -3.774764, -21.205261, 1.5865004, -3.774764, -21.084793, 1.6347731, -3.796124, -21.105513, 1.6392586, -3.796124, -21.210388, 1.6072192, -3.774764, -21.126446, 1.6439579, -3.774764, -21.215515, 1.6279382, -3.796124, -21.210388, 1.6072192, -3.796124, -21.105513, 1.6392586, -3.774764, -21.126446, 1.6439579, -3.796124, -21.105513, 1.6392586, -3.774764, -21.12431, 2.1395016, -3.774764, -21.126446, 1.6439579, -3.796124, -21.105513, 1.6392586, -3.796124, -21.10359, 2.144628, -3.774764, -21.12431, 2.1395016, -3.796124, -21.10359, 2.144628, -3.774764, -21.224058, 2.1709003, -3.774764, -21.12431, 2.1395016, -3.796124, -21.10359, 2.144628, -3.796124, -21.219786, 2.1918328, -3.774764, -21.224058, 2.1709003, -3.774764, -21.084793, 1.6347731, -3.796124, -21.10359, 2.144628, -3.796124, -21.105513, 1.6392586, -3.774764, -21.084793, 1.6347731, -3.774764, -21.082657, 2.1497543, -3.796124, -21.10359, 2.144628, -3.774764, -21.082657, 2.1497543, -3.796124, -21.219786, 2.1918328, -3.796124, -21.10359, 2.144628, -3.774764, -21.082657, 2.1497543, -3.774764, -21.215515, 2.2127652, -3.796124, -21.219786, 2.1918328, -3.774764, -21.224058, 2.1709003, -3.7534046, -21.340467, 2.1489, -3.7534046, -21.219786, 2.1918328, -3.774764, -21.224058, 2.1709003, -3.774764, -21.320818, 2.140356, -3.7534046, -21.340467, 2.1489, -3.7534046, -21.219786, 2.1918328, -3.774764, -21.360119, 2.1574438, -3.774764, -21.215515, 2.2127652, -3.7534046, -21.219786, 2.1918328, -3.7534046, -21.340467, 2.1489, -3.774764, -21.360119, 2.1574438, -3.774764, -21.215515, 2.2127652, -3.796124, -21.340467, 2.1489, -3.796124, -21.219786, 2.1918328, -3.774764, -21.215515, 2.2127652, -3.774764, -21.360119, 2.1574438, -3.796124, -21.340467, 2.1489, -3.796124, -21.219786, 2.1918328, -3.774764, -21.320818, 2.140356, -3.774764, -21.224058, 2.1709003, -3.796124, -21.219786, 2.1918328, -3.796124, -21.340467, 2.1489, -3.774764, -21.320818, 2.140356, -7.4558854, -21.333633, -0.46060777, -7.431749, -21.35008, -0.89848065, -7.431749, -21.355635, -0.45334554, -7.4558854, -21.333633, -0.46060777, -7.4558854, -21.328295, -0.89057755, -7.431749, -21.35008, -0.89848065, -7.431749, -21.355635, -0.45334554, -7.4558854, -21.371868, -0.9061701, -7.4558854, -21.37785, -0.4460833, -7.431749, -21.355635, -0.45334554, -7.431749, -21.35008, -0.89848065, -7.4558854, -21.371868, -0.9061701, -7.4558854, -21.37785, -0.4460833, -7.480022, -21.35008, -0.89848065, -7.480022, -21.355635, -0.45334554, -7.4558854, -21.37785, -0.4460833, -7.4558854, -21.371868, -0.9061701, -7.480022, -21.35008, -0.89848065, -7.480022, -21.355635, -0.45334554, -7.4558854, -21.328295, -0.89057755, -7.4558854, -21.333633, -0.46060777, -7.480022, -21.355635, -0.45334554, -7.480022, -21.35008, -0.89848065, -7.4558854, -21.328295, -0.89057755, -7.4558854, -21.328295, -0.89057755, -7.431749, -21.208895, -0.9149277, -7.431749, -21.35008, -0.89848065, -7.4558854, -21.328295, -0.89057755, -7.4558854, -21.21466, -0.8974128, -7.431749, -21.208895, -0.9149277, -7.431749, -21.35008, -0.89848065, -7.4558854, -21.20334, -0.9326565, -7.4558854, -21.371868, -0.9061701, -7.431749, -21.35008, -0.89848065, -7.431749, -21.208895, -0.9149277, -7.4558854, -21.20334, -0.9326565, -7.4558854, -21.371868, -0.9061701, -7.480022, -21.208895, -0.9149277, -7.480022, -21.35008, -0.89848065, -7.4558854, -21.371868, -0.9061701, -7.4558854, -21.20334, -0.9326565, -7.480022, -21.208895, -0.9149277, -7.480022, -21.35008, -0.89848065, -7.4558854, -21.21466, -0.8974128, -7.4558854, -21.328295, -0.89057755, -7.480022, -21.35008, -0.89848065, -7.480022, -21.208895, -0.9149277, -7.4558854, -21.21466, -0.8974128, -7.4558854, -21.21466, -0.8974128, -7.431749, -21.090775, -0.88758755, -7.431749, -21.208895, -0.9149277, -7.4558854, -21.21466, -0.8974128, -7.4558854, -21.114271, -0.8837426, -7.431749, -21.090775, -0.88758755, -7.431749, -21.208895, -0.9149277, -7.4558854, -21.06728, -0.89164567, -7.4558854, -21.20334, -0.9326565, -7.431749, -21.208895, -0.9149277, -7.431749, -21.090775, -0.88758755, -7.4558854, -21.06728, -0.89164567, -7.4558854, -21.20334, -0.9326565, -7.480022, -21.090775, -0.88758755, -7.480022, -21.208895, -0.9149277, -7.4558854, -21.20334, -0.9326565, -7.4558854, -21.06728, -0.89164567, -7.480022, -21.090775, -0.88758755, -7.480022, -21.208895, -0.9149277, -7.4558854, -21.114271, -0.8837426, -7.4558854, -21.21466, -0.8974128, -7.480022, -21.208895, -0.9149277, -7.480022, -21.090775, -0.88758755, -7.4558854, -21.114271, -0.8837426, -7.4558854, -21.114271, -0.8837426, -7.431749, -21.088425, -0.45697713, -7.431749, -21.090775, -0.88758755, -7.4558854, -21.114271, -0.8837426, -7.4558854, -21.111708, -0.46124887, -7.431749, -21.088425, -0.45697713, -7.4558854, -21.111708, -0.46124887, -7.431749, -21.219574, -0.416821, -7.431749, -21.088425, -0.45697713, -7.4558854, -21.111708, -0.46124887, -7.4558854, -21.224487, -0.43454933, -7.431749, -21.219574, -0.416821, -7.431749, -21.090775, -0.88758755, -7.4558854, -21.06493, -0.45249128, -7.4558854, -21.06728, -0.89164567, -7.431749, -21.090775, -0.88758755, -7.431749, -21.088425, -0.45697713, -7.4558854, -21.06493, -0.45249128, -7.431749, -21.088425, -0.45697713, -7.4558854, -21.214874, -0.39887857, -7.4558854, -21.06493, -0.45249128, -7.431749, -21.088425, -0.45697713, -7.431749, -21.219574, -0.416821, -7.4558854, -21.214874, -0.39887857, -7.4558854, -21.06728, -0.89164567, -7.480022, -21.088425, -0.45697713, -7.480022, -21.090775, -0.88758755, -7.4558854, -21.06728, -0.89164567, -7.4558854, -21.06493, -0.45249128, -7.480022, -21.088425, -0.45697713, -7.4558854, -21.06493, -0.45249128, -7.480022, -21.219574, -0.416821, -7.480022, -21.088425, -0.45697713, -7.4558854, -21.06493, -0.45249128, -7.4558854, -21.214874, -0.39887857, -7.480022, -21.219574, -0.416821, -7.480022, -21.090775, -0.88758755, -7.4558854, -21.111708, -0.46124887, -7.4558854, -21.114271, -0.8837426, -7.480022, -21.090775, -0.88758755, -7.480022, -21.088425, -0.45697713, -7.4558854, -21.111708, -0.46124887, -7.480022, -21.088425, -0.45697713, -7.4558854, -21.224487, -0.43454933, -7.4558854, -21.111708, -0.46124887, -7.480022, -21.088425, -0.45697713, -7.480022, -21.219574, -0.416821, -7.4558854, -21.224487, -0.43454933, -7.4558854, -21.224487, -0.43454933, -7.431749, -21.355635, -0.45334554, -7.431749, -21.219574, -0.416821, -7.4558854, -21.224487, -0.43454933, -7.4558854, -21.333633, -0.46060777, -7.431749, -21.355635, -0.45334554, -7.431749, -21.219574, -0.416821, -7.4558854, -21.37785, -0.4460833, -7.4558854, -21.214874, -0.39887857, -7.431749, -21.219574, -0.416821, -7.431749, -21.355635, -0.45334554, -7.4558854, -21.37785, -0.4460833, -7.4558854, -21.214874, -0.39887857, -7.480022, -21.355635, -0.45334554, -7.480022, -21.219574, -0.416821, -7.4558854, -21.214874, -0.39887857, -7.4558854, -21.37785, -0.4460833, -7.480022, -21.355635, -0.45334554, -7.480022, -21.219574, -0.416821, -7.4558854, -21.333633, -0.46060777, -7.4558854, -21.224487, -0.43454933, -7.480022, -21.219574, -0.416821, -7.480022, -21.355635, -0.45334554, -7.4558854, -21.333633, -0.46060777, -3.8775039, -21.320818, 0.85066056, -3.8561444, -21.335342, 0.33696115, -3.8561444, -21.34047, 0.85920453, -3.8775039, -21.320818, 0.85066056, -3.8775039, -21.316118, 0.34593213, -3.8561444, -21.335342, 0.33696115, -3.8775039, -21.316118, 0.34593213, -3.8561444, -21.210388, 0.31752384, -3.8561444, -21.335342, 0.33696115, -3.8775039, -21.316118, 0.34593213, -3.8775039, -21.215515, 0.33824265, -3.8561444, -21.210388, 0.31752384, -3.8561444, -21.34047, 0.85920453, -3.8775039, -21.35478, 0.32799017, -3.8775039, -21.360119, 0.8677484, -3.8561444, -21.34047, 0.85920453, -3.8561444, -21.335342, 0.33696115, -3.8775039, -21.35478, 0.32799017, -3.8561444, -21.335342, 0.33696115, -3.8775039, -21.205263, 0.29680502, -3.8775039, -21.35478, 0.32799017, -3.8561444, -21.335342, 0.33696115, -3.8561444, -21.210388, 0.31752384, -3.8775039, -21.205263, 0.29680502, -3.8775039, -21.360119, 0.8677484, -3.8988638, -21.335342, 0.33696115, -3.8988638, -21.34047, 0.85920453, -3.8775039, -21.360119, 0.8677484, -3.8775039, -21.35478, 0.32799017, -3.8988638, -21.335342, 0.33696115, -3.8775039, -21.35478, 0.32799017, -3.8988638, -21.210388, 0.31752384, -3.8988638, -21.335342, 0.33696115, -3.8775039, -21.35478, 0.32799017, -3.8775039, -21.205263, 0.29680502, -3.8988638, -21.210388, 0.31752384, -3.8988638, -21.34047, 0.85920453, -3.8775039, -21.316118, 0.34593213, -3.8775039, -21.320818, 0.85066056, -3.8988638, -21.34047, 0.85920453, -3.8988638, -21.335342, 0.33696115, -3.8775039, -21.316118, 0.34593213, -3.8988638, -21.335342, 0.33696115, -3.8775039, -21.215515, 0.33824265, -3.8775039, -21.316118, 0.34593213, -3.8988638, -21.335342, 0.33696115, -3.8988638, -21.210388, 0.31752384, -3.8775039, -21.215515, 0.33824265, -3.8775039, -21.215515, 0.33824265, -3.8561444, -21.105513, 0.34956324, -3.8561444, -21.210388, 0.31752384, -3.8775039, -21.215515, 0.33824265, -3.8775039, -21.126446, 0.35404897, -3.8561444, -21.105513, 0.34956324, -3.8775039, -21.126446, 0.35404897, -3.8561444, -21.10359, 0.85493267, -3.8561444, -21.105513, 0.34956324, -3.8775039, -21.126446, 0.35404897, -3.8775039, -21.12431, 0.8498062, -3.8561444, -21.10359, 0.85493267, -3.8775039, -21.12431, 0.8498062, -3.8561444, -21.219788, 0.9021374, -3.8561444, -21.10359, 0.85493267, -3.8775039, -21.12431, 0.8498062, -3.8775039, -21.224058, 0.88120484, -3.8561444, -21.219788, 0.9021374, -3.8561444, -21.210388, 0.31752384, -3.8775039, -21.084793, 0.34507775, -3.8775039, -21.205263, 0.29680502, -3.8561444, -21.210388, 0.31752384, -3.8561444, -21.105513, 0.34956324, -3.8775039, -21.084793, 0.34507775, -3.8561444, -21.105513, 0.34956324, -3.8775039, -21.082657, 0.8600589, -3.8775039, -21.084793, 0.34507775, -3.8561444, -21.105513, 0.34956324, -3.8561444, -21.10359, 0.85493267, -3.8775039, -21.082657, 0.8600589, -3.8561444, -21.10359, 0.85493267, -3.8775039, -21.215515, 0.92306983, -3.8775039, -21.082657, 0.8600589, -3.8561444, -21.10359, 0.85493267, -3.8561444, -21.219788, 0.9021374, -3.8775039, -21.215515, 0.92306983, -3.8775039, -21.205263, 0.29680502, -3.8988638, -21.105513, 0.34956324, -3.8988638, -21.210388, 0.31752384, -3.8775039, -21.205263, 0.29680502, -3.8775039, -21.084793, 0.34507775, -3.8988638, -21.105513, 0.34956324, -3.8988638, -21.210388, 0.31752384, -3.8775039, -21.126446, 0.35404897, -3.8775039, -21.215515, 0.33824265, -3.8988638, -21.210388, 0.31752384, -3.8988638, -21.105513, 0.34956324, -3.8775039, -21.126446, 0.35404897, -3.8988638, -21.105513, 0.34956324, -3.8775039, -21.12431, 0.8498062, -3.8775039, -21.126446, 0.35404897, -3.8988638, -21.105513, 0.34956324, -3.8988638, -21.10359, 0.85493267, -3.8775039, -21.12431, 0.8498062, -3.8988638, -21.10359, 0.85493267, -3.8775039, -21.224058, 0.88120484, -3.8775039, -21.12431, 0.8498062, -3.8988638, -21.10359, 0.85493267, -3.8988638, -21.219788, 0.9021374, -3.8775039, -21.224058, 0.88120484, -3.8775039, -21.084793, 0.34507775, -3.8988638, -21.10359, 0.85493267, -3.8988638, -21.105513, 0.34956324, -3.8775039, -21.084793, 0.34507775, -3.8775039, -21.082657, 0.8600589, -3.8988638, -21.10359, 0.85493267, -3.8775039, -21.082657, 0.8600589, -3.8988638, -21.219788, 0.9021374, -3.8988638, -21.10359, 0.85493267, -3.8775039, -21.082657, 0.8600589, -3.8775039, -21.215515, 0.92306983, -3.8988638, -21.219788, 0.9021374, -3.8775039, -21.224058, 0.88120484, -3.8561444, -21.34047, 0.85920453, -3.8561444, -21.219788, 0.9021374, -3.8775039, -21.224058, 0.88120484, -3.8775039, -21.320818, 0.85066056, -3.8561444, -21.34047, 0.85920453, -3.8561444, -21.219788, 0.9021374, -3.8775039, -21.360119, 0.8677484, -3.8775039, -21.215515, 0.92306983, -3.8561444, -21.219788, 0.9021374, -3.8561444, -21.34047, 0.85920453, -3.8775039, -21.360119, 0.8677484, -3.8775039, -21.215515, 0.92306983, -3.8988638, -21.34047, 0.85920453, -3.8988638, -21.219788, 0.9021374, -3.8775039, -21.215515, 0.92306983, -3.8775039, -21.360119, 0.8677484, -3.8988638, -21.34047, 0.85920453, -3.8988638, -21.219788, 0.9021374, -3.8775039, -21.320818, 0.85066056, -3.8775039, -21.224058, 0.88120484, -3.8988638, -21.219788, 0.9021374, -3.8988638, -21.34047, 0.85920453, -3.8775039, -21.320818, 0.85066056, -3.7194428, -21.320816, 3.3595648, -3.698083, -21.335342, 2.8460789, -3.698083, -21.340467, 3.3681085, -3.7194428, -21.320816, 3.3595648, -3.7194428, -21.316118, 2.8550498, -3.698083, -21.335342, 2.8460789, -3.7194428, -21.316118, 2.8550498, -3.698083, -21.210388, 2.826428, -3.698083, -21.335342, 2.8460789, -3.7194428, -21.316118, 2.8550498, -3.7194428, -21.215513, 2.8471467, -3.698083, -21.210388, 2.826428, -3.698083, -21.340467, 3.3681085, -3.7194428, -21.354778, 2.836894, -3.7194428, -21.360119, 3.3768659, -3.698083, -21.340467, 3.3681085, -3.698083, -21.335342, 2.8460789, -3.7194428, -21.354778, 2.836894, -3.698083, -21.335342, 2.8460789, -3.7194428, -21.205261, 2.805709, -3.7194428, -21.354778, 2.836894, -3.698083, -21.335342, 2.8460789, -3.698083, -21.210388, 2.826428, -3.7194428, -21.205261, 2.805709, -3.7194428, -21.360119, 3.3768659, -3.7408023, -21.335342, 2.8460789, -3.7408023, -21.340467, 3.3681085, -3.7194428, -21.360119, 3.3768659, -3.7194428, -21.354778, 2.836894, -3.7408023, -21.335342, 2.8460789, -3.7194428, -21.354778, 2.836894, -3.7408023, -21.210388, 2.826428, -3.7408023, -21.335342, 2.8460789, -3.7194428, -21.354778, 2.836894, -3.7194428, -21.205261, 2.805709, -3.7408023, -21.210388, 2.826428, -3.7408023, -21.340467, 3.3681085, -3.7194428, -21.316118, 2.8550498, -3.7194428, -21.320816, 3.3595648, -3.7408023, -21.340467, 3.3681085, -3.7408023, -21.335342, 2.8460789, -3.7194428, -21.316118, 2.8550498, -3.7408023, -21.335342, 2.8460789, -3.7194428, -21.215513, 2.8471467, -3.7194428, -21.316118, 2.8550498, -3.7408023, -21.335342, 2.8460789, -3.7408023, -21.210388, 2.826428, -3.7194428, -21.215513, 2.8471467, -3.7194428, -21.215513, 2.8471467, -3.698083, -21.105513, 2.8586807, -3.698083, -21.210388, 2.826428, -3.7194428, -21.215513, 2.8471467, -3.7194428, -21.126444, 2.8631663, -3.698083, -21.105513, 2.8586807, -3.7194428, -21.126444, 2.8631663, -3.698083, -21.10359, 3.3640501, -3.698083, -21.105513, 2.8586807, -3.7194428, -21.126444, 2.8631663, -3.7194428, -21.124308, 3.358924, -3.698083, -21.10359, 3.3640501, -3.7194428, -21.124308, 3.358924, -3.698083, -21.219786, 3.411255, -3.698083, -21.10359, 3.3640501, -3.7194428, -21.124308, 3.358924, -3.7194428, -21.224058, 3.3903227, -3.698083, -21.219786, 3.411255, -3.698083, -21.210388, 2.826428, -3.7194428, -21.084793, 2.8539817, -3.7194428, -21.205261, 2.805709, -3.698083, -21.210388, 2.826428, -3.698083, -21.105513, 2.8586807, -3.7194428, -21.084793, 2.8539817, -3.698083, -21.105513, 2.8586807, -3.7194428, -21.082657, 3.3691764, -3.7194428, -21.084793, 2.8539817, -3.698083, -21.105513, 2.8586807, -3.698083, -21.10359, 3.3640501, -3.7194428, -21.082657, 3.3691764, -3.698083, -21.10359, 3.3640501, -3.7194428, -21.215513, 3.431974, -3.7194428, -21.082657, 3.3691764, -3.698083, -21.10359, 3.3640501, -3.698083, -21.219786, 3.411255, -3.7194428, -21.215513, 3.431974, -3.7194428, -21.205261, 2.805709, -3.7408023, -21.105513, 2.8586807, -3.7408023, -21.210388, 2.826428, -3.7194428, -21.205261, 2.805709, -3.7194428, -21.084793, 2.8539817, -3.7408023, -21.105513, 2.8586807, -3.7408023, -21.210388, 2.826428, -3.7194428, -21.126444, 2.8631663, -3.7194428, -21.215513, 2.8471467, -3.7408023, -21.210388, 2.826428, -3.7408023, -21.105513, 2.8586807, -3.7194428, -21.126444, 2.8631663, -3.7408023, -21.105513, 2.8586807, -3.7194428, -21.124308, 3.358924, -3.7194428, -21.126444, 2.8631663, -3.7408023, -21.105513, 2.8586807, -3.7408023, -21.10359, 3.3640501, -3.7194428, -21.124308, 3.358924, -3.7408023, -21.10359, 3.3640501, -3.7194428, -21.224058, 3.3903227, -3.7194428, -21.124308, 3.358924, -3.7408023, -21.10359, 3.3640501, -3.7408023, -21.219786, 3.411255, -3.7194428, -21.224058, 3.3903227, -3.7194428, -21.084793, 2.8539817, -3.7408023, -21.10359, 3.3640501, -3.7408023, -21.105513, 2.8586807, -3.7194428, -21.084793, 2.8539817, -3.7194428, -21.082657, 3.3691764, -3.7408023, -21.10359, 3.3640501, -3.7194428, -21.082657, 3.3691764, -3.7408023, -21.219786, 3.411255, -3.7408023, -21.10359, 3.3640501, -3.7194428, -21.082657, 3.3691764, -3.7194428, -21.215513, 3.431974, -3.7408023, -21.219786, 3.411255, -3.7194428, -21.224058, 3.3903227, -3.698083, -21.340467, 3.3681085, -3.698083, -21.219786, 3.411255, -3.7194428, -21.224058, 3.3903227, -3.7194428, -21.320816, 3.3595648, -3.698083, -21.340467, 3.3681085, -3.698083, -21.219786, 3.411255, -3.7194428, -21.360119, 3.3768659, -3.7194428, -21.215513, 3.431974, -3.698083, -21.219786, 3.411255, -3.698083, -21.340467, 3.3681085, -3.7194428, -21.360119, 3.3768659, -3.7194428, -21.215513, 3.431974, -3.7408023, -21.340467, 3.3681085, -3.7408023, -21.219786, 3.411255, -3.7194428, -21.215513, 3.431974, -3.7194428, -21.360119, 3.3768659, -3.7408023, -21.340467, 3.3681085, -3.7408023, -21.219786, 3.411255, -3.7194428, -21.320816, 3.3595648, -3.7194428, -21.224058, 3.3903227, -3.7408023, -21.219786, 3.411255, -3.7408023, -21.340467, 3.3681085, -3.7194428, -21.320816, 3.3595648, -7.4172244, -21.333633, 4.035811, -7.393088, -21.35008, 3.6370263, -7.393088, -21.355633, 4.0424323, -7.4172244, -21.333633, 4.035811, -7.4172244, -21.328293, 3.6438615, -7.393088, -21.35008, 3.6370263, -7.393088, -21.355633, 4.0424323, -7.4172244, -21.371866, 3.6299777, -7.4172244, -21.377848, 4.049054, -7.393088, -21.355633, 4.0424323, -7.393088, -21.35008, 3.6370263, -7.4172244, -21.371866, 3.6299777, -7.4172244, -21.377848, 4.049054, -7.441361, -21.35008, 3.6370263, -7.441361, -21.355633, 4.0424323, -7.4172244, -21.377848, 4.049054, -7.4172244, -21.371866, 3.6299777, -7.441361, -21.35008, 3.6370263, -7.441361, -21.355633, 4.0424323, -7.4172244, -21.328293, 3.6438615, -7.4172244, -21.333633, 4.035811, -7.441361, -21.355633, 4.0424323, -7.441361, -21.35008, 3.6370263, -7.4172244, -21.328293, 3.6438615, -7.4172244, -21.328293, 3.6438615, -7.393088, -21.208893, 3.621861, -7.393088, -21.35008, 3.6370263, -7.4172244, -21.328293, 3.6438615, -7.4172244, -21.214659, 3.6378808, -7.393088, -21.208893, 3.621861, -7.393088, -21.35008, 3.6370263, -7.4172244, -21.203339, 3.6056278, -7.4172244, -21.371866, 3.6299777, -7.393088, -21.35008, 3.6370263, -7.393088, -21.208893, 3.621861, -7.4172244, -21.203339, 3.6056278, -7.4172244, -21.371866, 3.6299777, -7.441361, -21.208893, 3.621861, -7.441361, -21.35008, 3.6370263, -7.4172244, -21.371866, 3.6299777, -7.4172244, -21.203339, 3.6056278, -7.441361, -21.208893, 3.621861, -7.441361, -21.35008, 3.6370263, -7.4172244, -21.214659, 3.6378808, -7.4172244, -21.328293, 3.6438615, -7.441361, -21.35008, 3.6370263, -7.441361, -21.208893, 3.621861, -7.4172244, -21.214659, 3.6378808, -7.4172244, -21.214659, 3.6378808, -7.393088, -21.090773, 3.6466384, -7.393088, -21.208893, 3.621861, -7.4172244, -21.214659, 3.6378808, -7.4172244, -21.11427, 3.6502695, -7.393088, -21.090773, 3.6466384, -7.393088, -21.208893, 3.621861, -7.4172244, -21.067278, 3.6432207, -7.4172244, -21.203339, 3.6056278, -7.393088, -21.208893, 3.621861, -7.393088, -21.090773, 3.6466384, -7.4172244, -21.067278, 3.6432207, -7.4172244, -21.203339, 3.6056278, -7.441361, -21.090773, 3.6466384, -7.441361, -21.208893, 3.621861, -7.4172244, -21.203339, 3.6056278, -7.4172244, -21.067278, 3.6432207, -7.441361, -21.090773, 3.6466384, -7.441361, -21.208893, 3.621861, -7.4172244, -21.11427, 3.6502695, -7.4172244, -21.214659, 3.6378808, -7.441361, -21.208893, 3.621861, -7.441361, -21.090773, 3.6466384, -7.4172244, -21.11427, 3.6502695, -7.4172244, -21.11427, 3.6502695, -7.393088, -21.088425, 4.0392284, -7.393088, -21.090773, 3.6466384, -7.4172244, -21.11427, 3.6502695, -7.4172244, -21.111706, 4.03517, -7.393088, -21.088425, 4.0392284, -7.393088, -21.090773, 3.6466384, -7.4172244, -21.064928, 4.043073, -7.4172244, -21.067278, 3.6432207, -7.393088, -21.090773, 3.6466384, -7.393088, -21.088425, 4.0392284, -7.4172244, -21.064928, 4.043073, -7.4172244, -21.067278, 3.6432207, -7.441361, -21.088425, 4.0392284, -7.441361, -21.090773, 3.6466384, -7.4172244, -21.067278, 3.6432207, -7.4172244, -21.064928, 4.043073, -7.441361, -21.088425, 4.0392284, -7.441361, -21.090773, 3.6466384, -7.4172244, -21.111706, 4.03517, -7.4172244, -21.11427, 3.6502695, -7.441361, -21.090773, 3.6466384, -7.441361, -21.088425, 4.0392284, -7.4172244, -21.111706, 4.03517, -7.4172244, -21.111706, 4.03517, -7.393088, -21.219572, 4.0757537, -7.393088, -21.088425, 4.0392284, -7.4172244, -21.111706, 4.03517, -7.4172244, -21.224485, 4.0595202, -7.393088, -21.219572, 4.0757537, -7.393088, -21.088425, 4.0392284, -7.4172244, -21.214872, 4.0919867, -7.4172244, -21.064928, 4.043073, -7.393088, -21.088425, 4.0392284, -7.393088, -21.219572, 4.0757537, -7.4172244, -21.214872, 4.0919867, -7.4172244, -21.064928, 4.043073, -7.441361, -21.219572, 4.0757537, -7.441361, -21.088425, 4.0392284, -7.4172244, -21.064928, 4.043073, -7.4172244, -21.214872, 4.0919867, -7.441361, -21.219572, 4.0757537, -7.441361, -21.088425, 4.0392284, -7.4172244, -21.224485, 4.0595202, -7.4172244, -21.111706, 4.03517, -7.441361, -21.088425, 4.0392284, -7.441361, -21.219572, 4.0757537, -7.4172244, -21.224485, 4.0595202, -7.4172244, -21.224485, 4.0595202, -7.393088, -21.355633, 4.0424323, -7.393088, -21.219572, 4.0757537, -7.4172244, -21.224485, 4.0595202, -7.4172244, -21.333633, 4.035811, -7.393088, -21.355633, 4.0424323, -7.393088, -21.219572, 4.0757537, -7.4172244, -21.377848, 4.049054, -7.4172244, -21.214872, 4.0919867, -7.393088, -21.219572, 4.0757537, -7.393088, -21.355633, 4.0424323, -7.4172244, -21.377848, 4.049054, -7.4172244, -21.214872, 4.0919867, -7.441361, -21.355633, 4.0424323, -7.441361, -21.219572, 4.0757537, -7.4172244, -21.214872, 4.0919867, -7.4172244, -21.377848, 4.049054, -7.441361, -21.355633, 4.0424323, -7.441361, -21.219572, 4.0757537, -7.4172244, -21.333633, 4.035811, -7.4172244, -21.224485, 4.0595202, -7.441361, -21.219572, 4.0757537, -7.441361, -21.355633, 4.0424323, -7.4172244, -21.333633, 4.035811, -7.4558854, -21.333632, 4.726582, -7.431749, -21.350079, 4.2887096, -7.431749, -21.355633, 4.7338443, -7.4558854, -21.333632, 4.726582, -7.4558854, -21.328293, 4.2966127, -7.431749, -21.350079, 4.2887096, -7.431749, -21.355633, 4.7338443, -7.4558854, -21.371866, 4.281234, -7.4558854, -21.377846, 4.7411065, -7.431749, -21.355633, 4.7338443, -7.431749, -21.350079, 4.2887096, -7.4558854, -21.371866, 4.281234, -7.4558854, -21.377846, 4.7411065, -7.480022, -21.350079, 4.2887096, -7.480022, -21.355633, 4.7338443, -7.4558854, -21.377846, 4.7411065, -7.4558854, -21.371866, 4.281234, -7.480022, -21.350079, 4.2887096, -7.480022, -21.355633, 4.7338443, -7.4558854, -21.328293, 4.2966127, -7.4558854, -21.333632, 4.726582, -7.480022, -21.355633, 4.7338443, -7.480022, -21.350079, 4.2887096, -7.4558854, -21.328293, 4.2966127, -7.4558854, -21.328293, 4.2966127, -7.431749, -21.208893, 4.272262, -7.431749, -21.350079, 4.2887096, -7.4558854, -21.328293, 4.2966127, -7.4558854, -21.214659, 4.2899904, -7.431749, -21.208893, 4.272262, -7.431749, -21.350079, 4.2887096, -7.4558854, -21.203339, 4.254534, -7.4558854, -21.371866, 4.281234, -7.431749, -21.350079, 4.2887096, -7.431749, -21.208893, 4.272262, -7.4558854, -21.203339, 4.254534, -7.4558854, -21.371866, 4.281234, -7.480022, -21.208893, 4.272262, -7.480022, -21.350079, 4.2887096, -7.4558854, -21.371866, 4.281234, -7.4558854, -21.203339, 4.254534, -7.480022, -21.208893, 4.272262, -7.480022, -21.350079, 4.2887096, -7.4558854, -21.214659, 4.2899904, -7.4558854, -21.328293, 4.2966127, -7.480022, -21.350079, 4.2887096, -7.480022, -21.208893, 4.272262, -7.4558854, -21.214659, 4.2899904, -7.4558854, -21.214659, 4.2899904, -7.431749, -21.090773, 4.2996025, -7.431749, -21.208893, 4.272262, -7.4558854, -21.214659, 4.2899904, -7.4558854, -21.11427, 4.3034472, -7.431749, -21.090773, 4.2996025, -7.431749, -21.208893, 4.272262, -7.4558854, -21.067278, 4.295544, -7.4558854, -21.203339, 4.254534, -7.431749, -21.208893, 4.272262, -7.431749, -21.090773, 4.2996025, -7.4558854, -21.067278, 4.295544, -7.4558854, -21.203339, 4.254534, -7.480022, -21.090773, 4.2996025, -7.480022, -21.208893, 4.272262, -7.4558854, -21.203339, 4.254534, -7.4558854, -21.067278, 4.295544, -7.480022, -21.090773, 4.2996025, -7.480022, -21.208893, 4.272262, -7.4558854, -21.11427, 4.3034472, -7.4558854, -21.214659, 4.2899904, -7.480022, -21.208893, 4.272262, -7.480022, -21.090773, 4.2996025, -7.4558854, -21.11427, 4.3034472, -7.4558854, -21.11427, 4.3034472, -7.431749, -21.088423, 4.730213, -7.431749, -21.090773, 4.2996025, -7.4558854, -21.11427, 4.3034472, -7.4558854, -21.111706, 4.7259407, -7.431749, -21.088423, 4.730213, -7.4558854, -21.111706, 4.7259407, -7.431749, -21.219572, 4.770369, -7.431749, -21.088423, 4.730213, -7.4558854, -21.111706, 4.7259407, -7.4558854, -21.224485, 4.7526407, -7.431749, -21.219572, 4.770369, -7.431749, -21.090773, 4.2996025, -7.4558854, -21.064928, 4.7346983, -7.4558854, -21.067278, 4.295544, -7.431749, -21.090773, 4.2996025, -7.431749, -21.088423, 4.730213, -7.4558854, -21.064928, 4.7346983, -7.431749, -21.088423, 4.730213, -7.4558854, -21.214872, 4.788311, -7.4558854, -21.064928, 4.7346983, -7.431749, -21.088423, 4.730213, -7.431749, -21.219572, 4.770369, -7.4558854, -21.214872, 4.788311, -7.4558854, -21.067278, 4.295544, -7.480022, -21.088423, 4.730213, -7.480022, -21.090773, 4.2996025, -7.4558854, -21.067278, 4.295544, -7.4558854, -21.064928, 4.7346983, -7.480022, -21.088423, 4.730213, -7.4558854, -21.064928, 4.7346983, -7.480022, -21.219572, 4.770369, -7.480022, -21.088423, 4.730213, -7.4558854, -21.064928, 4.7346983, -7.4558854, -21.214872, 4.788311, -7.480022, -21.219572, 4.770369, -7.480022, -21.090773, 4.2996025, -7.4558854, -21.111706, 4.7259407, -7.4558854, -21.11427, 4.3034472, -7.480022, -21.090773, 4.2996025, -7.480022, -21.088423, 4.730213, -7.4558854, -21.111706, 4.7259407, -7.480022, -21.088423, 4.730213, -7.4558854, -21.224485, 4.7526407, -7.4558854, -21.111706, 4.7259407, -7.480022, -21.088423, 4.730213, -7.480022, -21.219572, 4.770369, -7.4558854, -21.224485, 4.7526407, -7.4558854, -21.224485, 4.7526407, -7.431749, -21.355633, 4.7338443, -7.431749, -21.219572, 4.770369, -7.4558854, -21.224485, 4.7526407, -7.4558854, -21.333632, 4.726582, -7.431749, -21.355633, 4.7338443, -7.431749, -21.219572, 4.770369, -7.4558854, -21.377846, 4.7411065, -7.4558854, -21.214872, 4.788311, -7.431749, -21.219572, 4.770369, -7.431749, -21.355633, 4.7338443, -7.4558854, -21.377846, 4.7411065, -7.4558854, -21.214872, 4.788311, -7.480022, -21.355633, 4.7338443, -7.480022, -21.219572, 4.770369, -7.4558854, -21.214872, 4.788311, -7.4558854, -21.377846, 4.7411065, -7.480022, -21.355633, 4.7338443, -7.480022, -21.219572, 4.770369, -7.4558854, -21.333632, 4.726582, -7.4558854, -21.224485, 4.7526407, -7.480022, -21.219572, 4.770369, -7.480022, -21.355633, 4.7338443, -7.4558854, -21.333632, 4.726582, -3.7589583, -21.320816, 4.7411065, -3.7375984, -21.33534, 4.2274075, -3.7375984, -21.340467, 4.7496505, -3.7589583, -21.320816, 4.7411065, -3.7589583, -21.316118, 4.236378, -3.7375984, -21.33534, 4.2274075, -3.7589583, -21.316118, 4.236378, -3.7375984, -21.210388, 4.2079697, -3.7375984, -21.33534, 4.2274075, -3.7589583, -21.316118, 4.236378, -3.7589583, -21.215513, 4.2284746, -3.7375984, -21.210388, 4.2079697, -3.7375984, -21.340467, 4.7496505, -3.7589583, -21.354778, 4.2184362, -3.7589583, -21.360119, 4.758194, -3.7375984, -21.340467, 4.7496505, -3.7375984, -21.33534, 4.2274075, -3.7589583, -21.354778, 4.2184362, -3.7375984, -21.33534, 4.2274075, -3.7589583, -21.205261, 4.1870375, -3.7589583, -21.354778, 4.2184362, -3.7375984, -21.33534, 4.2274075, -3.7375984, -21.210388, 4.2079697, -3.7589583, -21.205261, 4.1870375, -3.7589583, -21.360119, 4.758194, -3.7803178, -21.33534, 4.2274075, -3.7803178, -21.340467, 4.7496505, -3.7589583, -21.360119, 4.758194, -3.7589583, -21.354778, 4.2184362, -3.7803178, -21.33534, 4.2274075, -3.7589583, -21.354778, 4.2184362, -3.7803178, -21.210388, 4.2079697, -3.7803178, -21.33534, 4.2274075, -3.7589583, -21.354778, 4.2184362, -3.7589583, -21.205261, 4.1870375, -3.7803178, -21.210388, 4.2079697, -3.7803178, -21.340467, 4.7496505, -3.7589583, -21.316118, 4.236378, -3.7589583, -21.320816, 4.7411065, -3.7803178, -21.340467, 4.7496505, -3.7803178, -21.33534, 4.2274075, -3.7589583, -21.316118, 4.236378, -3.7803178, -21.33534, 4.2274075, -3.7589583, -21.215513, 4.2284746, -3.7589583, -21.316118, 4.236378, -3.7803178, -21.33534, 4.2274075, -3.7803178, -21.210388, 4.2079697, -3.7589583, -21.215513, 4.2284746, -3.7589583, -21.215513, 4.2284746, -3.7375984, -21.10551, 4.2400093, -3.7375984, -21.210388, 4.2079697, -3.7589583, -21.215513, 4.2284746, -3.7589583, -21.126444, 4.2444944, -3.7375984, -21.10551, 4.2400093, -3.7589583, -21.126444, 4.2444944, -3.7375984, -21.103588, 4.7453785, -3.7375984, -21.10551, 4.2400093, -3.7589583, -21.126444, 4.2444944, -3.7589583, -21.124308, 4.740252, -3.7375984, -21.103588, 4.7453785, -3.7589583, -21.124308, 4.740252, -3.7375984, -21.219786, 4.7925835, -3.7375984, -21.103588, 4.7453785, -3.7589583, -21.124308, 4.740252, -3.7589583, -21.224058, 4.7716503, -3.7375984, -21.219786, 4.7925835, -3.7375984, -21.210388, 4.2079697, -3.7589583, -21.084793, 4.23531, -3.7589583, -21.205261, 4.1870375, -3.7375984, -21.210388, 4.2079697, -3.7375984, -21.10551, 4.2400093, -3.7589583, -21.084793, 4.23531, -3.7375984, -21.10551, 4.2400093, -3.7589583, -21.082657, 4.7505045, -3.7589583, -21.084793, 4.23531, -3.7375984, -21.10551, 4.2400093, -3.7375984, -21.103588, 4.7453785, -3.7589583, -21.082657, 4.7505045, -3.7375984, -21.103588, 4.7453785, -3.7589583, -21.215513, 4.8135157, -3.7589583, -21.082657, 4.7505045, -3.7375984, -21.103588, 4.7453785, -3.7375984, -21.219786, 4.7925835, -3.7589583, -21.215513, 4.8135157, -3.7589583, -21.205261, 4.1870375, -3.7803178, -21.10551, 4.2400093, -3.7803178, -21.210388, 4.2079697, -3.7589583, -21.205261, 4.1870375, -3.7589583, -21.084793, 4.23531, -3.7803178, -21.10551, 4.2400093, -3.7803178, -21.210388, 4.2079697, -3.7589583, -21.126444, 4.2444944, -3.7589583, -21.215513, 4.2284746, -3.7803178, -21.210388, 4.2079697, -3.7803178, -21.10551, 4.2400093, -3.7589583, -21.126444, 4.2444944, -3.7803178, -21.10551, 4.2400093, -3.7589583, -21.124308, 4.740252, -3.7589583, -21.126444, 4.2444944, -3.7803178, -21.10551, 4.2400093, -3.7803178, -21.103588, 4.7453785, -3.7589583, -21.124308, 4.740252, -3.7803178, -21.103588, 4.7453785, -3.7589583, -21.224058, 4.7716503, -3.7589583, -21.124308, 4.740252, -3.7803178, -21.103588, 4.7453785, -3.7803178, -21.219786, 4.7925835, -3.7589583, -21.224058, 4.7716503, -3.7589583, -21.084793, 4.23531, -3.7803178, -21.103588, 4.7453785, -3.7803178, -21.10551, 4.2400093, -3.7589583, -21.084793, 4.23531, -3.7589583, -21.082657, 4.7505045, -3.7803178, -21.103588, 4.7453785, -3.7589583, -21.082657, 4.7505045, -3.7803178, -21.219786, 4.7925835, -3.7803178, -21.103588, 4.7453785, -3.7589583, -21.082657, 4.7505045, -3.7589583, -21.215513, 4.8135157, -3.7803178, -21.219786, 4.7925835, -3.7589583, -21.224058, 4.7716503, -3.7375984, -21.340467, 4.7496505, -3.7375984, -21.219786, 4.7925835, -3.7589583, -21.224058, 4.7716503, -3.7589583, -21.320816, 4.7411065, -3.7375984, -21.340467, 4.7496505, -3.7375984, -21.219786, 4.7925835, -3.7589583, -21.360119, 4.758194, -3.7589583, -21.215513, 4.8135157, -3.7375984, -21.219786, 4.7925835, -3.7375984, -21.340467, 4.7496505, -3.7589583, -21.360119, 4.758194, -3.7589583, -21.215513, 4.8135157, -3.7803178, -21.340467, 4.7496505, -3.7803178, -21.219786, 4.7925835, -3.7589583, -21.215513, 4.8135157, -3.7589583, -21.360119, 4.758194, -3.7803178, -21.340467, 4.7496505, -3.7803178, -21.219786, 4.7925835, -3.7589583, -21.320816, 4.7411065, -3.7589583, -21.224058, 4.7716503, -3.7803178, -21.219786, 4.7925835, -3.7803178, -21.340467, 4.7496505, -3.7589583, -21.320816, 4.7411065, -3.7589583, -21.320816, 5.3611774, -3.7375984, -21.33534, 4.8476915, -3.7375984, -21.340467, 5.3697214, -3.7589583, -21.320816, 5.3611774, -3.7589583, -21.316116, 4.8566623, -3.7375984, -21.33534, 4.8476915, -3.7589583, -21.316116, 4.8566623, -3.7375984, -21.210386, 4.82804, -3.7375984, -21.33534, 4.8476915, -3.7589583, -21.316116, 4.8566623, -3.7589583, -21.215513, 4.8487587, -3.7375984, -21.210386, 4.82804, -3.7375984, -21.340467, 5.3697214, -3.7589583, -21.354778, 4.8387203, -3.7589583, -21.360119, 5.378265, -3.7375984, -21.340467, 5.3697214, -3.7375984, -21.33534, 4.8476915, -3.7589583, -21.354778, 4.8387203, -3.7375984, -21.33534, 4.8476915, -3.7589583, -21.205261, 4.8073215, -3.7589583, -21.354778, 4.8387203, -3.7375984, -21.33534, 4.8476915, -3.7375984, -21.210386, 4.82804, -3.7589583, -21.205261, 4.8073215, -3.7589583, -21.360119, 5.378265, -3.7803178, -21.33534, 4.8476915, -3.7803178, -21.340467, 5.3697214, -3.7589583, -21.360119, 5.378265, -3.7589583, -21.354778, 4.8387203, -3.7803178, -21.33534, 4.8476915, -3.7589583, -21.354778, 4.8387203, -3.7803178, -21.210386, 4.82804, -3.7803178, -21.33534, 4.8476915, -3.7589583, -21.354778, 4.8387203, -3.7589583, -21.205261, 4.8073215, -3.7803178, -21.210386, 4.82804, -3.7803178, -21.340467, 5.3697214, -3.7589583, -21.316116, 4.8566623, -3.7589583, -21.320816, 5.3611774, -3.7803178, -21.340467, 5.3697214, -3.7803178, -21.33534, 4.8476915, -3.7589583, -21.316116, 4.8566623, -3.7803178, -21.33534, 4.8476915, -3.7589583, -21.215513, 4.8487587, -3.7589583, -21.316116, 4.8566623, -3.7803178, -21.33534, 4.8476915, -3.7803178, -21.210386, 4.82804, -3.7589583, -21.215513, 4.8487587, -3.7589583, -21.215513, 4.8487587, -3.7375984, -21.10551, 4.8602934, -3.7375984, -21.210386, 4.82804, -3.7589583, -21.215513, 4.8487587, -3.7589583, -21.126444, 4.8647785, -3.7375984, -21.10551, 4.8602934, -3.7589583, -21.126444, 4.8647785, -3.7375984, -21.103588, 5.3656626, -3.7375984, -21.10551, 4.8602934, -3.7589583, -21.126444, 4.8647785, -3.7589583, -21.124308, 5.3605366, -3.7375984, -21.103588, 5.3656626, -3.7589583, -21.124308, 5.3605366, -3.7375984, -21.219786, 5.412654, -3.7375984, -21.103588, 5.3656626, -3.7589583, -21.124308, 5.3605366, -3.7589583, -21.224056, 5.3917212, -3.7375984, -21.219786, 5.412654, -3.7375984, -21.210386, 4.82804, -3.7589583, -21.084793, 4.855594, -3.7589583, -21.205261, 4.8073215, -3.7375984, -21.210386, 4.82804, -3.7375984, -21.10551, 4.8602934, -3.7589583, -21.084793, 4.855594, -3.7375984, -21.10551, 4.8602934, -3.7589583, -21.082657, 5.370575, -3.7589583, -21.084793, 4.855594, -3.7375984, -21.10551, 4.8602934, -3.7375984, -21.103588, 5.3656626, -3.7589583, -21.082657, 5.370575, -3.7375984, -21.103588, 5.3656626, -3.7589583, -21.215513, 5.433586, -3.7589583, -21.082657, 5.370575, -3.7375984, -21.103588, 5.3656626, -3.7375984, -21.219786, 5.412654, -3.7589583, -21.215513, 5.433586, -3.7589583, -21.205261, 4.8073215, -3.7803178, -21.10551, 4.8602934, -3.7803178, -21.210386, 4.82804, -3.7589583, -21.205261, 4.8073215, -3.7589583, -21.084793, 4.855594, -3.7803178, -21.10551, 4.8602934, -3.7803178, -21.210386, 4.82804, -3.7589583, -21.126444, 4.8647785, -3.7589583, -21.215513, 4.8487587, -3.7803178, -21.210386, 4.82804, -3.7803178, -21.10551, 4.8602934, -3.7589583, -21.126444, 4.8647785, -3.7803178, -21.10551, 4.8602934, -3.7589583, -21.124308, 5.3605366, -3.7589583, -21.126444, 4.8647785, -3.7803178, -21.10551, 4.8602934, -3.7803178, -21.103588, 5.3656626, -3.7589583, -21.124308, 5.3605366, -3.7803178, -21.103588, 5.3656626, -3.7589583, -21.224056, 5.3917212, -3.7589583, -21.124308, 5.3605366, -3.7803178, -21.103588, 5.3656626, -3.7803178, -21.219786, 5.412654, -3.7589583, -21.224056, 5.3917212, -3.7589583, -21.084793, 4.855594, -3.7803178, -21.103588, 5.3656626, -3.7803178, -21.10551, 4.8602934, -3.7589583, -21.084793, 4.855594, -3.7589583, -21.082657, 5.370575, -3.7803178, -21.103588, 5.3656626, -3.7589583, -21.082657, 5.370575, -3.7803178, -21.219786, 5.412654, -3.7803178, -21.103588, 5.3656626, -3.7589583, -21.082657, 5.370575, -3.7589583, -21.215513, 5.433586, -3.7803178, -21.219786, 5.412654, -3.7589583, -21.224056, 5.3917212, -3.7375984, -21.340467, 5.3697214, -3.7375984, -21.219786, 5.412654, -3.7589583, -21.224056, 5.3917212, -3.7589583, -21.320816, 5.3611774, -3.7375984, -21.340467, 5.3697214, -3.7375984, -21.219786, 5.412654, -3.7589583, -21.360119, 5.378265, -3.7589583, -21.215513, 5.433586, -3.7375984, -21.219786, 5.412654, -3.7375984, -21.340467, 5.3697214, -3.7589583, -21.360119, 5.378265, -3.7589583, -21.215513, 5.433586, -3.7803178, -21.340467, 5.3697214, -3.7803178, -21.219786, 5.412654, -3.7589583, -21.215513, 5.433586, -3.7589583, -21.360119, 5.378265, -3.7803178, -21.340467, 5.3697214, -3.7803178, -21.219786, 5.412654, -3.7589583, -21.320816, 5.3611774, -3.7589583, -21.224056, 5.3917212, -3.7803178, -21.219786, 5.412654, -3.7803178, -21.340467, 5.3697214, -3.7589583, -21.320816, 5.3611774, -7.3817677, -21.333632, 5.409023, -7.357631, -21.350079, 4.8643517, -7.357631, -21.355633, 5.418207, -7.3817677, -21.333632, 5.409023, -7.3817677, -21.328293, 4.8739634, -7.357631, -21.350079, 4.8643517, -7.3817677, -21.328293, 4.8739634, -7.357631, -21.208893, 4.8436327, -7.357631, -21.350079, 4.8643517, -7.3817677, -21.328293, 4.8739634, -7.3817677, -21.214659, 4.8654194, -7.357631, -21.208893, 4.8436327, -7.357631, -21.355633, 5.418207, -7.3817677, -21.371866, 4.85474, -7.3817677, -21.377846, 5.4271784, -7.357631, -21.355633, 5.418207, -7.357631, -21.350079, 4.8643517, -7.3817677, -21.371866, 4.85474, -7.357631, -21.350079, 4.8643517, -7.3817677, -21.203339, 4.8216324, -7.3817677, -21.371866, 4.85474, -7.357631, -21.350079, 4.8643517, -7.357631, -21.208893, 4.8436327, -7.3817677, -21.203339, 4.8216324, -7.3817677, -21.377846, 5.4271784, -7.4059043, -21.350079, 4.8643517, -7.4059043, -21.355633, 5.418207, -7.3817677, -21.377846, 5.4271784, -7.3817677, -21.371866, 4.85474, -7.4059043, -21.350079, 4.8643517, -7.3817677, -21.371866, 4.85474, -7.4059043, -21.208893, 4.8436327, -7.4059043, -21.350079, 4.8643517, -7.3817677, -21.371866, 4.85474, -7.3817677, -21.203339, 4.8216324, -7.4059043, -21.208893, 4.8436327, -7.4059043, -21.355633, 5.418207, -7.3817677, -21.328293, 4.8739634, -7.3817677, -21.333632, 5.409023, -7.4059043, -21.355633, 5.418207, -7.4059043, -21.350079, 4.8643517, -7.3817677, -21.328293, 4.8739634, -7.4059043, -21.350079, 4.8643517, -7.3817677, -21.214659, 4.8654194, -7.3817677, -21.328293, 4.8739634, -7.4059043, -21.350079, 4.8643517, -7.4059043, -21.208893, 4.8436327, -7.3817677, -21.214659, 4.8654194, -7.3817677, -21.214659, 4.8654194, -7.357631, -21.090773, 4.8775945, -7.357631, -21.208893, 4.8436327, -7.3817677, -21.214659, 4.8654194, -7.3817677, -21.11427, 4.8825073, -7.357631, -21.090773, 4.8775945, -7.3817677, -21.11427, 4.8825073, -7.357631, -21.088423, 5.4137216, -7.357631, -21.090773, 4.8775945, -7.3817677, -21.11427, 4.8825073, -7.3817677, -21.111706, 5.4083815, -7.357631, -21.088423, 5.4137216, -7.3817677, -21.111706, 5.4083815, -7.357631, -21.219572, 5.463703, -7.357631, -21.088423, 5.4137216, -7.3817677, -21.111706, 5.4083815, -7.3817677, -21.224483, 5.441489, -7.357631, -21.219572, 5.463703, -7.357631, -21.208893, 4.8436327, -7.3817677, -21.067278, 4.8726816, -7.3817677, -21.203339, 4.8216324, -7.357631, -21.208893, 4.8436327, -7.357631, -21.090773, 4.8775945, -7.3817677, -21.067278, 4.8726816, -7.357631, -21.090773, 4.8775945, -7.3817677, -21.064928, 5.4190617, -7.3817677, -21.067278, 4.8726816, -7.357631, -21.090773, 4.8775945, -7.357631, -21.088423, 5.4137216, -7.3817677, -21.064928, 5.4190617, -7.357631, -21.088423, 5.4137216, -7.3817677, -21.214872, 5.485917, -7.3817677, -21.064928, 5.4190617, -7.357631, -21.088423, 5.4137216, -7.357631, -21.219572, 5.463703, -7.3817677, -21.214872, 5.485917, -7.3817677, -21.203339, 4.8216324, -7.4059043, -21.090773, 4.8775945, -7.4059043, -21.208893, 4.8436327, -7.3817677, -21.203339, 4.8216324, -7.3817677, -21.067278, 4.8726816, -7.4059043, -21.090773, 4.8775945, -7.4059043, -21.208893, 4.8436327, -7.3817677, -21.11427, 4.8825073, -7.3817677, -21.214659, 4.8654194, -7.4059043, -21.208893, 4.8436327, -7.4059043, -21.090773, 4.8775945, -7.3817677, -21.11427, 4.8825073, -7.4059043, -21.090773, 4.8775945, -7.3817677, -21.111706, 5.4083815, -7.3817677, -21.11427, 4.8825073, -7.4059043, -21.090773, 4.8775945, -7.4059043, -21.088423, 5.4137216, -7.3817677, -21.111706, 5.4083815, -7.4059043, -21.088423, 5.4137216, -7.3817677, -21.224483, 5.441489, -7.3817677, -21.111706, 5.4083815, -7.4059043, -21.088423, 5.4137216, -7.4059043, -21.219572, 5.463703, -7.3817677, -21.224483, 5.441489, -7.3817677, -21.067278, 4.8726816, -7.4059043, -21.088423, 5.4137216, -7.4059043, -21.090773, 4.8775945, -7.3817677, -21.067278, 4.8726816, -7.3817677, -21.064928, 5.4190617, -7.4059043, -21.088423, 5.4137216, -7.3817677, -21.064928, 5.4190617, -7.4059043, -21.219572, 5.463703, -7.4059043, -21.088423, 5.4137216, -7.3817677, -21.064928, 5.4190617, -7.3817677, -21.214872, 5.485917, -7.4059043, -21.219572, 5.463703, -7.3817677, -21.224483, 5.441489, -7.357631, -21.355633, 5.418207, -7.357631, -21.219572, 5.463703, -7.3817677, -21.224483, 5.441489, -7.3817677, -21.333632, 5.409023, -7.357631, -21.355633, 5.418207, -7.357631, -21.219572, 5.463703, -7.3817677, -21.377846, 5.4271784, -7.3817677, -21.214872, 5.485917, -7.357631, -21.219572, 5.463703, -7.357631, -21.355633, 5.418207, -7.3817677, -21.377846, 5.4271784, -7.3817677, -21.214872, 5.485917, -7.4059043, -21.355633, 5.418207, -7.4059043, -21.219572, 5.463703, -7.3817677, -21.214872, 5.485917, -7.3817677, -21.377846, 5.4271784, -7.4059043, -21.355633, 5.418207, -7.4059043, -21.219572, 5.463703, -7.3817677, -21.333632, 5.409023, -7.3817677, -21.224483, 5.441489, -7.4059043, -21.219572, 5.463703, -7.4059043, -21.355633, 5.418207, -7.3817677, -21.333632, 5.409023, 16.86181, -22.803505, -1.1805243, 12.459326, -22.631578, -0.992887, 16.845665, -22.437832, -1.1752877, 16.86181, -22.803505, -1.1805243, 12.4754715, -22.996815, -0.99855995, 12.459326, -22.631578, -0.992887, 21.372076, -22.244087, -0.9867778, 16.86181, -22.803505, -1.1805243, 16.845665, -22.437832, -1.1752877, 21.372076, -22.244087, -0.9867778, 21.38822, -22.609324, -0.9924512, 16.86181, -22.803505, -1.1805243, 16.86181, -22.78867, -2.160163, 12.4754715, -22.996815, -0.99855995, 16.86181, -22.803505, -1.1805243, 16.86181, -22.78867, -2.160163, 12.4754715, -22.984161, -1.8285255, 12.4754715, -22.996815, -0.99855995, 16.86181, -22.803505, -1.1805243, 21.38822, -22.592306, -2.12569, 16.86181, -22.78867, -2.160163, 16.86181, -22.803505, -1.1805243, 21.38822, -22.601032, -1.5496888, 21.38822, -22.592306, -2.12569, 21.38822, -22.601032, -1.5496888, 16.86181, -22.803505, -1.1805243, 21.38822, -22.609324, -0.9924512, 12.4754715, -22.984161, -1.8285255, 12.459326, -22.631578, -0.992887, 12.4754715, -22.996815, -0.99855995, 12.4754715, -22.984161, -1.8285255, 12.459326, -22.618923, -1.8228531, 12.459326, -22.631578, -0.992887, 21.372076, -22.236668, -1.4776883, 21.20582, -22.351868, -1.5763068, 21.38822, -22.601032, -1.5496888, 16.845665, -22.426924, -1.9096894, 16.849592, -22.514633, -2.155799, 17.977158, -22.374123, -2.1461992, 16.845665, -22.426924, -1.9096894, 15.749081, -22.472305, -2.0715804, 16.849592, -22.514633, -2.155799, 16.845665, -22.426924, -1.9096894, 12.459326, -22.618923, -1.8228531, 15.749081, -22.472305, -2.0715804, 16.845665, -22.426924, -1.9096894, 12.459326, -22.631578, -0.992887, 12.459326, -22.618923, -1.8228531, 12.459326, -22.631578, -0.992887, 16.845665, -22.426924, -1.9096894, 16.845665, -22.437832, -1.1752877, 17.977158, -22.374123, -2.1461992, 16.845665, -22.437832, -1.1752877, 16.845665, -22.426924, -1.9096894, 16.845665, -22.437832, -1.1752877, 21.372076, -22.236668, -1.4776883, 21.372076, -22.244087, -0.9867778, 16.845665, -22.437832, -1.1752877, 20.594036, -22.268959, -1.5780525, 21.372076, -22.236668, -1.4776883, 17.977158, -22.374123, -2.1461992, 20.594036, -22.268959, -1.5780525, 16.845665, -22.437832, -1.1752877, 21.20582, -22.351868, -1.5763068, 21.372076, -22.236668, -1.4776883, 20.594036, -22.268959, -1.5780525, 21.372076, -22.232304, -1.7539072, 21.20582, -22.351868, -1.5763068, 20.594036, -22.268959, -1.5780525, 20.594036, -22.268959, -1.5780525, 17.977158, -22.374123, -2.1461992, 21.372076, -22.232304, -1.7539072, 21.372076, -22.232304, -1.7539072, 17.977158, -22.374123, -2.1461992, 21.37251, -22.227068, -2.1200175, 21.38822, -22.609324, -0.9924512, 21.372076, -22.236668, -1.4776883, 21.38822, -22.601032, -1.5496888, 21.38822, -22.609324, -0.9924512, 21.372076, -22.244087, -0.9867778, 21.372076, -22.236668, -1.4776883, 21.372076, -22.232304, -1.7539072, 21.38822, -22.592306, -2.12569, 21.38822, -22.601032, -1.5496888, 21.372076, -22.232304, -1.7539072, 21.37251, -22.227068, -2.1200175, 21.38822, -22.592306, -2.12569, 21.20582, -22.351868, -1.5763068, 21.372076, -22.232304, -1.7539072, 21.38822, -22.601032, -1.5496888, 21.37251, -22.227068, -2.1200175, 16.86181, -22.78867, -2.160163, 21.38822, -22.592306, -2.12569, 16.86181, -22.78867, -2.160163, 21.37251, -22.227068, -2.1200175, 16.849592, -22.514633, -2.155799, 16.849592, -22.514633, -2.155799, 21.37251, -22.227068, -2.1200175, 17.977158, -22.374123, -2.1461992, 16.849592, -22.514633, -2.155799, 12.459326, -22.618923, -1.8228531, 16.86181, -22.78867, -2.160163, 16.86181, -22.78867, -2.160163, 12.459326, -22.618923, -1.8228531, 12.4754715, -22.984161, -1.8285255, 12.459326, -22.618923, -1.8228531, 16.849592, -22.514633, -2.155799, 15.749081, -22.472305, -2.0715804, 13.651039, -22.54256, -3.436966, 12.76609, -22.946634, -3.462275, 13.666748, -22.907797, -3.4426389, 13.651039, -22.54256, -3.436966, 12.749945, -22.581396, -3.4570384, 12.76609, -22.946634, -3.462275, 13.666748, -22.907797, -3.4426389, 21.08582, -22.31085, -3.4526749, 13.651039, -22.54256, -3.436966, 13.651039, -22.54256, -3.436966, 21.08582, -22.31085, -3.4526749, 19.224287, -22.300377, -3.4474387, 21.08582, -22.31085, -3.4526749, 13.666748, -22.907797, -3.4426389, 21.0976, -22.584887, -3.4566026, 12.749945, -22.596233, -2.4743457, 13.651039, -22.54256, -3.436966, 13.651039, -22.557396, -2.4507813, 19.224287, -22.300377, -3.4474387, 13.651039, -22.557396, -2.4507813, 13.651039, -22.54256, -3.436966, 19.224287, -22.300377, -3.4474387, 21.081892, -22.231869, -2.6214004, 13.651039, -22.557396, -2.4507813, 21.081892, -22.231869, -2.6214004, 19.224287, -22.300377, -3.4474387, 21.081892, -22.222704, -3.2436562, 21.081892, -22.222704, -3.2436562, 19.224287, -22.300377, -3.4474387, 21.08582, -22.31085, -3.4526749, 13.651039, -22.54256, -3.436966, 12.749945, -22.596233, -2.4743457, 13.230383, -22.567432, -2.9914374, 13.230383, -22.567432, -2.9914374, 12.749945, -22.596233, -2.4743457, 12.749945, -22.590124, -2.8701282, 12.749945, -22.590124, -2.8701282, 12.838528, -22.724524, -2.981401, 13.230383, -22.567432, -2.9914374, 12.838528, -22.724524, -2.981401, 12.749945, -22.586197, -3.144165, 13.230383, -22.567432, -2.9914374, 13.651039, -22.54256, -3.436966, 13.230383, -22.567432, -2.9914374, 12.749945, -22.586197, -3.144165, 13.651039, -22.54256, -3.436966, 12.749945, -22.586197, -3.144165, 12.749945, -22.581396, -3.4570384, 13.666748, -22.923069, -2.4560184, 12.749945, -22.596233, -2.4743457, 13.651039, -22.557396, -2.4507813, 13.666748, -22.923069, -2.4560184, 12.76609, -22.96147, -2.4795818, 12.749945, -22.596233, -2.4743457, 21.081892, -22.231869, -2.6214004, 13.666748, -22.923069, -2.4560184, 13.651039, -22.557396, -2.4507813, 21.081892, -22.231869, -2.6214004, 21.0976, -22.597105, -2.6270728, 13.666748, -22.923069, -2.4560184, 12.76609, -22.946634, -3.462275, 13.666748, -22.923069, -2.4560184, 13.666748, -22.907797, -3.4426389, 13.666748, -22.923069, -2.4560184, 12.76609, -22.946634, -3.462275, 12.76609, -22.954924, -2.931219, 13.666748, -22.923069, -2.4560184, 12.76609, -22.954924, -2.931219, 12.76609, -22.96147, -2.4795818, 21.0976, -22.597105, -2.6270728, 13.666748, -22.907797, -3.4426389, 13.666748, -22.923069, -2.4560184, 21.0976, -22.597105, -2.6270728, 21.0976, -22.584887, -3.4566026, 13.666748, -22.907797, -3.4426389, 12.749945, -22.586197, -3.144165, 12.838528, -22.724524, -2.981401, 12.76609, -22.954924, -2.931219, 12.838528, -22.724524, -2.981401, 12.749945, -22.590124, -2.8701282, 12.76609, -22.954924, -2.931219, 12.76609, -22.946634, -3.462275, 12.749945, -22.586197, -3.144165, 12.76609, -22.954924, -2.931219, 12.76609, -22.946634, -3.462275, 12.749945, -22.581396, -3.4570384, 12.749945, -22.586197, -3.144165, 12.749945, -22.590124, -2.8701282, 12.76609, -22.96147, -2.4795818, 12.76609, -22.954924, -2.931219, 12.749945, -22.590124, -2.8701282, 12.749945, -22.596233, -2.4743457, 12.76609, -22.96147, -2.4795818, 21.081892, -22.231869, -2.6214004, 21.0976, -22.584887, -3.4566026, 21.0976, -22.597105, -2.6270728, 21.0976, -22.584887, -3.4566026, 21.081892, -22.231869, -2.6214004, 21.08582, -22.31085, -3.4526749, 21.08582, -22.31085, -3.4526749, 21.081892, -22.231869, -2.6214004, 21.081892, -22.222704, -3.2436562, 16.86661, -22.74067, -5.2888975, 12.750381, -22.556524, -5.1012607, 16.850464, -22.375433, -5.2836614, 16.86661, -22.74067, -5.2888975, 12.76609, -22.92176, -5.1069336, 12.750381, -22.556524, -5.1012607, 21.081892, -22.194342, -5.0951514, 16.86661, -22.74067, -5.2888975, 16.850464, -22.375433, -5.2836614, 21.081892, -22.194342, -5.0951514, 21.0976, -22.559578, -5.100825, 16.86661, -22.74067, -5.2888975, 16.86661, -22.725834, -6.2681003, 12.76609, -22.92176, -5.1069336, 16.86661, -22.74067, -5.2888975, 16.86661, -22.725834, -6.2681003, 12.76609, -22.909105, -5.936463, 12.76609, -22.92176, -5.1069336, 16.86661, -22.74067, -5.2888975, 21.0976, -22.54256, -6.2344995, 16.86661, -22.725834, -6.2681003, 16.86661, -22.74067, -5.2888975, 21.0976, -22.551287, -5.658062, 21.0976, -22.54256, -6.2344995, 21.0976, -22.551287, -5.658062, 16.86661, -22.74067, -5.2888975, 21.0976, -22.559578, -5.100825, 12.76609, -22.909105, -5.936463, 12.750381, -22.556524, -5.1012607, 12.76609, -22.92176, -5.1069336, 12.76609, -22.909105, -5.936463, 12.750381, -22.543869, -5.93079, 12.750381, -22.556524, -5.1012607, 21.081892, -22.186922, -5.5856256, 20.926546, -22.301687, -5.6846805, 21.0976, -22.551287, -5.658062, 16.850464, -22.364523, -6.018063, 16.854391, -22.451796, -6.2637362, 17.908213, -22.314777, -6.2541366, 16.850464, -22.364523, -6.018063, 15.825443, -22.406414, -6.179518, 16.854391, -22.451796, -6.2637362, 16.850464, -22.364523, -6.018063, 12.750381, -22.543869, -5.93079, 15.825443, -22.406414, -6.179518, 16.850464, -22.364523, -6.018063, 12.750381, -22.556524, -5.1012607, 12.750381, -22.543869, -5.93079, 12.750381, -22.556524, -5.1012607, 16.850464, -22.364523, -6.018063, 16.850464, -22.375433, -5.2836614, 17.908213, -22.314777, -6.2541366, 16.850464, -22.375433, -5.2836614, 16.850464, -22.364523, -6.018063, 16.850464, -22.375433, -5.2836614, 21.081892, -22.186922, -5.5856256, 21.081892, -22.194342, -5.0951514, 16.850464, -22.375433, -5.2836614, 20.354473, -22.217031, -5.6864257, 21.081892, -22.186922, -5.5856256, 17.908213, -22.314777, -6.2541366, 20.354473, -22.217031, -5.6864257, 16.850464, -22.375433, -5.2836614, 20.926546, -22.301687, -5.6846805, 21.081892, -22.186922, -5.5856256, 20.354473, -22.217031, -5.6864257, 21.081892, -22.182995, -5.862281, 20.926546, -22.301687, -5.6846805, 20.354473, -22.217031, -5.6864257, 20.354473, -22.217031, -5.6864257, 17.908213, -22.314777, -6.2541366, 21.081892, -22.182995, -5.862281, 21.081892, -22.182995, -5.862281, 17.908213, -22.314777, -6.2541366, 21.081892, -22.177322, -6.228827, 21.0976, -22.559578, -5.100825, 21.081892, -22.186922, -5.5856256, 21.0976, -22.551287, -5.658062, 21.0976, -22.559578, -5.100825, 21.081892, -22.194342, -5.0951514, 21.081892, -22.186922, -5.5856256, 21.081892, -22.182995, -5.862281, 21.0976, -22.54256, -6.2344995, 21.0976, -22.551287, -5.658062, 21.081892, -22.182995, -5.862281, 21.081892, -22.177322, -6.228827, 21.0976, -22.54256, -6.2344995, 20.926546, -22.301687, -5.6846805, 21.081892, -22.182995, -5.862281, 21.0976, -22.551287, -5.658062, 21.081892, -22.177322, -6.228827, 16.86661, -22.725834, -6.2681003, 21.0976, -22.54256, -6.2344995, 16.86661, -22.725834, -6.2681003, 21.081892, -22.177322, -6.228827, 16.854391, -22.451796, -6.2637362, 16.854391, -22.451796, -6.2637362, 21.081892, -22.177322, -6.228827, 17.908213, -22.314777, -6.2541366, 16.854391, -22.451796, -6.2637362, 12.750381, -22.543869, -5.93079, 16.86661, -22.725834, -6.2681003, 16.86661, -22.725834, -6.2681003, 12.750381, -22.543869, -5.93079, 12.76609, -22.909105, -5.936463, 12.750381, -22.543869, -5.93079, 16.854391, -22.451796, -6.2637362, 15.825443, -22.406414, -6.179518, 19.724798, -22.63725, -3.9361672, 12.459763, -22.58925, -3.7760215, 19.70909, -22.271578, -3.9304943, 19.724798, -22.63725, -3.9361672, 12.4754715, -22.954489, -3.781694, 12.459763, -22.58925, -3.7760215, 21.37251, -22.199577, -3.9226394, 19.724798, -22.63725, -3.9361672, 19.70909, -22.271578, -3.9304943, 21.37251, -22.199577, -3.9226394, 21.38822, -22.564814, -3.9283128, 19.724798, -22.63725, -3.9361672, 19.724798, -22.621979, -4.944606, 12.4754715, -22.954489, -3.781694, 19.724798, -22.63725, -3.9361672, 19.724798, -22.621979, -4.944606, 12.4754715, -22.939651, -4.764387, 12.4754715, -22.954489, -3.781694, 21.38822, -22.564814, -3.9283128, 19.724798, -22.621979, -4.944606, 19.724798, -22.63725, -3.9361672, 21.38822, -22.564814, -3.9283128, 21.38822, -22.55216, -4.7578416, 19.724798, -22.621979, -4.944606, 21.38822, -22.564814, -3.9283128, 21.37251, -22.186922, -4.752169, 21.38822, -22.55216, -4.7578416, 21.38822, -22.564814, -3.9283128, 21.37251, -22.199577, -3.9226394, 21.37251, -22.186922, -4.752169, 19.70909, -22.260231, -4.6867146, 19.713017, -22.34794, -4.9402423, 20.124945, -22.239286, -4.8926787, 19.70909, -22.260231, -4.6867146, 17.896868, -22.335724, -4.8939877, 19.713017, -22.34794, -4.9402423, 19.70909, -22.260231, -4.6867146, 13.819038, -22.514633, -4.792751, 17.896868, -22.335724, -4.8939877, 20.124945, -22.239286, -4.8926787, 19.70909, -22.271578, -3.9304943, 19.70909, -22.260231, -4.6867146, 20.124945, -22.239286, -4.8926787, 21.37251, -22.199577, -3.9226394, 19.70909, -22.271578, -3.9304943, 21.37251, -22.199577, -3.9226394, 20.124945, -22.239286, -4.8926787, 21.37251, -22.186922, -4.752169, 12.459763, -22.58925, -3.7760215, 19.70909, -22.260231, -4.6867146, 19.70909, -22.271578, -3.9304943, 19.70909, -22.260231, -4.6867146, 12.459763, -22.58925, -3.7760215, 13.819038, -22.514633, -4.792751, 13.819038, -22.514633, -4.792751, 12.459763, -22.58925, -3.7760215, 12.459763, -22.577906, -4.513041, 13.819038, -22.514633, -4.792751, 12.459763, -22.577906, -4.513041, 12.46369, -22.665615, -4.7600236, 21.37251, -22.186922, -4.752169, 19.724798, -22.621979, -4.944606, 21.38822, -22.55216, -4.7578416, 19.724798, -22.621979, -4.944606, 21.37251, -22.186922, -4.752169, 19.713017, -22.34794, -4.9402423, 19.713017, -22.34794, -4.9402423, 21.37251, -22.186922, -4.752169, 20.124945, -22.239286, -4.8926787, 19.713017, -22.34794, -4.9402423, 12.46369, -22.665615, -4.7600236, 19.724798, -22.621979, -4.944606, 19.724798, -22.621979, -4.944606, 12.46369, -22.665615, -4.7600236, 12.4754715, -22.939651, -4.764387, 12.46369, -22.665615, -4.7600236, 19.713017, -22.34794, -4.9402423, 13.819038, -22.514633, -4.792751, 13.819038, -22.514633, -4.792751, 19.713017, -22.34794, -4.9402423, 17.896868, -22.335724, -4.8939877, 12.46369, -22.665615, -4.7600236, 12.4754715, -22.954489, -3.781694, 12.4754715, -22.939651, -4.764387, 12.4754715, -22.954489, -3.781694, 12.46369, -22.665615, -4.7600236, 12.459763, -22.58925, -3.7760215, 12.459763, -22.58925, -3.7760215, 12.46369, -22.665615, -4.7600236, 12.459763, -22.577906, -4.513041, 16.95781, -23.00336, -0.79565, 21.06182, -22.63725, -0.9811053, 16.95781, -22.63725, -0.79565, 16.95781, -23.00336, -0.79565, 21.06182, -23.00336, -0.9811053, 21.06182, -22.63725, -0.9811053, 12.722454, -22.63725, -0.9811053, 16.95781, -23.00336, -0.79565, 16.95781, -22.63725, -0.79565, 12.722454, -22.63725, -0.9811053, 12.722454, -23.00336, -0.9811053, 16.95781, -23.00336, -0.79565, 16.95781, -23.00336, 0.18311548, 21.06182, -23.00336, -0.9811053, 16.95781, -23.00336, -0.79565, 16.95781, -23.00336, 0.18311548, 21.06182, -23.00336, -0.15113974, 21.06182, -23.00336, -0.9811053, 16.95781, -23.00336, -0.79565, 12.722454, -23.00336, 0.15257025, 16.95781, -23.00336, 0.18311548, 16.95781, -23.00336, -0.79565, 12.722454, -23.00336, -0.4238677, 12.722454, -23.00336, 0.15257025, 12.722454, -23.00336, -0.4238677, 16.95781, -23.00336, -0.79565, 12.722454, -23.00336, -0.9811053, 21.06182, -23.00336, -0.15113974, 21.06182, -22.63725, -0.9811053, 21.06182, -23.00336, -0.9811053, 21.06182, -23.00336, -0.15113974, 21.06182, -22.63725, -0.15113974, 21.06182, -22.63725, -0.9811053, 12.722454, -22.63725, -0.49063158, 12.882601, -22.746778, -0.39288568, 12.722454, -23.00336, -0.4238677, 16.95781, -22.63725, -0.06124878, 16.95781, -22.728888, 0.18311548, 15.899189, -22.63725, 0.17569733, 16.95781, -22.63725, -0.06124878, 17.983704, -22.63725, 0.09977007, 16.95781, -22.728888, 0.18311548, 16.95781, -22.63725, -0.06124878, 21.06182, -22.63725, -0.15113974, 17.983704, -22.63725, 0.09977007, 16.95781, -22.63725, -0.06124878, 21.06182, -22.63725, -0.9811053, 21.06182, -22.63725, -0.15113974, 21.06182, -22.63725, -0.9811053, 16.95781, -22.63725, -0.06124878, 16.95781, -22.63725, -0.79565, 15.899189, -22.63725, 0.17569733, 16.95781, -22.63725, -0.79565, 16.95781, -22.63725, -0.06124878, 16.95781, -22.63725, -0.79565, 12.722454, -22.63725, -0.49063158, 12.722454, -22.63725, -0.9811053, 16.95781, -22.63725, -0.79565, 13.450312, -22.63725, -0.39070415, 12.722454, -22.63725, -0.49063158, 15.899189, -22.63725, 0.17569733, 13.450312, -22.63725, -0.39070415, 16.95781, -22.63725, -0.79565, 12.882601, -22.746778, -0.39288568, 12.722454, -22.63725, -0.49063158, 13.450312, -22.63725, -0.39070415, 12.722454, -22.63725, -0.21441269, 12.882601, -22.746778, -0.39288568, 13.450312, -22.63725, -0.39070415, 13.450312, -22.63725, -0.39070415, 15.899189, -22.63725, 0.17569733, 12.722454, -22.63725, -0.21441269, 12.722454, -22.63725, -0.21441269, 15.899189, -22.63725, 0.17569733, 12.722454, -22.63725, 0.15257025, 12.722454, -23.00336, -0.9811053, 12.722454, -22.63725, -0.49063158, 12.722454, -23.00336, -0.4238677, 12.722454, -23.00336, -0.9811053, 12.722454, -22.63725, -0.9811053, 12.722454, -22.63725, -0.49063158, 12.722454, -22.63725, -0.21441269, 12.722454, -23.00336, 0.15257025, 12.722454, -23.00336, -0.4238677, 12.722454, -22.63725, -0.21441269, 12.722454, -22.63725, 0.15257025, 12.722454, -23.00336, 0.15257025, 12.882601, -22.746778, -0.39288568, 12.722454, -22.63725, -0.21441269, 12.722454, -23.00336, -0.4238677, 12.722454, -22.63725, 0.15257025, 16.95781, -23.00336, 0.18311548, 12.722454, -23.00336, 0.15257025, 16.95781, -23.00336, 0.18311548, 12.722454, -22.63725, 0.15257025, 15.899189, -22.63725, 0.17569733, 16.95781, -23.00336, 0.18311548, 15.899189, -22.63725, 0.17569733, 16.95781, -22.728888, 0.18311548, 16.95781, -22.728888, 0.18311548, 21.06182, -23.00336, -0.15113974, 16.95781, -23.00336, 0.18311548, 16.95781, -22.728888, 0.18311548, 21.06182, -22.63725, -0.15113974, 21.06182, -23.00336, -0.15113974, 21.06182, -22.63725, -0.15113974, 16.95781, -22.728888, 0.18311548, 17.983704, -22.63725, 0.09977007, 20.16029, -22.63725, 2.5866122, 21.06182, -23.00336, 2.6058125, 20.16029, -23.00336, 2.5866122, 20.16029, -22.63725, 2.5866122, 21.06182, -22.63725, 2.6058125, 21.06182, -23.00336, 2.6058125, 20.16029, -23.00336, 2.5866122, 14.582241, -22.63725, 2.6010118, 20.16029, -22.63725, 2.5866122, 20.16029, -23.00336, 2.5866122, 12.722454, -22.728888, 2.6058125, 14.582241, -22.63725, 2.6010118, 12.722454, -22.728888, 2.6058125, 20.16029, -23.00336, 2.5866122, 12.722454, -23.00336, 2.6058125, 21.06182, -22.63725, 1.6231189, 20.16029, -22.63725, 2.5866122, 20.16029, -22.63725, 1.6004281, 14.582241, -22.63725, 2.6010118, 20.16029, -22.63725, 1.6004281, 20.16029, -22.63725, 2.5866122, 14.582241, -22.63725, 2.6010118, 12.722454, -22.63725, 1.7762828, 20.16029, -22.63725, 1.6004281, 12.722454, -22.63725, 1.7762828, 14.582241, -22.63725, 2.6010118, 12.722454, -22.63725, 2.398975, 12.722454, -22.63725, 2.398975, 14.582241, -22.63725, 2.6010118, 12.722454, -22.728888, 2.6058125, 20.16029, -22.63725, 2.5866122, 21.06182, -22.63725, 1.6231189, 20.581383, -22.63725, 2.1410842, 20.581383, -22.63725, 2.1410842, 21.06182, -22.63725, 1.6231189, 21.06182, -22.63725, 2.0193381, 21.06182, -22.63725, 2.0193381, 20.979784, -22.776888, 2.128429, 20.581383, -22.63725, 2.1410842, 20.979784, -22.776888, 2.128429, 21.06182, -22.63725, 2.2933755, 20.581383, -22.63725, 2.1410842, 20.16029, -22.63725, 2.5866122, 20.581383, -22.63725, 2.1410842, 21.06182, -22.63725, 2.2933755, 20.16029, -22.63725, 2.5866122, 21.06182, -22.63725, 2.2933755, 21.06182, -22.63725, 2.6058125, 20.16029, -23.00336, 1.6004281, 21.06182, -22.63725, 1.6231189, 20.16029, -22.63725, 1.6004281, 20.16029, -23.00336, 1.6004281, 21.06182, -23.00336, 1.6231189, 21.06182, -22.63725, 1.6231189, 12.722454, -22.63725, 1.7762828, 20.16029, -23.00336, 1.6004281, 20.16029, -22.63725, 1.6004281, 12.722454, -22.63725, 1.7762828, 12.722454, -23.00336, 1.7762828, 20.16029, -23.00336, 1.6004281, 21.06182, -23.00336, 2.6058125, 20.16029, -23.00336, 1.6004281, 20.16029, -23.00336, 2.5866122, 20.16029, -23.00336, 1.6004281, 21.06182, -23.00336, 2.6058125, 21.06182, -23.00336, 2.0747561, 20.16029, -23.00336, 1.6004281, 21.06182, -23.00336, 2.0747561, 21.06182, -23.00336, 1.6231189, 12.722454, -23.00336, 1.7762828, 20.16029, -23.00336, 2.5866122, 20.16029, -23.00336, 1.6004281, 12.722454, -23.00336, 1.7762828, 12.722454, -23.00336, 2.6058125, 20.16029, -23.00336, 2.5866122, 21.06182, -22.63725, 2.2933755, 20.979784, -22.776888, 2.128429, 21.06182, -23.00336, 2.0747561, 20.979784, -22.776888, 2.128429, 21.06182, -22.63725, 2.0193381, 21.06182, -23.00336, 2.0747561, 21.06182, -23.00336, 2.6058125, 21.06182, -22.63725, 2.2933755, 21.06182, -23.00336, 2.0747561, 21.06182, -23.00336, 2.6058125, 21.06182, -22.63725, 2.6058125, 21.06182, -22.63725, 2.2933755, 21.06182, -22.63725, 2.0193381, 21.06182, -23.00336, 1.6231189, 21.06182, -23.00336, 2.0747561, 21.06182, -22.63725, 2.0193381, 21.06182, -22.63725, 1.6231189, 21.06182, -23.00336, 1.6231189, 12.722454, -22.63725, 1.7762828, 12.722454, -23.00336, 2.6058125, 12.722454, -23.00336, 1.7762828, 12.722454, -23.00336, 2.6058125, 12.722454, -22.63725, 1.7762828, 12.722454, -22.63725, 2.398975, 12.722454, -23.00336, 2.6058125, 12.722454, -22.63725, 2.398975, 12.722454, -22.728888, 2.6058125, 20.38851, -22.63725, 3.7669787, 21.352875, -23.00336, 3.786178, 20.38851, -23.00336, 3.7669787, 20.38851, -22.63725, 3.7669787, 21.352875, -22.63725, 3.786178, 21.352875, -23.00336, 3.786178, 20.38851, -23.00336, 3.7669787, 14.420786, -22.63725, 3.7813792, 20.38851, -22.63725, 3.7669787, 20.38851, -23.00336, 3.7669787, 12.431835, -22.728888, 3.786178, 14.420786, -22.63725, 3.7813792, 12.431835, -22.728888, 3.786178, 20.38851, -23.00336, 3.7669787, 12.431835, -23.00336, 3.786178, 21.352875, -22.63725, 2.8034854, 20.38851, -22.63725, 3.7669787, 20.38851, -22.63725, 2.7803578, 14.420786, -22.63725, 3.7813792, 20.38851, -22.63725, 2.7803578, 20.38851, -22.63725, 3.7669787, 14.420786, -22.63725, 3.7813792, 12.431835, -22.63725, 2.9562135, 20.38851, -22.63725, 2.7803578, 12.431835, -22.63725, 2.9562135, 14.420786, -22.63725, 3.7813792, 12.431835, -22.63725, 3.5784698, 12.431835, -22.63725, 3.5784698, 14.420786, -22.63725, 3.7813792, 12.431835, -22.728888, 3.786178, 20.38851, -22.63725, 3.7669787, 21.352875, -22.63725, 2.8034854, 20.838837, -22.63725, 3.321015, 20.838837, -22.63725, 3.321015, 21.352875, -22.63725, 2.8034854, 21.352875, -22.63725, 3.1997046, 21.352875, -22.63725, 3.1997046, 21.264729, -22.776888, 3.3083596, 20.838837, -22.63725, 3.321015, 21.264729, -22.776888, 3.3083596, 21.352875, -22.63725, 3.4733052, 20.838837, -22.63725, 3.321015, 20.38851, -22.63725, 3.7669787, 20.838837, -22.63725, 3.321015, 21.352875, -22.63725, 3.4733052, 20.38851, -22.63725, 3.7669787, 21.352875, -22.63725, 3.4733052, 21.352875, -22.63725, 3.786178, 20.38851, -23.00336, 2.7803578, 21.352875, -22.63725, 2.8034854, 20.38851, -22.63725, 2.7803578, 20.38851, -23.00336, 2.7803578, 21.352875, -23.00336, 2.8034854, 21.352875, -22.63725, 2.8034854, 12.431835, -22.63725, 2.9562135, 20.38851, -23.00336, 2.7803578, 20.38851, -22.63725, 2.7803578, 12.431835, -22.63725, 2.9562135, 12.431835, -23.00336, 2.9562135, 20.38851, -23.00336, 2.7803578, 21.352875, -23.00336, 3.786178, 20.38851, -23.00336, 2.7803578, 20.38851, -23.00336, 3.7669787, 20.38851, -23.00336, 2.7803578, 21.352875, -23.00336, 3.786178, 21.352875, -23.00336, 3.2546868, 20.38851, -23.00336, 2.7803578, 21.352875, -23.00336, 3.2546868, 21.352875, -23.00336, 2.8034854, 12.431835, -23.00336, 2.9562135, 20.38851, -23.00336, 3.7669787, 20.38851, -23.00336, 2.7803578, 12.431835, -23.00336, 2.9562135, 12.431835, -23.00336, 3.786178, 20.38851, -23.00336, 3.7669787, 21.352875, -22.63725, 3.4733052, 21.264729, -22.776888, 3.3083596, 21.352875, -23.00336, 3.2546868, 21.264729, -22.776888, 3.3083596, 21.352875, -22.63725, 3.1997046, 21.352875, -23.00336, 3.2546868, 21.352875, -23.00336, 3.786178, 21.352875, -22.63725, 3.4733052, 21.352875, -23.00336, 3.2546868, 21.352875, -23.00336, 3.786178, 21.352875, -22.63725, 3.786178, 21.352875, -22.63725, 3.4733052, 21.352875, -22.63725, 3.1997046, 21.352875, -23.00336, 2.8034854, 21.352875, -23.00336, 3.2546868, 21.352875, -22.63725, 3.1997046, 21.352875, -22.63725, 2.8034854, 21.352875, -23.00336, 2.8034854, 12.431835, -22.63725, 2.9562135, 12.431835, -23.00336, 3.786178, 12.431835, -23.00336, 2.9562135, 12.431835, -23.00336, 3.786178, 12.431835, -22.63725, 2.9562135, 12.431835, -22.63725, 3.5784698, 12.431835, -23.00336, 3.786178, 12.431835, -22.63725, 3.5784698, 12.431835, -22.728888, 3.786178, 14.157221, -23.00336, 0.4183159, 21.256002, -22.63725, 0.25860643, 14.157221, -22.63725, 0.4183159, 14.157221, -23.00336, 0.4183159, 21.256002, -23.00336, 0.25860643, 21.256002, -22.63725, 0.25860643, 12.528709, -22.63725, 0.41177034, 14.157221, -23.00336, 0.4183159, 14.157221, -22.63725, 0.4183159, 12.528709, -22.63725, 0.41177034, 12.528709, -23.00336, 0.41177034, 14.157221, -23.00336, 0.4183159, 14.157221, -23.00336, 1.4267545, 21.256002, -23.00336, 0.25860643, 14.157221, -23.00336, 0.4183159, 14.157221, -23.00336, 1.4267545, 21.256002, -23.00336, 1.2413001, 21.256002, -23.00336, 0.25860643, 12.528709, -23.00336, 0.41177034, 14.157221, -23.00336, 1.4267545, 14.157221, -23.00336, 0.4183159, 12.528709, -23.00336, 0.41177034, 12.528709, -23.00336, 1.2413001, 14.157221, -23.00336, 1.4267545, 12.528709, -23.00336, 0.41177034, 12.528709, -22.63725, 1.2413001, 12.528709, -23.00336, 1.2413001, 12.528709, -23.00336, 0.41177034, 12.528709, -22.63725, 0.41177034, 12.528709, -22.63725, 1.2413001, 14.157221, -22.63725, 1.1745362, 14.157221, -22.728888, 1.4267545, 13.750093, -22.63725, 1.3805003, 14.157221, -22.63725, 1.1745362, 15.931917, -22.63725, 1.3805003, 14.157221, -22.728888, 1.4267545, 14.157221, -22.63725, 1.1745362, 19.925089, -22.63725, 1.2762094, 15.931917, -22.63725, 1.3805003, 13.750093, -22.63725, 1.3805003, 14.157221, -22.63725, 0.4183159, 14.157221, -22.63725, 1.1745362, 13.750093, -22.63725, 1.3805003, 12.528709, -22.63725, 0.41177034, 14.157221, -22.63725, 0.4183159, 12.528709, -22.63725, 0.41177034, 13.750093, -22.63725, 1.3805003, 12.528709, -22.63725, 1.2413001, 21.256002, -22.63725, 0.25860643, 14.157221, -22.63725, 1.1745362, 14.157221, -22.63725, 0.4183159, 14.157221, -22.63725, 1.1745362, 21.256002, -22.63725, 0.25860643, 19.925089, -22.63725, 1.2762094, 19.925089, -22.63725, 1.2762094, 21.256002, -22.63725, 0.25860643, 21.256002, -22.63725, 0.99606276, 19.925089, -22.63725, 1.2762094, 21.256002, -22.63725, 0.99606276, 21.256002, -22.728888, 1.2413001, 12.528709, -22.63725, 1.2413001, 14.157221, -23.00336, 1.4267545, 12.528709, -23.00336, 1.2413001, 14.157221, -23.00336, 1.4267545, 12.528709, -22.63725, 1.2413001, 13.750093, -22.63725, 1.3805003, 14.157221, -23.00336, 1.4267545, 13.750093, -22.63725, 1.3805003, 14.157221, -22.728888, 1.4267545, 14.157221, -22.728888, 1.4267545, 21.256002, -23.00336, 1.2413001, 14.157221, -23.00336, 1.4267545, 14.157221, -22.728888, 1.4267545, 19.925089, -22.63725, 1.2762094, 21.256002, -23.00336, 1.2413001, 21.256002, -23.00336, 1.2413001, 19.925089, -22.63725, 1.2762094, 21.256002, -22.728888, 1.2413001, 19.925089, -22.63725, 1.2762094, 14.157221, -22.728888, 1.4267545, 15.931917, -22.63725, 1.3805003, 21.256002, -22.728888, 1.2413001, 21.256002, -23.00336, 0.25860643, 21.256002, -23.00336, 1.2413001, 21.256002, -23.00336, 0.25860643, 21.256002, -22.728888, 1.2413001, 21.256002, -22.63725, 0.99606276, 21.256002, -23.00336, 0.25860643, 21.256002, -22.63725, 0.99606276, 21.256002, -22.63725, 0.25860643, 17.195192, -22.439577, -16.726889, 21.299639, -22.073904, -16.912344, 17.195192, -22.073904, -16.726889, 17.195192, -22.439577, -16.726889, 21.299639, -22.439577, -16.912344, 21.299639, -22.073904, -16.912344, 12.960274, -22.073904, -16.912344, 17.195192, -22.439577, -16.726889, 17.195192, -22.073904, -16.726889, 12.960274, -22.073904, -16.912344, 12.960274, -22.439577, -16.912344, 17.195192, -22.439577, -16.726889, 17.195192, -22.439577, -15.748123, 21.299639, -22.439577, -16.912344, 17.195192, -22.439577, -16.726889, 17.195192, -22.439577, -15.748123, 21.299639, -22.439577, -16.082378, 21.299639, -22.439577, -16.912344, 17.195192, -22.439577, -16.726889, 12.960274, -22.439577, -15.779104, 17.195192, -22.439577, -15.748123, 17.195192, -22.439577, -16.726889, 12.960274, -22.439577, -16.355106, 12.960274, -22.439577, -15.779104, 12.960274, -22.439577, -16.355106, 17.195192, -22.439577, -16.726889, 12.960274, -22.439577, -16.912344, 21.299639, -22.439577, -16.082378, 21.299639, -22.073904, -16.912344, 21.299639, -22.439577, -16.912344, 21.299639, -22.439577, -16.082378, 21.299639, -22.073904, -16.082378, 21.299639, -22.073904, -16.912344, 12.960274, -22.073904, -16.422306, 13.119982, -22.182995, -16.32456, 12.960274, -22.439577, -16.355106, 17.195192, -22.073904, -15.992487, 17.195192, -22.165104, -15.748123, 16.136572, -22.073904, -15.755539, 17.195192, -22.073904, -15.992487, 18.221523, -22.073904, -15.831467, 17.195192, -22.165104, -15.748123, 17.195192, -22.073904, -15.992487, 21.299639, -22.073904, -16.082378, 18.221523, -22.073904, -15.831467, 17.195192, -22.073904, -15.992487, 21.299639, -22.073904, -16.912344, 21.299639, -22.073904, -16.082378, 21.299639, -22.073904, -16.912344, 17.195192, -22.073904, -15.992487, 17.195192, -22.073904, -16.726889, 16.136572, -22.073904, -15.755539, 17.195192, -22.073904, -16.726889, 17.195192, -22.073904, -15.992487, 17.195192, -22.073904, -16.726889, 12.960274, -22.073904, -16.422306, 12.960274, -22.073904, -16.912344, 17.195192, -22.073904, -16.726889, 13.688129, -22.073904, -16.321941, 12.960274, -22.073904, -16.422306, 16.136572, -22.073904, -15.755539, 13.688129, -22.073904, -16.321941, 17.195192, -22.073904, -16.726889, 13.119982, -22.182995, -16.32456, 12.960274, -22.073904, -16.422306, 13.688129, -22.073904, -16.321941, 12.960274, -22.073904, -16.14565, 13.119982, -22.182995, -16.32456, 13.688129, -22.073904, -16.321941, 13.688129, -22.073904, -16.321941, 16.136572, -22.073904, -15.755539, 12.960274, -22.073904, -16.14565, 12.960274, -22.073904, -16.14565, 16.136572, -22.073904, -15.755539, 12.960274, -22.073904, -15.779104, 12.960274, -22.439577, -16.912344, 12.960274, -22.073904, -16.422306, 12.960274, -22.439577, -16.355106, 12.960274, -22.439577, -16.912344, 12.960274, -22.073904, -16.912344, 12.960274, -22.073904, -16.422306, 12.960274, -22.073904, -16.14565, 12.960274, -22.439577, -15.779104, 12.960274, -22.439577, -16.355106, 12.960274, -22.073904, -16.14565, 12.960274, -22.073904, -15.779104, 12.960274, -22.439577, -15.779104, 13.119982, -22.182995, -16.32456, 12.960274, -22.073904, -16.14565, 12.960274, -22.439577, -16.355106, 12.960274, -22.073904, -15.779104, 17.195192, -22.439577, -15.748123, 12.960274, -22.439577, -15.779104, 17.195192, -22.439577, -15.748123, 12.960274, -22.073904, -15.779104, 16.136572, -22.073904, -15.755539, 17.195192, -22.439577, -15.748123, 16.136572, -22.073904, -15.755539, 17.195192, -22.165104, -15.748123, 17.195192, -22.165104, -15.748123, 21.299639, -22.439577, -16.082378, 17.195192, -22.439577, -15.748123, 17.195192, -22.165104, -15.748123, 21.299639, -22.073904, -16.082378, 21.299639, -22.439577, -16.082378, 21.299639, -22.073904, -16.082378, 17.195192, -22.165104, -15.748123, 18.221523, -22.073904, -15.831467, 20.39811, -22.073904, -13.344625, 21.299639, -22.439577, -13.325426, 20.39811, -22.439577, -13.344625, 20.39811, -22.073904, -13.344625, 21.299639, -22.073904, -13.325426, 21.299639, -22.439577, -13.325426, 20.39811, -22.439577, -13.344625, 14.819624, -22.073904, -13.330225, 20.39811, -22.073904, -13.344625, 20.39811, -22.439577, -13.344625, 12.960274, -22.165104, -13.325426, 14.819624, -22.073904, -13.330225, 12.960274, -22.165104, -13.325426, 20.39811, -22.439577, -13.344625, 12.960274, -22.439577, -13.325426, 21.299639, -22.073904, -14.308119, 20.39811, -22.073904, -13.344625, 20.39811, -22.073904, -14.331245, 14.819624, -22.073904, -13.330225, 20.39811, -22.073904, -14.331245, 20.39811, -22.073904, -13.344625, 14.819624, -22.073904, -13.330225, 12.960274, -22.073904, -14.154955, 20.39811, -22.073904, -14.331245, 12.960274, -22.073904, -14.154955, 14.819624, -22.073904, -13.330225, 12.960274, -22.073904, -13.5327, 12.960274, -22.073904, -13.5327, 14.819624, -22.073904, -13.330225, 12.960274, -22.165104, -13.325426, 20.39811, -22.073904, -13.344625, 21.299639, -22.073904, -14.308119, 20.818766, -22.073904, -13.7901535, 20.818766, -22.073904, -13.7901535, 21.299639, -22.073904, -14.308119, 21.299639, -22.073904, -13.9119, 21.299639, -22.073904, -13.9119, 21.217165, -22.213541, -13.803246, 20.818766, -22.073904, -13.7901535, 21.217165, -22.213541, -13.803246, 21.299639, -22.073904, -13.6383, 20.818766, -22.073904, -13.7901535, 20.39811, -22.073904, -13.344625, 20.818766, -22.073904, -13.7901535, 21.299639, -22.073904, -13.6383, 20.39811, -22.073904, -13.344625, 21.299639, -22.073904, -13.6383, 21.299639, -22.073904, -13.325426, 20.39811, -22.439577, -14.331245, 21.299639, -22.073904, -14.308119, 20.39811, -22.073904, -14.331245, 20.39811, -22.439577, -14.331245, 21.299639, -22.439577, -14.308119, 21.299639, -22.073904, -14.308119, 12.960274, -22.073904, -14.154955, 20.39811, -22.439577, -14.331245, 20.39811, -22.073904, -14.331245, 12.960274, -22.073904, -14.154955, 12.960274, -22.439577, -14.154955, 20.39811, -22.439577, -14.331245, 21.299639, -22.439577, -13.325426, 20.39811, -22.439577, -14.331245, 20.39811, -22.439577, -13.344625, 20.39811, -22.439577, -14.331245, 21.299639, -22.439577, -13.325426, 21.299639, -22.439577, -13.856482, 20.39811, -22.439577, -14.331245, 21.299639, -22.439577, -13.856482, 21.299639, -22.439577, -14.308119, 12.960274, -22.439577, -14.154955, 20.39811, -22.439577, -13.344625, 20.39811, -22.439577, -14.331245, 12.960274, -22.439577, -14.154955, 12.960274, -22.439577, -13.325426, 20.39811, -22.439577, -13.344625, 21.299639, -22.073904, -13.6383, 21.217165, -22.213541, -13.803246, 21.299639, -22.439577, -13.856482, 21.217165, -22.213541, -13.803246, 21.299639, -22.073904, -13.9119, 21.299639, -22.439577, -13.856482, 21.299639, -22.439577, -13.325426, 21.299639, -22.073904, -13.6383, 21.299639, -22.439577, -13.856482, 21.299639, -22.439577, -13.325426, 21.299639, -22.073904, -13.325426, 21.299639, -22.073904, -13.6383, 21.299639, -22.073904, -13.9119, 21.299639, -22.439577, -14.308119, 21.299639, -22.439577, -13.856482, 21.299639, -22.073904, -13.9119, 21.299639, -22.073904, -14.308119, 21.299639, -22.439577, -14.308119, 12.960274, -22.073904, -14.154955, 12.960274, -22.439577, -13.325426, 12.960274, -22.439577, -14.154955, 12.960274, -22.439577, -13.325426, 12.960274, -22.073904, -14.154955, 12.960274, -22.073904, -13.5327, 12.960274, -22.439577, -13.325426, 12.960274, -22.073904, -13.5327, 12.960274, -22.165104, -13.325426, 13.295837, -22.635942, 4.8980365, 13.527548, -22.543432, 4.8980365, 13.527548, -22.635942, 4.8980365, 13.295837, -22.635942, 4.8980365, 13.295837, -22.543432, 4.8980365, 13.527548, -22.543432, 4.8980365, 13.295837, -22.543432, 4.8980365, 13.527548, -22.543432, -19.742168, 13.527548, -22.543432, 4.8980365, 13.295837, -22.543432, 4.8980365, 13.295837, -22.543432, -19.742168, 13.527548, -22.543432, -19.742168, 13.295837, -22.543432, -19.742168, 13.527548, -22.635942, -19.742168, 13.527548, -22.543432, -19.742168, 13.295837, -22.543432, -19.742168, 13.295837, -22.635942, -19.742168, 13.527548, -22.635942, -19.742168, 13.295837, -22.635942, -19.742168, 13.527548, -22.635942, 4.8980365, 13.527548, -22.635942, -19.742168, 13.295837, -22.635942, -19.742168, 13.295837, -22.635942, 4.8980365, 13.527548, -22.635942, 4.8980365, 13.527548, -22.635942, 4.8980365, 13.527548, -22.543432, -19.742168, 13.527548, -22.635942, -19.742168, 13.527548, -22.635942, 4.8980365, 13.527548, -22.543432, 4.8980365, 13.527548, -22.543432, -19.742168, 13.295837, -22.635942, -19.742168, 13.295837, -22.543432, 4.8980365, 13.295837, -22.635942, 4.8980365, 13.295837, -22.635942, -19.742168, 13.295837, -22.543432, -19.742168, 13.295837, -22.543432, 4.8980365, 14.33395, -22.439577, -18.07831, 21.590258, -22.073904, -18.237583, 14.33395, -22.073904, -18.07831, 14.33395, -22.439577, -18.07831, 21.590258, -22.439577, -18.237583, 21.590258, -22.073904, -18.237583, 12.669218, -22.073904, -18.084854, 14.33395, -22.439577, -18.07831, 14.33395, -22.073904, -18.07831, 12.669218, -22.073904, -18.084854, 12.669218, -22.439577, -18.084854, 14.33395, -22.439577, -18.07831, 14.33395, -22.439577, -17.069435, 21.590258, -22.439577, -18.237583, 14.33395, -22.439577, -18.07831, 14.33395, -22.439577, -17.069435, 21.590258, -22.439577, -17.25489, 21.590258, -22.439577, -18.237583, 12.669218, -22.439577, -18.084854, 14.33395, -22.439577, -17.069435, 14.33395, -22.439577, -18.07831, 12.669218, -22.439577, -18.084854, 12.669218, -22.439577, -17.25489, 14.33395, -22.439577, -17.069435, 12.669218, -22.439577, -18.084854, 12.669218, -22.073904, -17.25489, 12.669218, -22.439577, -17.25489, 12.669218, -22.439577, -18.084854, 12.669218, -22.073904, -18.084854, 12.669218, -22.073904, -17.25489, 14.33395, -22.073904, -17.321653, 14.33395, -22.165104, -17.069435, 13.917658, -22.073904, -17.115688, 14.33395, -22.073904, -17.321653, 16.147917, -22.073904, -17.115688, 14.33395, -22.165104, -17.069435, 14.33395, -22.073904, -17.321653, 20.229671, -22.073904, -17.220417, 16.147917, -22.073904, -17.115688, 13.917658, -22.073904, -17.115688, 14.33395, -22.073904, -18.07831, 14.33395, -22.073904, -17.321653, 13.917658, -22.073904, -17.115688, 12.669218, -22.073904, -18.084854, 14.33395, -22.073904, -18.07831, 12.669218, -22.073904, -18.084854, 13.917658, -22.073904, -17.115688, 12.669218, -22.073904, -17.25489, 21.590258, -22.073904, -18.237583, 14.33395, -22.073904, -17.321653, 14.33395, -22.073904, -18.07831, 14.33395, -22.073904, -17.321653, 21.590258, -22.073904, -18.237583, 20.229671, -22.073904, -17.220417, 20.229671, -22.073904, -17.220417, 21.590258, -22.073904, -18.237583, 21.590258, -22.073904, -17.501, 20.229671, -22.073904, -17.220417, 21.590258, -22.073904, -17.501, 21.590258, -22.165104, -17.25489, 12.669218, -22.073904, -17.25489, 14.33395, -22.439577, -17.069435, 12.669218, -22.439577, -17.25489, 14.33395, -22.439577, -17.069435, 12.669218, -22.073904, -17.25489, 13.917658, -22.073904, -17.115688, 14.33395, -22.439577, -17.069435, 13.917658, -22.073904, -17.115688, 14.33395, -22.165104, -17.069435, 14.33395, -22.165104, -17.069435, 21.590258, -22.439577, -17.25489, 14.33395, -22.439577, -17.069435, 14.33395, -22.165104, -17.069435, 20.229671, -22.073904, -17.220417, 21.590258, -22.439577, -17.25489, 21.590258, -22.439577, -17.25489, 20.229671, -22.073904, -17.220417, 21.590258, -22.165104, -17.25489, 20.229671, -22.073904, -17.220417, 14.33395, -22.165104, -17.069435, 16.147917, -22.073904, -17.115688, 21.590258, -22.165104, -17.25489, 21.590258, -22.439577, -18.237583, 21.590258, -22.439577, -17.25489, 21.590258, -22.439577, -18.237583, 21.590258, -22.165104, -17.25489, 21.590258, -22.073904, -17.501, 21.590258, -22.439577, -18.237583, 21.590258, -22.073904, -17.501, 21.590258, -22.073904, -18.237583, 12.749508, -25.90344, -19.752205, 13.527111, -21.174994, -19.579403, 13.354311, -25.90344, -19.752205, 12.749508, -25.90344, -19.752205, 12.57671, -21.174994, -19.579403, 13.527111, -21.174994, -19.579403, 12.57671, -21.174994, -19.579403, 13.527111, -21.174994, -20.52937, 13.527111, -21.174994, -19.579403, 12.57671, -21.174994, -19.579403, 12.57671, -21.174994, -20.52937, 13.527111, -21.174994, -20.52937, 12.57671, -21.174994, -20.52937, 13.354311, -25.90344, -20.356571, 13.527111, -21.174994, -20.52937, 12.57671, -21.174994, -20.52937, 12.749508, -25.90344, -20.356571, 13.354311, -25.90344, -20.356571, 12.749508, -25.90344, -20.356571, 13.354311, -25.90344, -19.752205, 13.354311, -25.90344, -20.356571, 12.749508, -25.90344, -20.356571, 12.749508, -25.90344, -19.752205, 13.354311, -25.90344, -19.752205, 13.354311, -25.90344, -19.752205, 13.527111, -21.174994, -20.52937, 13.354311, -25.90344, -20.356571, 13.354311, -25.90344, -19.752205, 13.527111, -21.174994, -19.579403, 13.527111, -21.174994, -20.52937, 12.749508, -25.90344, -20.356571, 12.57671, -21.174994, -19.579403, 12.749508, -25.90344, -19.752205, 12.749508, -25.90344, -20.356571, 12.57671, -21.174994, -20.52937, 12.57671, -21.174994, -19.579403, 20.39811, -22.073904, -18.576202, 21.299639, -22.439577, -18.556564, 20.39811, -22.439577, -18.576202, 20.39811, -22.073904, -18.576202, 21.299639, -22.073904, -18.556564, 21.299639, -22.439577, -18.556564, 20.39811, -22.439577, -18.576202, 14.819624, -22.073904, -18.561365, 20.39811, -22.073904, -18.576202, 20.39811, -22.439577, -18.576202, 12.960274, -22.165104, -18.556564, 14.819624, -22.073904, -18.561365, 12.960274, -22.165104, -18.556564, 20.39811, -22.439577, -18.576202, 12.960274, -22.439577, -18.556564, 21.299639, -22.073904, -19.539696, 20.39811, -22.073904, -18.576202, 20.39811, -22.073904, -19.562386, 14.819624, -22.073904, -18.561365, 20.39811, -22.073904, -19.562386, 20.39811, -22.073904, -18.576202, 14.819624, -22.073904, -18.561365, 12.960274, -22.073904, -19.386532, 20.39811, -22.073904, -19.562386, 12.960274, -22.073904, -19.386532, 14.819624, -22.073904, -18.561365, 12.960274, -22.073904, -18.764275, 12.960274, -22.073904, -18.764275, 14.819624, -22.073904, -18.561365, 12.960274, -22.165104, -18.556564, 20.39811, -22.073904, -18.576202, 21.299639, -22.073904, -19.539696, 20.818766, -22.073904, -19.022167, 20.818766, -22.073904, -19.022167, 21.299639, -22.073904, -19.539696, 21.299639, -22.073904, -19.143913, 21.299639, -22.073904, -19.143913, 21.217165, -22.213541, -19.034386, 20.818766, -22.073904, -19.022167, 21.217165, -22.213541, -19.034386, 21.299639, -22.073904, -18.869438, 20.818766, -22.073904, -19.022167, 20.39811, -22.073904, -18.576202, 20.818766, -22.073904, -19.022167, 21.299639, -22.073904, -18.869438, 20.39811, -22.073904, -18.576202, 21.299639, -22.073904, -18.869438, 21.299639, -22.073904, -18.556564, 20.39811, -22.439577, -19.562386, 21.299639, -22.073904, -19.539696, 20.39811, -22.073904, -19.562386, 20.39811, -22.439577, -19.562386, 21.299639, -22.439577, -19.539696, 21.299639, -22.073904, -19.539696, 12.960274, -22.073904, -19.386532, 20.39811, -22.439577, -19.562386, 20.39811, -22.073904, -19.562386, 12.960274, -22.073904, -19.386532, 12.960274, -22.439577, -19.386532, 20.39811, -22.439577, -19.562386, 21.299639, -22.439577, -18.556564, 20.39811, -22.439577, -19.562386, 20.39811, -22.439577, -18.576202, 20.39811, -22.439577, -19.562386, 21.299639, -22.439577, -18.556564, 21.299639, -22.439577, -19.088493, 20.39811, -22.439577, -19.562386, 21.299639, -22.439577, -19.088493, 21.299639, -22.439577, -19.539696, 12.960274, -22.439577, -19.386532, 20.39811, -22.439577, -18.576202, 20.39811, -22.439577, -19.562386, 12.960274, -22.439577, -19.386532, 12.960274, -22.439577, -18.556564, 20.39811, -22.439577, -18.576202, 21.299639, -22.073904, -18.869438, 21.217165, -22.213541, -19.034386, 21.299639, -22.439577, -19.088493, 21.217165, -22.213541, -19.034386, 21.299639, -22.073904, -19.143913, 21.299639, -22.439577, -19.088493, 21.299639, -22.439577, -18.556564, 21.299639, -22.073904, -18.869438, 21.299639, -22.439577, -19.088493, 21.299639, -22.439577, -18.556564, 21.299639, -22.073904, -18.556564, 21.299639, -22.073904, -18.869438, 21.299639, -22.073904, -19.143913, 21.299639, -22.439577, -19.539696, 21.299639, -22.439577, -19.088493, 21.299639, -22.073904, -19.143913, 21.299639, -22.073904, -19.539696, 21.299639, -22.439577, -19.539696, 12.960274, -22.073904, -19.386532, 12.960274, -22.439577, -18.556564, 12.960274, -22.439577, -19.386532, 12.960274, -22.439577, -18.556564, 12.960274, -22.073904, -19.386532, 12.960274, -22.073904, -18.764275, 12.960274, -22.439577, -18.556564, 12.960274, -22.073904, -18.764275, 12.960274, -22.165104, -18.556564, 20.62589, -22.073904, -12.164696, 21.590258, -22.439577, -12.14506, 20.62589, -22.439577, -12.164696, 20.62589, -22.073904, -12.164696, 21.590258, -22.073904, -12.14506, 21.590258, -22.439577, -12.14506, 20.62589, -22.439577, -12.164696, 14.658169, -22.073904, -12.149858, 20.62589, -22.073904, -12.164696, 20.62589, -22.439577, -12.164696, 12.669218, -22.165104, -12.14506, 14.658169, -22.073904, -12.149858, 12.669218, -22.165104, -12.14506, 20.62589, -22.439577, -12.164696, 12.669218, -22.439577, -12.14506, 21.590258, -22.073904, -13.127752, 20.62589, -22.073904, -12.164696, 20.62589, -22.073904, -13.150879, 14.658169, -22.073904, -12.149858, 20.62589, -22.073904, -13.150879, 20.62589, -22.073904, -12.164696, 14.658169, -22.073904, -12.149858, 12.669218, -22.073904, -12.975025, 20.62589, -22.073904, -13.150879, 12.669218, -22.073904, -12.975025, 14.658169, -22.073904, -12.149858, 12.669218, -22.073904, -12.352768, 12.669218, -22.073904, -12.352768, 14.658169, -22.073904, -12.149858, 12.669218, -22.165104, -12.14506, 20.62589, -22.073904, -12.164696, 21.590258, -22.073904, -13.127752, 21.07622, -22.073904, -12.610224, 21.07622, -22.073904, -12.610224, 21.590258, -22.073904, -13.127752, 21.590258, -22.073904, -12.73197, 21.590258, -22.073904, -12.73197, 21.502111, -22.213541, -12.622879, 21.07622, -22.073904, -12.610224, 21.502111, -22.213541, -12.622879, 21.590258, -22.073904, -12.457933, 21.07622, -22.073904, -12.610224, 20.62589, -22.073904, -12.164696, 21.07622, -22.073904, -12.610224, 21.590258, -22.073904, -12.457933, 20.62589, -22.073904, -12.164696, 21.590258, -22.073904, -12.457933, 21.590258, -22.073904, -12.14506, 20.62589, -22.439577, -13.150879, 21.590258, -22.073904, -13.127752, 20.62589, -22.073904, -13.150879, 20.62589, -22.439577, -13.150879, 21.590258, -22.439577, -13.127752, 21.590258, -22.073904, -13.127752, 12.669218, -22.073904, -12.975025, 20.62589, -22.439577, -13.150879, 20.62589, -22.073904, -13.150879, 12.669218, -22.073904, -12.975025, 12.669218, -22.439577, -12.975025, 20.62589, -22.439577, -13.150879, 21.590258, -22.439577, -12.14506, 20.62589, -22.439577, -13.150879, 20.62589, -22.439577, -12.164696, 20.62589, -22.439577, -13.150879, 21.590258, -22.439577, -12.14506, 21.590258, -22.439577, -12.676552, 20.62589, -22.439577, -13.150879, 21.590258, -22.439577, -12.676552, 21.590258, -22.439577, -13.127752, 12.669218, -22.439577, -12.975025, 20.62589, -22.439577, -12.164696, 20.62589, -22.439577, -13.150879, 12.669218, -22.439577, -12.975025, 12.669218, -22.439577, -12.14506, 20.62589, -22.439577, -12.164696, 21.590258, -22.073904, -12.457933, 21.502111, -22.213541, -12.622879, 21.590258, -22.439577, -12.676552, 21.502111, -22.213541, -12.622879, 21.590258, -22.073904, -12.73197, 21.590258, -22.439577, -12.676552, 21.590258, -22.439577, -12.14506, 21.590258, -22.073904, -12.457933, 21.590258, -22.439577, -12.676552, 21.590258, -22.439577, -12.14506, 21.590258, -22.073904, -12.14506, 21.590258, -22.073904, -12.457933, 21.590258, -22.073904, -12.73197, 21.590258, -22.439577, -13.127752, 21.590258, -22.439577, -12.676552, 21.590258, -22.073904, -12.73197, 21.590258, -22.073904, -13.127752, 21.590258, -22.439577, -13.127752, 12.669218, -22.073904, -12.975025, 12.669218, -22.439577, -12.14506, 12.669218, -22.439577, -12.975025, 12.669218, -22.439577, -12.14506, 12.669218, -22.073904, -12.975025, 12.669218, -22.073904, -12.352768, 12.669218, -22.439577, -12.14506, 12.669218, -22.073904, -12.352768, 12.669218, -22.165104, -12.14506, 17.199993, -22.439577, -11.627094, 21.590258, -22.073904, -11.812113, 17.199993, -22.073904, -11.627094, 17.199993, -22.439577, -11.627094, 21.590258, -22.439577, -11.812113, 21.590258, -22.073904, -11.812113, 12.669218, -22.073904, -11.812113, 17.199993, -22.439577, -11.627094, 17.199993, -22.073904, -11.627094, 12.669218, -22.073904, -11.812113, 12.669218, -22.439577, -11.812113, 17.199993, -22.439577, -11.627094, 17.199993, -22.439577, -10.647455, 21.590258, -22.439577, -11.812113, 17.199993, -22.439577, -11.627094, 17.199993, -22.439577, -10.647455, 21.590258, -22.439577, -10.982147, 21.590258, -22.439577, -11.812113, 17.199993, -22.439577, -11.627094, 12.669218, -22.439577, -10.678438, 17.199993, -22.439577, -10.647455, 17.199993, -22.439577, -11.627094, 12.669218, -22.439577, -11.254438, 12.669218, -22.439577, -10.678438, 12.669218, -22.439577, -11.254438, 17.199993, -22.439577, -11.627094, 12.669218, -22.439577, -11.812113, 21.590258, -22.439577, -10.982147, 21.590258, -22.073904, -11.812113, 21.590258, -22.439577, -11.812113, 21.590258, -22.439577, -10.982147, 21.590258, -22.073904, -10.982147, 21.590258, -22.073904, -11.812113, 12.669218, -22.073904, -11.32164, 12.840273, -22.182995, -11.224329, 12.669218, -22.439577, -11.254438, 17.199993, -22.073904, -10.892256, 17.199993, -22.165104, -10.647455, 16.06719, -22.073904, -10.65531, 17.199993, -22.073904, -10.892256, 18.29745, -22.073904, -10.731237, 17.199993, -22.165104, -10.647455, 17.199993, -22.073904, -10.892256, 21.590258, -22.073904, -10.982147, 18.29745, -22.073904, -10.731237, 17.199993, -22.073904, -10.892256, 21.590258, -22.073904, -11.812113, 21.590258, -22.073904, -10.982147, 21.590258, -22.073904, -11.812113, 17.199993, -22.073904, -10.892256, 17.199993, -22.073904, -11.627094, 16.06719, -22.073904, -10.65531, 17.199993, -22.073904, -11.627094, 17.199993, -22.073904, -10.892256, 17.199993, -22.073904, -11.627094, 12.669218, -22.073904, -11.32164, 12.669218, -22.073904, -11.812113, 17.199993, -22.073904, -11.627094, 13.447693, -22.073904, -11.221275, 12.669218, -22.073904, -11.32164, 16.06719, -22.073904, -10.65531, 13.447693, -22.073904, -11.221275, 17.199993, -22.073904, -11.627094, 12.840273, -22.182995, -11.224329, 12.669218, -22.073904, -11.32164, 13.447693, -22.073904, -11.221275, 12.669218, -22.073904, -11.044985, 12.840273, -22.182995, -11.224329, 13.447693, -22.073904, -11.221275, 13.447693, -22.073904, -11.221275, 16.06719, -22.073904, -10.65531, 12.669218, -22.073904, -11.044985, 12.669218, -22.073904, -11.044985, 16.06719, -22.073904, -10.65531, 12.669218, -22.073904, -10.678438, 12.669218, -22.439577, -11.812113, 12.669218, -22.073904, -11.32164, 12.669218, -22.439577, -11.254438, 12.669218, -22.439577, -11.812113, 12.669218, -22.073904, -11.812113, 12.669218, -22.073904, -11.32164, 12.669218, -22.073904, -11.044985, 12.669218, -22.439577, -10.678438, 12.669218, -22.439577, -11.254438, 12.669218, -22.073904, -11.044985, 12.669218, -22.073904, -10.678438, 12.669218, -22.439577, -10.678438, 12.840273, -22.182995, -11.224329, 12.669218, -22.073904, -11.044985, 12.669218, -22.439577, -11.254438, 12.669218, -22.073904, -10.678438, 17.199993, -22.439577, -10.647455, 12.669218, -22.439577, -10.678438, 17.199993, -22.439577, -10.647455, 12.669218, -22.073904, -10.678438, 16.06719, -22.073904, -10.65531, 17.199993, -22.439577, -10.647455, 16.06719, -22.073904, -10.65531, 17.199993, -22.165104, -10.647455, 17.199993, -22.165104, -10.647455, 21.590258, -22.439577, -10.982147, 17.199993, -22.439577, -10.647455, 17.199993, -22.165104, -10.647455, 21.590258, -22.073904, -10.982147, 21.590258, -22.439577, -10.982147, 21.590258, -22.073904, -10.982147, 17.199993, -22.165104, -10.647455, 18.29745, -22.073904, -10.731237, 20.39811, -22.073904, -9.367163, 21.299639, -22.439577, -9.347525, 20.39811, -22.439577, -9.367163, 20.39811, -22.073904, -9.367163, 21.299639, -22.073904, -9.347525, 21.299639, -22.439577, -9.347525, 20.39811, -22.439577, -9.367163, 14.819624, -22.073904, -9.352325, 20.39811, -22.073904, -9.367163, 20.39811, -22.439577, -9.367163, 12.960274, -22.165104, -9.347525, 14.819624, -22.073904, -9.352325, 12.960274, -22.165104, -9.347525, 20.39811, -22.439577, -9.367163, 12.960274, -22.439577, -9.347525, 21.299639, -22.073904, -10.330656, 20.39811, -22.073904, -9.367163, 20.39811, -22.073904, -10.353346, 14.819624, -22.073904, -9.352325, 20.39811, -22.073904, -10.353346, 20.39811, -22.073904, -9.367163, 14.819624, -22.073904, -9.352325, 12.960274, -22.073904, -10.17749, 20.39811, -22.073904, -10.353346, 12.960274, -22.073904, -10.17749, 14.819624, -22.073904, -9.352325, 12.960274, -22.073904, -9.555235, 12.960274, -22.073904, -9.555235, 14.819624, -22.073904, -9.352325, 12.960274, -22.165104, -9.347525, 20.39811, -22.073904, -9.367163, 21.299639, -22.073904, -10.330656, 20.818766, -22.073904, -9.8131275, 20.818766, -22.073904, -9.8131275, 21.299639, -22.073904, -10.330656, 21.299639, -22.073904, -9.934872, 21.299639, -22.073904, -9.934872, 21.217165, -22.213541, -9.825344, 20.818766, -22.073904, -9.8131275, 21.217165, -22.213541, -9.825344, 21.299639, -22.073904, -9.6603985, 20.818766, -22.073904, -9.8131275, 20.39811, -22.073904, -9.367163, 20.818766, -22.073904, -9.8131275, 21.299639, -22.073904, -9.6603985, 20.39811, -22.073904, -9.367163, 21.299639, -22.073904, -9.6603985, 21.299639, -22.073904, -9.347525, 20.39811, -22.439577, -10.353346, 21.299639, -22.073904, -10.330656, 20.39811, -22.073904, -10.353346, 20.39811, -22.439577, -10.353346, 21.299639, -22.439577, -10.330656, 21.299639, -22.073904, -10.330656, 12.960274, -22.073904, -10.17749, 20.39811, -22.439577, -10.353346, 20.39811, -22.073904, -10.353346, 12.960274, -22.073904, -10.17749, 12.960274, -22.439577, -10.17749, 20.39811, -22.439577, -10.353346, 21.299639, -22.439577, -9.347525, 20.39811, -22.439577, -10.353346, 20.39811, -22.439577, -9.367163, 20.39811, -22.439577, -10.353346, 21.299639, -22.439577, -9.347525, 21.299639, -22.439577, -9.879017, 20.39811, -22.439577, -10.353346, 21.299639, -22.439577, -9.879017, 21.299639, -22.439577, -10.330656, 12.960274, -22.439577, -10.17749, 20.39811, -22.439577, -9.367163, 20.39811, -22.439577, -10.353346, 12.960274, -22.439577, -10.17749, 12.960274, -22.439577, -9.347525, 20.39811, -22.439577, -9.367163, 21.299639, -22.073904, -9.6603985, 21.217165, -22.213541, -9.825344, 21.299639, -22.439577, -9.879017, 21.217165, -22.213541, -9.825344, 21.299639, -22.073904, -9.934872, 21.299639, -22.439577, -9.879017, 21.299639, -22.439577, -9.347525, 21.299639, -22.073904, -9.6603985, 21.299639, -22.439577, -9.879017, 21.299639, -22.439577, -9.347525, 21.299639, -22.073904, -9.347525, 21.299639, -22.073904, -9.6603985, 21.299639, -22.073904, -9.934872, 21.299639, -22.439577, -10.330656, 21.299639, -22.439577, -9.879017, 21.299639, -22.073904, -9.934872, 21.299639, -22.073904, -10.330656, 21.299639, -22.439577, -10.330656, 12.960274, -22.073904, -10.17749, 12.960274, -22.439577, -9.347525, 12.960274, -22.439577, -10.17749, 12.960274, -22.439577, -9.347525, 12.960274, -22.073904, -10.17749, 12.960274, -22.073904, -9.555235, 12.960274, -22.439577, -9.347525, 12.960274, -22.073904, -9.555235, 12.960274, -22.165104, -9.347525, 20.474909, -25.90344, -19.689804, 21.125528, -21.174994, -19.768787, 21.204512, -25.90344, -19.689804, 20.474909, -25.90344, -19.689804, 20.553892, -21.174994, -19.768787, 21.125528, -21.174994, -19.768787, 20.553892, -21.174994, -19.768787, 21.125528, -21.174994, -20.340424, 21.125528, -21.174994, -19.768787, 20.553892, -21.174994, -19.768787, 20.553892, -21.174994, -20.340424, 21.125528, -21.174994, -20.340424, 20.553892, -21.174994, -20.340424, 21.204512, -25.90344, -20.419407, 21.125528, -21.174994, -20.340424, 20.553892, -21.174994, -20.340424, 20.474909, -25.90344, -20.419407, 21.204512, -25.90344, -20.419407, 20.474909, -25.90344, -20.419407, 21.204512, -25.90344, -19.689804, 21.204512, -25.90344, -20.419407, 20.474909, -25.90344, -20.419407, 20.474909, -25.90344, -19.689804, 21.204512, -25.90344, -19.689804, 21.204512, -25.90344, -19.689804, 21.125528, -21.174994, -20.340424, 21.204512, -25.90344, -20.419407, 21.204512, -25.90344, -19.689804, 21.125528, -21.174994, -19.768787, 21.125528, -21.174994, -20.340424, 20.474909, -25.90344, -20.419407, 20.553892, -21.174994, -19.768787, 20.474909, -25.90344, -19.689804, 20.474909, -25.90344, -20.419407, 20.553892, -21.174994, -20.340424, 20.553892, -21.174994, -19.768787, 12.886963, -25.90344, 5.2994914, 13.6170025, -21.174994, 5.2994914, 13.6170025, -25.90344, 5.2994914, 12.886963, -25.90344, 5.2994914, 12.886963, -21.174994, 5.2994914, 13.6170025, -21.174994, 5.2994914, 12.886963, -25.90344, 4.5698895, 13.6170025, -25.90344, 5.2994914, 13.6170025, -25.90344, 4.5698895, 12.886963, -25.90344, 4.5698895, 12.886963, -25.90344, 5.2994914, 13.6170025, -25.90344, 5.2994914, 13.6170025, -25.90344, 5.2994914, 13.6170025, -21.174994, 4.5698895, 13.6170025, -25.90344, 4.5698895, 13.6170025, -25.90344, 5.2994914, 13.6170025, -21.174994, 5.2994914, 13.6170025, -21.174994, 4.5698895, 12.886963, -22.357105, 4.5698895, 13.069365, -21.174994, 4.5698895, 12.886963, -21.174994, 4.7522902, 12.886963, -25.90344, 5.2994914, 12.886963, -22.357105, 4.5698895, 12.886963, -21.174994, 4.7522902, 12.886963, -22.357105, 4.5698895, 12.886963, -25.90344, 5.2994914, 12.886963, -25.90344, 4.5698895, 12.886963, -25.90344, 5.2994914, 12.886963, -21.174994, 4.7522902, 12.886963, -21.174994, 5.2994914, 12.886963, -21.174994, 4.7522902, 13.6170025, -21.174994, 5.2994914, 12.886963, -21.174994, 5.2994914, 13.6170025, -21.174994, 5.2994914, 12.886963, -21.174994, 4.7522902, 13.069365, -21.174994, 4.5698895, 13.6170025, -21.174994, 5.2994914, 13.069365, -21.174994, 4.5698895, 13.6170025, -21.174994, 4.5698895, 12.886963, -25.90344, 4.5698895, 13.069365, -21.174994, 4.5698895, 12.886963, -22.357105, 4.5698895, 13.069365, -21.174994, 4.5698895, 12.886963, -25.90344, 4.5698895, 13.6170025, -25.90344, 4.5698895, 13.069365, -21.174994, 4.5698895, 13.6170025, -25.90344, 4.5698895, 13.6170025, -21.174994, 4.5698895, 17.195192, -22.439577, -7.5178485, 21.299639, -22.073904, -7.703304, 17.195192, -22.073904, -7.5178485, 17.195192, -22.439577, -7.5178485, 21.299639, -22.439577, -7.703304, 21.299639, -22.073904, -7.703304, 12.960274, -22.073904, -7.703304, 17.195192, -22.439577, -7.5178485, 17.195192, -22.073904, -7.5178485, 12.960274, -22.073904, -7.703304, 12.960274, -22.439577, -7.703304, 17.195192, -22.439577, -7.5178485, 17.195192, -22.439577, -6.539082, 21.299639, -22.439577, -7.703304, 17.195192, -22.439577, -7.5178485, 17.195192, -22.439577, -6.539082, 21.299639, -22.439577, -6.873338, 21.299639, -22.439577, -7.703304, 17.195192, -22.439577, -7.5178485, 12.960274, -22.439577, -6.570064, 17.195192, -22.439577, -6.539082, 17.195192, -22.439577, -7.5178485, 12.960274, -22.439577, -7.146065, 12.960274, -22.439577, -6.570064, 12.960274, -22.439577, -7.146065, 17.195192, -22.439577, -7.5178485, 12.960274, -22.439577, -7.703304, 21.299639, -22.439577, -6.873338, 21.299639, -22.073904, -7.703304, 21.299639, -22.439577, -7.703304, 21.299639, -22.439577, -6.873338, 21.299639, -22.073904, -6.873338, 21.299639, -22.073904, -7.703304, 12.960274, -22.073904, -7.2123933, 13.119982, -22.182995, -7.11552, 12.960274, -22.439577, -7.146065, 17.195192, -22.073904, -6.783447, 17.195192, -22.165104, -6.539082, 16.136572, -22.073904, -6.5465007, 17.195192, -22.073904, -6.783447, 18.221523, -22.073904, -6.6224284, 17.195192, -22.165104, -6.539082, 17.195192, -22.073904, -6.783447, 21.299639, -22.073904, -6.873338, 18.221523, -22.073904, -6.6224284, 17.195192, -22.073904, -6.783447, 21.299639, -22.073904, -7.703304, 21.299639, -22.073904, -6.873338, 21.299639, -22.073904, -7.703304, 17.195192, -22.073904, -6.783447, 17.195192, -22.073904, -7.5178485, 16.136572, -22.073904, -6.5465007, 17.195192, -22.073904, -7.5178485, 17.195192, -22.073904, -6.783447, 17.195192, -22.073904, -7.5178485, 12.960274, -22.073904, -7.2123933, 12.960274, -22.073904, -7.703304, 17.195192, -22.073904, -7.5178485, 13.688129, -22.073904, -7.112902, 12.960274, -22.073904, -7.2123933, 16.136572, -22.073904, -6.5465007, 13.688129, -22.073904, -7.112902, 17.195192, -22.073904, -7.5178485, 13.119982, -22.182995, -7.11552, 12.960274, -22.073904, -7.2123933, 13.688129, -22.073904, -7.112902, 12.960274, -22.073904, -6.9366107, 13.119982, -22.182995, -7.11552, 13.688129, -22.073904, -7.112902, 13.688129, -22.073904, -7.112902, 16.136572, -22.073904, -6.5465007, 12.960274, -22.073904, -6.9366107, 12.960274, -22.073904, -6.9366107, 16.136572, -22.073904, -6.5465007, 12.960274, -22.073904, -6.570064, 12.960274, -22.439577, -7.703304, 12.960274, -22.073904, -7.2123933, 12.960274, -22.439577, -7.146065, 12.960274, -22.439577, -7.703304, 12.960274, -22.073904, -7.703304, 12.960274, -22.073904, -7.2123933, 12.960274, -22.073904, -6.9366107, 12.960274, -22.439577, -6.570064, 12.960274, -22.439577, -7.146065, 12.960274, -22.073904, -6.9366107, 12.960274, -22.073904, -6.570064, 12.960274, -22.439577, -6.570064, 13.119982, -22.182995, -7.11552, 12.960274, -22.073904, -6.9366107, 12.960274, -22.439577, -7.146065, 12.960274, -22.073904, -6.570064, 17.195192, -22.439577, -6.539082, 12.960274, -22.439577, -6.570064, 17.195192, -22.439577, -6.539082, 12.960274, -22.073904, -6.570064, 16.136572, -22.073904, -6.5465007, 17.195192, -22.439577, -6.539082, 16.136572, -22.073904, -6.5465007, 17.195192, -22.165104, -6.539082, 17.195192, -22.165104, -6.539082, 21.299639, -22.439577, -6.873338, 17.195192, -22.439577, -6.539082, 17.195192, -22.165104, -6.539082, 21.299639, -22.073904, -6.873338, 21.299639, -22.439577, -6.873338, 21.299639, -22.073904, -6.873338, 17.195192, -22.165104, -6.539082, 18.221523, -22.073904, -6.6224284, 14.394604, -22.439577, -15.51292, 21.493383, -22.073904, -15.67263, 14.394604, -22.073904, -15.51292, 14.394604, -22.439577, -15.51292, 21.493383, -22.439577, -15.67263, 21.493383, -22.073904, -15.67263, 12.76609, -22.073904, -15.519905, 14.394604, -22.439577, -15.51292, 14.394604, -22.073904, -15.51292, 12.76609, -22.073904, -15.519905, 12.76609, -22.439577, -15.519905, 14.394604, -22.439577, -15.51292, 14.394604, -22.439577, -14.504482, 21.493383, -22.439577, -15.67263, 14.394604, -22.439577, -15.51292, 14.394604, -22.439577, -14.504482, 21.493383, -22.439577, -14.689938, 21.493383, -22.439577, -15.67263, 12.76609, -22.439577, -15.519905, 14.394604, -22.439577, -14.504482, 14.394604, -22.439577, -15.51292, 12.76609, -22.439577, -15.519905, 12.76609, -22.439577, -14.689938, 14.394604, -22.439577, -14.504482, 12.76609, -22.439577, -15.519905, 12.76609, -22.073904, -14.689938, 12.76609, -22.439577, -14.689938, 12.76609, -22.439577, -15.519905, 12.76609, -22.073904, -15.519905, 12.76609, -22.073904, -14.689938, 14.394604, -22.073904, -14.756702, 14.394604, -22.165104, -14.504482, 13.987476, -22.073904, -14.550737, 14.394604, -22.073904, -14.756702, 16.1693, -22.073904, -14.550737, 14.394604, -22.165104, -14.504482, 14.394604, -22.073904, -14.756702, 20.162472, -22.073904, -14.655029, 16.1693, -22.073904, -14.550737, 13.987476, -22.073904, -14.550737, 14.394604, -22.073904, -15.51292, 14.394604, -22.073904, -14.756702, 13.987476, -22.073904, -14.550737, 12.76609, -22.073904, -15.519905, 14.394604, -22.073904, -15.51292, 12.76609, -22.073904, -15.519905, 13.987476, -22.073904, -14.550737, 12.76609, -22.073904, -14.689938, 21.493383, -22.073904, -15.67263, 14.394604, -22.073904, -14.756702, 14.394604, -22.073904, -15.51292, 14.394604, -22.073904, -14.756702, 21.493383, -22.073904, -15.67263, 20.162472, -22.073904, -14.655029, 20.162472, -22.073904, -14.655029, 21.493383, -22.073904, -15.67263, 21.493383, -22.073904, -14.935175, 20.162472, -22.073904, -14.655029, 21.493383, -22.073904, -14.935175, 21.493383, -22.165104, -14.689938, 12.76609, -22.073904, -14.689938, 14.394604, -22.439577, -14.504482, 12.76609, -22.439577, -14.689938, 14.394604, -22.439577, -14.504482, 12.76609, -22.073904, -14.689938, 13.987476, -22.073904, -14.550737, 14.394604, -22.439577, -14.504482, 13.987476, -22.073904, -14.550737, 14.394604, -22.165104, -14.504482, 14.394604, -22.165104, -14.504482, 21.493383, -22.439577, -14.689938, 14.394604, -22.439577, -14.504482, 14.394604, -22.165104, -14.504482, 20.162472, -22.073904, -14.655029, 21.493383, -22.439577, -14.689938, 21.493383, -22.439577, -14.689938, 20.162472, -22.073904, -14.655029, 21.493383, -22.165104, -14.689938, 20.162472, -22.073904, -14.655029, 14.394604, -22.165104, -14.504482, 16.1693, -22.073904, -14.550737, 21.493383, -22.165104, -14.689938, 21.493383, -22.439577, -15.67263, 21.493383, -22.439577, -14.689938, 21.493383, -22.439577, -15.67263, 21.493383, -22.165104, -14.689938, 21.493383, -22.073904, -14.935175, 21.493383, -22.439577, -15.67263, 21.493383, -22.073904, -14.935175, 21.493383, -22.073904, -15.67263, 14.33395, -22.439577, -8.868834, 21.590258, -22.073904, -9.028543, 14.33395, -22.073904, -8.868834, 14.33395, -22.439577, -8.868834, 21.590258, -22.439577, -9.028543, 21.590258, -22.073904, -9.028543, 12.669218, -22.073904, -8.875814, 14.33395, -22.439577, -8.868834, 14.33395, -22.073904, -8.868834, 12.669218, -22.073904, -8.875814, 12.669218, -22.439577, -8.875814, 14.33395, -22.439577, -8.868834, 14.33395, -22.439577, -7.860395, 21.590258, -22.439577, -9.028543, 14.33395, -22.439577, -8.868834, 14.33395, -22.439577, -7.860395, 21.590258, -22.439577, -8.045849, 21.590258, -22.439577, -9.028543, 12.669218, -22.439577, -8.875814, 14.33395, -22.439577, -7.860395, 14.33395, -22.439577, -8.868834, 12.669218, -22.439577, -8.875814, 12.669218, -22.439577, -8.045849, 14.33395, -22.439577, -7.860395, 12.669218, -22.439577, -8.875814, 12.669218, -22.073904, -8.045849, 12.669218, -22.439577, -8.045849, 12.669218, -22.439577, -8.875814, 12.669218, -22.073904, -8.875814, 12.669218, -22.073904, -8.045849, 14.33395, -22.073904, -8.112614, 14.33395, -22.165104, -7.860395, 13.917658, -22.073904, -7.906649, 14.33395, -22.073904, -8.112614, 16.147917, -22.073904, -7.906649, 14.33395, -22.165104, -7.860395, 14.33395, -22.073904, -8.112614, 20.229671, -22.073904, -8.011377, 16.147917, -22.073904, -7.906649, 13.917658, -22.073904, -7.906649, 14.33395, -22.073904, -8.868834, 14.33395, -22.073904, -8.112614, 13.917658, -22.073904, -7.906649, 12.669218, -22.073904, -8.875814, 14.33395, -22.073904, -8.868834, 12.669218, -22.073904, -8.875814, 13.917658, -22.073904, -7.906649, 12.669218, -22.073904, -8.045849, 21.590258, -22.073904, -9.028543, 14.33395, -22.073904, -8.112614, 14.33395, -22.073904, -8.868834, 14.33395, -22.073904, -8.112614, 21.590258, -22.073904, -9.028543, 20.229671, -22.073904, -8.011377, 20.229671, -22.073904, -8.011377, 21.590258, -22.073904, -9.028543, 21.590258, -22.073904, -8.29196, 20.229671, -22.073904, -8.011377, 21.590258, -22.073904, -8.29196, 21.590258, -22.165104, -8.045849, 12.669218, -22.073904, -8.045849, 14.33395, -22.439577, -7.860395, 12.669218, -22.439577, -8.045849, 14.33395, -22.439577, -7.860395, 12.669218, -22.073904, -8.045849, 13.917658, -22.073904, -7.906649, 14.33395, -22.439577, -7.860395, 13.917658, -22.073904, -7.906649, 14.33395, -22.165104, -7.860395, 14.33395, -22.165104, -7.860395, 21.590258, -22.439577, -8.045849, 14.33395, -22.439577, -7.860395, 14.33395, -22.165104, -7.860395, 20.229671, -22.073904, -8.011377, 21.590258, -22.439577, -8.045849, 21.590258, -22.439577, -8.045849, 20.229671, -22.073904, -8.011377, 21.590258, -22.165104, -8.045849, 20.229671, -22.073904, -8.011377, 14.33395, -22.165104, -7.860395, 16.147917, -22.073904, -7.906649, 21.590258, -22.165104, -8.045849, 21.590258, -22.439577, -9.028543, 21.590258, -22.439577, -8.045849, 21.590258, -22.439577, -9.028543, 21.590258, -22.165104, -8.045849, 21.590258, -22.073904, -8.29196, 21.590258, -22.439577, -9.028543, 21.590258, -22.073904, -8.29196, 21.590258, -22.073904, -9.028543, 20.608437, -25.90344, 5.6272016, 21.338474, -21.174994, 5.6272016, 21.338474, -25.90344, 5.6272016, 20.608437, -25.90344, 5.6272016, 20.608437, -21.723503, 5.6272016, 21.338474, -21.174994, 5.6272016, 20.608437, -21.723503, 5.6272016, 21.338474, -21.174994, 4.8975997, 21.338474, -21.174994, 5.6272016, 20.608437, -21.723503, 5.6272016, 20.608437, -21.174994, 4.8975997, 21.338474, -21.174994, 4.8975997, 20.608437, -21.174994, 4.8975997, 21.338474, -25.90344, 4.8975997, 21.338474, -21.174994, 4.8975997, 20.608437, -21.174994, 4.8975997, 20.608437, -25.90344, 4.8975997, 21.338474, -25.90344, 4.8975997, 20.608437, -25.90344, 4.8975997, 21.338474, -25.90344, 5.6272016, 21.338474, -25.90344, 4.8975997, 20.608437, -25.90344, 4.8975997, 20.608437, -25.90344, 5.6272016, 21.338474, -25.90344, 5.6272016, 21.338474, -25.90344, 5.6272016, 21.338474, -21.174994, 4.8975997, 21.338474, -25.90344, 4.8975997, 21.338474, -25.90344, 5.6272016, 21.338474, -21.174994, 5.6272016, 21.338474, -21.174994, 4.8975997, 20.608437, -25.90344, 4.8975997, 20.608437, -21.723503, 5.6272016, 20.608437, -25.90344, 5.6272016, 20.608437, -25.90344, 4.8975997, 20.608437, -21.174994, 4.8975997, 20.608437, -21.723503, 5.6272016, 20.752874, -22.635942, 4.8980365, 20.984583, -22.543432, 4.8980365, 20.984583, -22.635942, 4.8980365, 20.752874, -22.635942, 4.8980365, 20.752874, -22.543432, 4.8980365, 20.984583, -22.543432, 4.8980365, 20.752874, -22.543432, 4.8980365, 20.984583, -22.543432, -19.742168, 20.984583, -22.543432, 4.8980365, 20.752874, -22.543432, 4.8980365, 20.752874, -22.543432, -19.742168, 20.984583, -22.543432, -19.742168, 20.752874, -22.543432, -19.742168, 20.984583, -22.635942, -19.742168, 20.984583, -22.543432, -19.742168, 20.752874, -22.543432, -19.742168, 20.752874, -22.635942, -19.742168, 20.984583, -22.635942, -19.742168, 20.752874, -22.635942, -19.742168, 20.984583, -22.635942, 4.8980365, 20.984583, -22.635942, -19.742168, 20.752874, -22.635942, -19.742168, 20.752874, -22.635942, 4.8980365, 20.984583, -22.635942, 4.8980365, 20.984583, -22.635942, 4.8980365, 20.984583, -22.543432, -19.742168, 20.984583, -22.635942, -19.742168, 20.984583, -22.635942, 4.8980365, 20.984583, -22.543432, 4.8980365, 20.984583, -22.543432, -19.742168, 20.752874, -22.635942, -19.742168, 20.752874, -22.543432, 4.8980365, 20.752874, -22.635942, 4.8980365, 20.752874, -22.635942, -19.742168, 20.752874, -22.543432, -19.742168, 20.752874, -22.543432, 4.8980365, 12.886963, -25.348385, -5.927299, 13.6170025, -20.619938, -5.927299, 13.6170025, -25.348385, -5.927299, 12.886963, -25.348385, -5.927299, 12.886963, -20.619938, -5.927299, 13.6170025, -20.619938, -5.927299, 12.886963, -25.348385, -6.6573377, 13.6170025, -25.348385, -5.927299, 13.6170025, -25.348385, -6.6573377, 12.886963, -25.348385, -6.6573377, 12.886963, -25.348385, -5.927299, 13.6170025, -25.348385, -5.927299, 13.6170025, -25.348385, -5.927299, 13.6170025, -20.619938, -6.6573377, 13.6170025, -25.348385, -6.6573377, 13.6170025, -25.348385, -5.927299, 13.6170025, -20.619938, -5.927299, 13.6170025, -20.619938, -6.6573377, 12.886963, -21.80205, -6.6573377, 13.069365, -20.619938, -6.6573377, 12.886963, -20.619938, -6.474937, 12.886963, -25.348385, -5.927299, 12.886963, -21.80205, -6.6573377, 12.886963, -20.619938, -6.474937, 12.886963, -21.80205, -6.6573377, 12.886963, -25.348385, -5.927299, 12.886963, -25.348385, -6.6573377, 12.886963, -25.348385, -5.927299, 12.886963, -20.619938, -6.474937, 12.886963, -20.619938, -5.927299, 12.886963, -20.619938, -6.474937, 13.6170025, -20.619938, -5.927299, 12.886963, -20.619938, -5.927299, 13.6170025, -20.619938, -5.927299, 12.886963, -20.619938, -6.474937, 13.069365, -20.619938, -6.6573377, 13.6170025, -20.619938, -5.927299, 13.069365, -20.619938, -6.6573377, 13.6170025, -20.619938, -6.6573377, 12.886963, -25.348385, -6.6573377, 13.069365, -20.619938, -6.6573377, 12.886963, -21.80205, -6.6573377, 13.069365, -20.619938, -6.6573377, 12.886963, -25.348385, -6.6573377, 13.6170025, -25.348385, -6.6573377, 13.069365, -20.619938, -6.6573377, 13.6170025, -25.348385, -6.6573377, 13.6170025, -20.619938, -6.6573377, 12.886963, -26.235077, -13.537062, 13.6170025, -21.50663, -13.537062, 13.6170025, -26.235077, -13.537062, 12.886963, -26.235077, -13.537062, 12.886963, -21.50663, -13.537062, 13.6170025, -21.50663, -13.537062, 12.886963, -26.235077, -14.2666645, 13.6170025, -26.235077, -13.537062, 13.6170025, -26.235077, -14.2666645, 12.886963, -26.235077, -14.2666645, 12.886963, -26.235077, -13.537062, 13.6170025, -26.235077, -13.537062, 13.6170025, -26.235077, -13.537062, 13.6170025, -21.50663, -14.2666645, 13.6170025, -26.235077, -14.2666645, 13.6170025, -26.235077, -13.537062, 13.6170025, -21.50663, -13.537062, 13.6170025, -21.50663, -14.2666645, 12.886963, -22.688742, -14.2666645, 13.069365, -21.50663, -14.2666645, 12.886963, -21.50663, -14.084265, 12.886963, -26.235077, -13.537062, 12.886963, -22.688742, -14.2666645, 12.886963, -21.50663, -14.084265, 12.886963, -22.688742, -14.2666645, 12.886963, -26.235077, -13.537062, 12.886963, -26.235077, -14.2666645, 12.886963, -26.235077, -13.537062, 12.886963, -21.50663, -14.084265, 12.886963, -21.50663, -13.537062, 12.886963, -21.50663, -14.084265, 13.6170025, -21.50663, -13.537062, 12.886963, -21.50663, -13.537062, 13.6170025, -21.50663, -13.537062, 12.886963, -21.50663, -14.084265, 13.069365, -21.50663, -14.2666645, 13.6170025, -21.50663, -13.537062, 13.069365, -21.50663, -14.2666645, 13.6170025, -21.50663, -14.2666645, 12.886963, -26.235077, -14.2666645, 13.069365, -21.50663, -14.2666645, 12.886963, -22.688742, -14.2666645, 13.069365, -21.50663, -14.2666645, 12.886963, -26.235077, -14.2666645, 13.6170025, -26.235077, -14.2666645, 13.069365, -21.50663, -14.2666645, 13.6170025, -26.235077, -14.2666645, 13.6170025, -21.50663, -14.2666645, 21.21673, -25.99464, -5.8286805, 21.946331, -21.266193, -5.8286805, 21.946331, -25.99464, -5.8286805, 21.21673, -25.99464, -5.8286805, 21.21673, -21.266193, -5.8286805, 21.946331, -21.266193, -5.8286805, 21.21673, -25.99464, -6.558719, 21.946331, -25.99464, -5.8286805, 21.946331, -25.99464, -6.558719, 21.21673, -25.99464, -6.558719, 21.21673, -25.99464, -5.8286805, 21.946331, -25.99464, -5.8286805, 21.946331, -25.99464, -5.8286805, 21.946331, -21.266193, -6.558719, 21.946331, -25.99464, -6.558719, 21.946331, -25.99464, -5.8286805, 21.946331, -21.266193, -5.8286805, 21.946331, -21.266193, -6.558719, 21.21673, -22.448305, -6.558719, 21.39913, -21.266193, -6.558719, 21.21673, -21.266193, -6.3763185, 21.21673, -25.99464, -5.8286805, 21.21673, -22.448305, -6.558719, 21.21673, -21.266193, -6.3763185, 21.21673, -22.448305, -6.558719, 21.21673, -25.99464, -5.8286805, 21.21673, -25.99464, -6.558719, 21.21673, -25.99464, -5.8286805, 21.21673, -21.266193, -6.3763185, 21.21673, -21.266193, -5.8286805, 21.21673, -21.266193, -6.3763185, 21.946331, -21.266193, -5.8286805, 21.21673, -21.266193, -5.8286805, 21.946331, -21.266193, -5.8286805, 21.21673, -21.266193, -6.3763185, 21.39913, -21.266193, -6.558719, 21.946331, -21.266193, -5.8286805, 21.39913, -21.266193, -6.558719, 21.946331, -21.266193, -6.558719, 21.21673, -25.99464, -6.558719, 21.39913, -21.266193, -6.558719, 21.21673, -22.448305, -6.558719, 21.39913, -21.266193, -6.558719, 21.21673, -25.99464, -6.558719, 21.946331, -25.99464, -6.558719, 21.39913, -21.266193, -6.558719, 21.946331, -25.99464, -6.558719, 21.946331, -21.266193, -6.558719, 21.21673, -25.99464, -13.537062, 21.946331, -21.266193, -13.537062, 21.946331, -25.99464, -13.537062, 21.21673, -25.99464, -13.537062, 21.21673, -21.266193, -13.537062, 21.946331, -21.266193, -13.537062, 21.21673, -25.99464, -14.2666645, 21.946331, -25.99464, -13.537062, 21.946331, -25.99464, -14.2666645, 21.21673, -25.99464, -14.2666645, 21.21673, -25.99464, -13.537062, 21.946331, -25.99464, -13.537062, 21.946331, -25.99464, -13.537062, 21.946331, -21.266193, -14.2666645, 21.946331, -25.99464, -14.2666645, 21.946331, -25.99464, -13.537062, 21.946331, -21.266193, -13.537062, 21.946331, -21.266193, -14.2666645, 21.21673, -22.448305, -14.2666645, 21.39913, -21.266193, -14.2666645, 21.21673, -21.266193, -14.084265, 21.21673, -25.99464, -13.537062, 21.21673, -22.448305, -14.2666645, 21.21673, -21.266193, -14.084265, 21.21673, -22.448305, -14.2666645, 21.21673, -25.99464, -13.537062, 21.21673, -25.99464, -14.2666645, 21.21673, -25.99464, -13.537062, 21.21673, -21.266193, -14.084265, 21.21673, -21.266193, -13.537062, 21.21673, -21.266193, -14.084265, 21.946331, -21.266193, -13.537062, 21.21673, -21.266193, -13.537062, 21.946331, -21.266193, -13.537062, 21.21673, -21.266193, -14.084265, 21.39913, -21.266193, -14.2666645, 21.946331, -21.266193, -13.537062, 21.39913, -21.266193, -14.2666645, 21.946331, -21.266193, -14.2666645, 21.21673, -25.99464, -14.2666645, 21.39913, -21.266193, -14.2666645, 21.21673, -22.448305, -14.2666645, 21.39913, -21.266193, -14.2666645, 21.21673, -25.99464, -14.2666645, 21.946331, -25.99464, -14.2666645, 21.39913, -21.266193, -14.2666645, 21.946331, -25.99464, -14.2666645, 21.946331, -21.266193, -14.2666645, -13.073534, -0.35833585, 29.034882, -8.669889, -0.18640822, 28.876835, -13.057354, 0.007337749, 29.029753, -13.073534, -0.35833585, 29.034882, -8.686073, -0.5516454, 28.882399, -8.669889, -0.18640822, 28.876835, -17.582396, 0.2010836, 28.810827, -13.073534, -0.35833585, 29.034882, -13.057354, 0.007337749, 29.029753, -17.582396, 0.2010836, 28.810827, -17.59858, -0.16415358, 28.816393, -13.073534, -0.35833585, 29.034882, -13.080118, -0.3434996, 30.014496, -8.686073, -0.5516454, 28.882399, -13.073534, -0.35833585, 29.034882, -13.080118, -0.3434996, 30.014496, -8.69165, -0.538991, 29.712345, -8.686073, -0.5516454, 28.882399, -13.073534, -0.35833585, 29.034882, -17.606195, -0.1471355, 29.949606, -13.080118, -0.3434996, 30.014496, -13.073534, -0.35833585, 29.034882, -17.602324, -0.15586269, 29.373617, -17.606195, -0.1471355, 29.949606, -17.602324, -0.15586269, 29.373617, -13.073534, -0.35833585, 29.034882, -17.59858, -0.16415358, 28.816393, -8.69165, -0.538991, 29.712345, -8.669889, -0.18640822, 28.876835, -8.686073, -0.5516454, 28.882399, -8.69165, -0.538991, 29.712345, -8.675467, -0.17375374, 29.706781, -8.669889, -0.18640822, 28.876835, -17.585695, 0.20850176, 29.301727, -17.420107, 0.093301594, 29.40146, -17.602324, -0.15586269, 29.373617, -13.06229, 0.01824683, 29.764137, -13.067871, -0.06946254, 30.010216, -14.195347, 0.07104677, 29.993038, -13.06229, 0.01824683, 29.764137, -11.966818, -0.027135193, 29.933395, -13.067871, -0.06946254, 30.010216, -13.06229, 0.01824683, 29.764137, -8.675467, -0.17375374, 29.706781, -11.966818, -0.027135193, 29.933395, -13.06229, 0.01824683, 29.764137, -8.669889, -0.18640822, 28.876835, -8.675467, -0.17375374, 29.706781, -8.669889, -0.18640822, 28.876835, -13.06229, 0.01824683, 29.764137, -13.057354, 0.007337749, 29.029753, -14.195347, 0.07104677, 29.993038, -13.057354, 0.007337749, 29.029753, -13.06229, 0.01824683, 29.764137, -13.057354, 0.007337749, 29.029753, -17.585695, 0.20850176, 29.301727, -17.582396, 0.2010836, 28.810827, -13.057354, 0.007337749, 29.029753, -16.80835, 0.17621082, 29.407318, -17.585695, 0.20850176, 29.301727, -14.195347, 0.07104677, 29.993038, -16.80835, 0.17621082, 29.407318, -13.057354, 0.007337749, 29.029753, -17.420107, 0.093301594, 29.40146, -17.585695, 0.20850176, 29.301727, -16.80835, 0.17621082, 29.407318, -17.587553, 0.21286553, 29.57794, -17.420107, 0.093301594, 29.40146, -16.80835, 0.17621082, 29.407318, -16.80835, 0.17621082, 29.407318, -14.195347, 0.07104677, 29.993038, -17.587553, 0.21286553, 29.57794, -17.587553, 0.21286553, 29.57794, -14.195347, 0.07104677, 29.993038, -17.590448, 0.21810168, 29.944038, -17.59858, -0.16415358, 28.816393, -17.585695, 0.20850176, 29.301727, -17.602324, -0.15586269, 29.373617, -17.59858, -0.16415358, 28.816393, -17.582396, 0.2010836, 28.810827, -17.585695, 0.20850176, 29.301727, -17.587553, 0.21286553, 29.57794, -17.606195, -0.1471355, 29.949606, -17.602324, -0.15586269, 29.373617, -17.587553, 0.21286553, 29.57794, -17.590448, 0.21810168, 29.944038, -17.606195, -0.1471355, 29.949606, -17.420107, 0.093301594, 29.40146, -17.587553, 0.21286553, 29.57794, -17.602324, -0.15586269, 29.373617, -17.590448, 0.21810168, 29.944038, -13.080118, -0.3434996, 30.014496, -17.606195, -0.1471355, 29.949606, -13.080118, -0.3434996, 30.014496, -17.590448, 0.21810168, 29.944038, -13.067871, -0.06946254, 30.010216, -13.067871, -0.06946254, 30.010216, -17.590448, 0.21810168, 29.944038, -14.195347, 0.07104677, 29.993038, -13.067871, -0.06946254, 30.010216, -8.675467, -0.17375374, 29.706781, -13.080118, -0.3434996, 30.014496, -13.080118, -0.3434996, 30.014496, -8.675467, -0.17375374, 29.706781, -8.69165, -0.538991, 29.712345, -8.675467, -0.17375374, 29.706781, -13.067871, -0.06946254, 30.010216, -11.966818, -0.027135193, 29.933395, -9.878, -0.097390115, 31.312849, -8.993242, -0.5014638, 31.344105, -9.893747, -0.4626273, 31.318417, -9.878, -0.097390115, 31.312849, -8.977062, -0.13622653, 31.338978, -8.993242, -0.5014638, 31.344105, -9.893747, -0.4626273, 31.318417, -17.31272, 0.1343196, 31.278593, -9.878, -0.097390115, 31.312849, -9.878, -0.097390115, 31.312849, -17.31272, 0.1343196, 31.278593, -15.451195, 0.14479238, 31.285868, -17.31272, 0.1343196, 31.278593, -9.893747, -0.4626273, 31.318417, -17.324528, -0.13971746, 31.28244, -8.970457, -0.15106285, 30.356306, -9.878, -0.097390115, 31.312849, -9.871372, -0.11222637, 30.326687, -15.451195, 0.14479238, 31.285868, -9.871372, -0.11222637, 30.326687, -9.878, -0.097390115, 31.312849, -15.451195, 0.14479238, 31.285868, -17.303205, 0.21330172, 30.447365, -9.871372, -0.11222637, 30.326687, -17.303205, 0.21330172, 30.447365, -15.451195, 0.14479238, 31.285868, -17.307388, 0.22246522, 31.069607, -17.307388, 0.22246522, 31.069607, -15.451195, 0.14479238, 31.285868, -17.31272, 0.1343196, 31.278593, -9.878, -0.097390115, 31.312849, -8.970457, -0.15106285, 30.356306, -9.454359, -0.122262776, 30.87016, -9.454359, -0.122262776, 30.87016, -8.970457, -0.15106285, 30.356306, -8.973118, -0.14495373, 30.75208, -8.973118, -0.14495373, 30.75208, -9.062445, -0.27935404, 30.862755, -9.454359, -0.122262776, 30.87016, -9.062445, -0.27935404, 30.862755, -8.974958, -0.14102644, 31.026112, -9.454359, -0.122262776, 30.87016, -9.878, -0.097390115, 31.312849, -9.454359, -0.122262776, 30.87016, -8.974958, -0.14102644, 31.026112, -9.878, -0.097390115, 31.312849, -8.974958, -0.14102644, 31.026112, -8.977062, -0.13622653, 31.338978, -9.8871155, -0.47789997, 30.331818, -8.970457, -0.15106285, 30.356306, -9.871372, -0.11222637, 30.326687, -9.8871155, -0.47789997, 30.331818, -8.986637, -0.5163001, 30.361433, -8.970457, -0.15106285, 30.356306, -17.303205, 0.21330172, 30.447365, -9.8871155, -0.47789997, 30.331818, -9.871372, -0.11222637, 30.326687, -17.303205, 0.21330172, 30.447365, -17.318953, -0.15193558, 30.45293, -9.8871155, -0.47789997, 30.331818, -8.993242, -0.5014638, 31.344105, -9.8871155, -0.47789997, 30.331818, -9.893747, -0.4626273, 31.318417, -9.8871155, -0.47789997, 30.331818, -8.993242, -0.5014638, 31.344105, -8.989674, -0.50975466, 30.81306, -9.8871155, -0.47789997, 30.331818, -8.989674, -0.50975466, 30.81306, -8.986637, -0.5163001, 30.361433, -17.318953, -0.15193558, 30.45293, -9.893747, -0.4626273, 31.318417, -9.8871155, -0.47789997, 30.331818, -17.318953, -0.15193558, 30.45293, -17.324528, -0.13971746, 31.28244, -9.893747, -0.4626273, 31.318417, -8.974958, -0.14102644, 31.026112, -9.062445, -0.27935404, 30.862755, -8.989674, -0.50975466, 30.81306, -9.062445, -0.27935404, 30.862755, -8.973118, -0.14495373, 30.75208, -8.989674, -0.50975466, 30.81306, -8.993242, -0.5014638, 31.344105, -8.974958, -0.14102644, 31.026112, -8.989674, -0.50975466, 30.81306, -8.993242, -0.5014638, 31.344105, -8.977062, -0.13622653, 31.338978, -8.974958, -0.14102644, 31.026112, -8.973118, -0.14495373, 30.75208, -8.986637, -0.5163001, 30.361433, -8.989674, -0.50975466, 30.81306, -8.973118, -0.14495373, 30.75208, -8.970457, -0.15106285, 30.356306, -8.986637, -0.5163001, 30.361433, -17.303205, 0.21330172, 30.447365, -17.324528, -0.13971746, 31.28244, -17.318953, -0.15193558, 30.45293, -17.324528, -0.13971746, 31.28244, -17.303205, 0.21330172, 30.447365, -17.31272, 0.1343196, 31.278593, -17.31272, 0.1343196, 31.278593, -17.303205, 0.21330172, 30.447365, -17.307388, 0.22246522, 31.069607, -13.105944, -0.29549986, 33.143127, -8.988548, -0.111353934, 32.98316, -13.089764, 0.069737375, 33.138, -13.105944, -0.29549986, 33.143127, -9.004295, -0.47659117, 32.988728, -8.988548, -0.111353934, 32.98316, -17.31983, 0.25082868, 32.92106, -13.105944, -0.29549986, 33.143127, -13.089764, 0.069737375, 33.138, -17.31983, 0.25082868, 32.92106, -17.335577, -0.11440843, 32.926624, -13.105944, -0.29549986, 33.143127, -13.112524, -0.2806636, 34.12231, -9.004295, -0.47659117, 32.988728, -13.105944, -0.29549986, 33.143127, -13.112524, -0.2806636, 34.12231, -9.009869, -0.4639367, 33.818237, -9.004295, -0.47659117, 32.988728, -13.105944, -0.29549986, 33.143127, -17.343197, -0.09739047, 34.060276, -13.112524, -0.2806636, 34.12231, -13.105944, -0.29549986, 33.143127, -17.339323, -0.10611755, 33.483852, -17.343197, -0.09739047, 34.060276, -17.339323, -0.10611755, 33.483852, -13.105944, -0.29549986, 33.143127, -17.335577, -0.11440843, 32.926624, -9.009869, -0.4639367, 33.818237, -8.988548, -0.111353934, 32.98316, -9.004295, -0.47659117, 32.988728, -9.009869, -0.4639367, 33.818237, -8.994122, -0.09869945, 33.812668, -8.988548, -0.111353934, 32.98316, -17.323126, 0.25824696, 33.411522, -17.16845, 0.14348298, 33.51162, -17.339323, -0.10611755, 33.483852, -13.0947, 0.080646336, 33.872387, -13.100277, -0.0066265464, 34.118027, -14.154011, 0.1303919, 34.101345, -13.0947, 0.080646336, 33.872387, -12.070787, 0.038755357, 34.040726, -13.100277, -0.0066265464, 34.118027, -13.0947, 0.080646336, 33.872387, -8.994122, -0.09869945, 33.812668, -12.070787, 0.038755357, 34.040726, -13.0947, 0.080646336, 33.872387, -8.988548, -0.111353934, 32.98316, -8.994122, -0.09869945, 33.812668, -8.988548, -0.111353934, 32.98316, -13.0947, 0.080646336, 33.872387, -13.089764, 0.069737375, 33.138, -14.154011, 0.1303919, 34.101345, -13.089764, 0.069737375, 33.138, -13.0947, 0.080646336, 33.872387, -13.089764, 0.069737375, 33.138, -17.323126, 0.25824696, 33.411522, -17.31983, 0.25082868, 32.92106, -13.089764, 0.069737375, 33.138, -16.596401, 0.22813767, 33.51721, -17.323126, 0.25824696, 33.411522, -14.154011, 0.1303919, 34.101345, -16.596401, 0.22813767, 33.51721, -13.089764, 0.069737375, 33.138, -17.16845, 0.14348298, 33.51162, -17.323126, 0.25824696, 33.411522, -16.596401, 0.22813767, 33.51721, -17.324986, 0.2621742, 33.68817, -17.16845, 0.14348298, 33.51162, -16.596401, 0.22813767, 33.51721, -16.596401, 0.22813767, 33.51721, -14.154011, 0.1303919, 34.101345, -17.324986, 0.2621742, 33.68817, -17.324986, 0.2621742, 33.68817, -14.154011, 0.1303919, 34.101345, -17.32745, 0.26784688, 34.05471, -17.335577, -0.11440843, 32.926624, -17.323126, 0.25824696, 33.411522, -17.339323, -0.10611755, 33.483852, -17.335577, -0.11440843, 32.926624, -17.31983, 0.25082868, 32.92106, -17.323126, 0.25824696, 33.411522, -17.324986, 0.2621742, 33.68817, -17.343197, -0.09739047, 34.060276, -17.339323, -0.10611755, 33.483852, -17.324986, 0.2621742, 33.68817, -17.32745, 0.26784688, 34.05471, -17.343197, -0.09739047, 34.060276, -17.16845, 0.14348298, 33.51162, -17.324986, 0.2621742, 33.68817, -17.339323, -0.10611755, 33.483852, -17.32745, 0.26784688, 34.05471, -13.112524, -0.2806636, 34.12231, -17.343197, -0.09739047, 34.060276, -13.112524, -0.2806636, 34.12231, -17.32745, 0.26784688, 34.05471, -13.100277, -0.0066265464, 34.118027, -13.100277, -0.0066265464, 34.118027, -17.32745, 0.26784688, 34.05471, -14.154011, 0.1303919, 34.101345, -13.100277, -0.0066265464, 34.118027, -8.994122, -0.09869945, 33.812668, -13.112524, -0.2806636, 34.12231, -13.112524, -0.2806636, 34.12231, -8.994122, -0.09869945, 33.812668, -9.009869, -0.4639367, 33.818237, -8.994122, -0.09869945, 33.812668, -13.100277, -0.0066265464, 34.118027, -12.070787, 0.038755357, 34.040726, -15.954979, -0.19208127, 31.771221, -8.68903, -0.14408118, 31.659903, -15.939232, 0.17359239, 31.765654, -15.954979, -0.19208127, 31.771221, -8.704777, -0.50931835, 31.66547, -8.68903, -0.14408118, 31.659903, -17.602564, 0.24559253, 31.74662, -15.954979, -0.19208127, 31.771221, -15.939232, 0.17359239, 31.765654, -17.602564, 0.24559253, 31.74662, -17.61831, -0.11964476, 31.752188, -15.954979, -0.19208127, 31.771221, -15.961756, -0.1768086, 32.779636, -8.704777, -0.50931835, 31.66547, -15.954979, -0.19208127, 31.771221, -15.961756, -0.1768086, 32.779636, -8.71138, -0.49448207, 32.64814, -8.704777, -0.50931835, 31.66547, -17.61831, -0.11964476, 31.752188, -15.961756, -0.1768086, 32.779636, -15.954979, -0.19208127, 31.771221, -17.61831, -0.11964476, 31.752188, -17.623884, -0.10699028, 32.5817, -15.961756, -0.1768086, 32.779636, -17.61831, -0.11964476, 31.752188, -17.608137, 0.25824708, 32.57613, -17.623884, -0.10699028, 32.5817, -17.61831, -0.11964476, 31.752188, -17.602564, 0.24559253, 31.74662, -17.608137, 0.25824708, 32.57613, -15.944313, 0.18493778, 32.52186, -15.949944, 0.09722847, 32.775352, -16.361544, 0.20588332, 32.72502, -15.944313, 0.18493778, 32.52186, -14.133524, 0.109446704, 32.741302, -15.949944, 0.09722847, 32.775352, -15.944313, 0.18493778, 32.52186, -10.055107, -0.069462895, 32.667473, -14.133524, 0.109446704, 32.741302, -16.361544, 0.20588332, 32.72502, -15.939232, 0.17359239, 31.765654, -15.944313, 0.18493778, 32.52186, -16.361544, 0.20588332, 32.72502, -17.602564, 0.24559253, 31.74662, -15.939232, 0.17359239, 31.765654, -17.602564, 0.24559253, 31.74662, -16.361544, 0.20588332, 32.72502, -17.608137, 0.25824708, 32.57613, -8.68903, -0.14408118, 31.659903, -15.944313, 0.18493778, 32.52186, -15.939232, 0.17359239, 31.765654, -15.944313, 0.18493778, 32.52186, -8.68903, -0.14408118, 31.659903, -10.055107, -0.069462895, 32.667473, -10.055107, -0.069462895, 32.667473, -8.68903, -0.14408118, 31.659903, -8.693983, -0.13273573, 32.396904, -10.055107, -0.069462895, 32.667473, -8.693983, -0.13273573, 32.396904, -8.69957, -0.2204451, 32.643856, -17.608137, 0.25824708, 32.57613, -15.961756, -0.1768086, 32.779636, -17.623884, -0.10699028, 32.5817, -15.961756, -0.1768086, 32.779636, -17.608137, 0.25824708, 32.57613, -15.949944, 0.09722847, 32.775352, -15.949944, 0.09722847, 32.775352, -17.608137, 0.25824708, 32.57613, -16.361544, 0.20588332, 32.72502, -15.949944, 0.09722847, 32.775352, -8.69957, -0.2204451, 32.643856, -15.961756, -0.1768086, 32.779636, -15.961756, -0.1768086, 32.779636, -8.69957, -0.2204451, 32.643856, -8.71138, -0.49448207, 32.64814, -8.69957, -0.2204451, 32.643856, -15.949944, 0.09722847, 32.775352, -10.055107, -0.069462895, 32.667473, -10.055107, -0.069462895, 32.667473, -15.949944, 0.09722847, 32.775352, -14.133524, 0.109446704, 32.741302, -8.69957, -0.2204451, 32.643856, -8.704777, -0.50931835, 31.66547, -8.71138, -0.49448207, 32.64814, -8.704777, -0.50931835, 31.66547, -8.69957, -0.2204451, 32.643856, -8.68903, -0.14408118, 31.659903, -8.68903, -0.14408118, 31.659903, -8.69957, -0.2204451, 32.643856, -8.693983, -0.13273573, 32.396904, -13.166946, -0.5581908, 28.64937, -17.27211, -0.19208091, 28.80724, -13.166946, -0.19208091, 28.64937, -13.166946, -0.5581908, 28.64937, -17.27211, -0.5581909, 28.80724, -17.27211, -0.19208091, 28.80724, -8.932932, -0.19208091, 28.863285, -13.166946, -0.5581908, 28.64937, -13.166946, -0.19208091, 28.64937, -8.932932, -0.19208091, 28.863285, -8.932932, -0.5581909, 28.863285, -13.166946, -0.5581908, 28.64937, -13.160368, -0.5581907, 27.670628, -17.27211, -0.5581909, 28.80724, -13.166946, -0.5581908, 28.64937, -13.160368, -0.5581907, 27.670628, -17.266533, -0.5581908, 27.977295, -17.27211, -0.5581909, 28.80724, -13.166946, -0.5581908, 28.64937, -8.925314, -0.55819076, 27.729633, -13.160368, -0.5581907, 27.670628, -13.166946, -0.5581908, 28.64937, -8.929188, -0.5581908, 28.306059, -8.925314, -0.55819076, 27.729633, -8.929188, -0.5581908, 28.306059, -13.166946, -0.5581908, 28.64937, -8.932932, -0.5581909, 28.863285, -17.266533, -0.5581908, 27.977295, -17.27211, -0.19208091, 28.80724, -17.27211, -0.5581909, 28.80724, -17.266533, -0.5581908, 27.977295, -17.266533, -0.1920808, 27.977295, -17.27211, -0.19208091, 28.80724, -8.929636, -0.19208086, 28.372822, -9.089121, -0.30160832, 28.274002, -8.929188, -0.5581908, 28.306059, -13.16201, -0.1920808, 27.914986, -13.160368, -0.28371733, 27.670628, -12.101822, -0.1920808, 27.685158, -13.16201, -0.1920808, 27.914986, -14.1868, -0.1920808, 27.747076, -13.160368, -0.28371733, 27.670628, -13.16201, -0.1920808, 27.914986, -17.266533, -0.1920808, 27.977295, -14.1868, -0.1920808, 27.747076, -13.16201, -0.1920808, 27.914986, -17.27211, -0.19208091, 28.80724, -17.266533, -0.1920808, 27.977295, -17.27211, -0.19208091, 28.80724, -13.16201, -0.1920808, 27.914986, -13.166946, -0.19208091, 28.64937, -12.101822, -0.1920808, 27.685158, -13.166946, -0.19208091, 28.64937, -13.16201, -0.1920808, 27.914986, -13.166946, -0.19208091, 28.64937, -8.929636, -0.19208086, 28.372822, -8.932932, -0.19208091, 28.863285, -13.166946, -0.19208091, 28.64937, -9.656805, -0.19208086, 28.268005, -8.929636, -0.19208086, 28.372822, -12.101822, -0.1920808, 27.685158, -9.656805, -0.19208086, 28.268005, -13.166946, -0.19208091, 28.64937, -9.089121, -0.30160832, 28.274002, -8.929636, -0.19208086, 28.372822, -9.656805, -0.19208086, 28.268005, -8.92778, -0.19208086, 28.09661, -9.089121, -0.30160832, 28.274002, -9.656805, -0.19208086, 28.268005, -9.656805, -0.19208086, 28.268005, -12.101822, -0.1920808, 27.685158, -8.92778, -0.19208086, 28.09661, -8.92778, -0.19208086, 28.09661, -12.101822, -0.1920808, 27.685158, -8.925314, -0.1920808, 27.729633, -8.932932, -0.5581909, 28.863285, -8.929636, -0.19208086, 28.372822, -8.929188, -0.5581908, 28.306059, -8.932932, -0.5581909, 28.863285, -8.932932, -0.19208091, 28.863285, -8.929636, -0.19208086, 28.372822, -8.92778, -0.19208086, 28.09661, -8.925314, -0.55819076, 27.729633, -8.929188, -0.5581908, 28.306059, -8.92778, -0.19208086, 28.09661, -8.925314, -0.1920808, 27.729633, -8.925314, -0.55819076, 27.729633, -9.089121, -0.30160832, 28.274002, -8.92778, -0.19208086, 28.09661, -8.929188, -0.5581908, 28.306059, -8.925314, -0.1920808, 27.729633, -13.160368, -0.5581907, 27.670628, -8.925314, -0.55819076, 27.729633, -13.160368, -0.5581907, 27.670628, -8.925314, -0.1920808, 27.729633, -12.101822, -0.1920808, 27.685158, -13.160368, -0.5581907, 27.670628, -12.101822, -0.1920808, 27.685158, -13.160368, -0.28371733, 27.670628, -13.160368, -0.28371733, 27.670628, -17.266533, -0.5581908, 27.977295, -13.160368, -0.5581907, 27.670628, -13.160368, -0.28371733, 27.670628, -17.266533, -0.1920808, 27.977295, -17.266533, -0.5581908, 27.977295, -17.266533, -0.1920808, 27.977295, -13.160368, -0.28371733, 27.670628, -14.1868, -0.1920808, 27.747076, -16.346624, -0.1920805, 25.245663, -17.248005, -0.55819046, 25.220404, -16.346624, -0.55819046, 25.245663, -16.346624, -0.1920805, 25.245663, -17.248005, -0.1920805, 25.220404, -17.248005, -0.55819046, 25.220404, -16.346624, -0.55819046, 25.245663, -10.768602, -0.1920805, 25.26875, -16.346624, -0.1920805, 25.245663, -16.346624, -0.55819046, 25.245663, -8.908827, -0.28371704, 25.276447, -10.768602, -0.1920805, 25.26875, -8.908827, -0.28371704, 25.276447, -16.346624, -0.55819046, 25.245663, -8.908827, -0.55819046, 25.276447, -17.254608, -0.19208062, 26.203075, -16.346624, -0.1920805, 25.245663, -16.353252, -0.19208062, 26.231823, -10.768602, -0.1920805, 25.26875, -16.353252, -0.19208062, 26.231823, -16.346624, -0.1920805, 25.245663, -10.768602, -0.1920805, 25.26875, -8.914402, -0.19208062, 26.105959, -16.353252, -0.19208062, 26.231823, -8.914402, -0.19208062, 26.105959, -10.768602, -0.1920805, 25.26875, -8.910217, -0.1920805, 25.48328, -8.910217, -0.1920805, 25.48328, -10.768602, -0.1920805, 25.26875, -8.908827, -0.28371704, 25.276447, -16.346624, -0.1920805, 25.245663, -17.254608, -0.19208062, 26.203075, -16.7707, -0.19208056, 25.68835, -16.7707, -0.19208056, 25.68835, -17.254608, -0.19208062, 26.203075, -17.251945, -0.19208056, 25.806864, -17.251945, -0.19208056, 25.806864, -17.169178, -0.33171725, 25.698326, -16.7707, -0.19208056, 25.68835, -17.169178, -0.33171725, 25.698326, -17.250105, -0.1920805, 25.532833, -16.7707, -0.19208056, 25.68835, -16.346624, -0.1920805, 25.245663, -16.7707, -0.19208056, 25.68835, -17.250105, -0.1920805, 25.532833, -16.346624, -0.1920805, 25.245663, -17.250105, -0.1920805, 25.532833, -17.248005, -0.1920805, 25.220404, -16.353252, -0.5581906, 26.231823, -17.254608, -0.19208062, 26.203075, -16.353252, -0.19208062, 26.231823, -16.353252, -0.5581906, 26.231823, -17.254608, -0.5581906, 26.203075, -17.254608, -0.19208062, 26.203075, -8.914402, -0.19208062, 26.105959, -16.353252, -0.5581906, 26.231823, -16.353252, -0.19208062, 26.231823, -8.914402, -0.19208062, 26.105959, -8.914402, -0.5581906, 26.105959, -16.353252, -0.5581906, 26.231823, -17.248005, -0.55819046, 25.220404, -16.353252, -0.5581906, 26.231823, -16.346624, -0.55819046, 25.245663, -16.353252, -0.5581906, 26.231823, -17.248005, -0.55819046, 25.220404, -17.251574, -0.5581905, 25.751448, -16.353252, -0.5581906, 26.231823, -17.251574, -0.5581905, 25.751448, -17.254608, -0.5581906, 26.203075, -8.914402, -0.5581906, 26.105959, -16.346624, -0.55819046, 25.245663, -16.353252, -0.5581906, 26.231823, -8.914402, -0.5581906, 26.105959, -8.908827, -0.55819046, 25.276447, -16.346624, -0.55819046, 25.245663, -17.250105, -0.1920805, 25.532833, -17.169178, -0.33171725, 25.698326, -17.251574, -0.5581905, 25.751448, -17.169178, -0.33171725, 25.698326, -17.251945, -0.19208056, 25.806864, -17.251574, -0.5581905, 25.751448, -17.248005, -0.55819046, 25.220404, -17.250105, -0.1920805, 25.532833, -17.251574, -0.5581905, 25.751448, -17.248005, -0.55819046, 25.220404, -17.248005, -0.1920805, 25.220404, -17.250105, -0.1920805, 25.532833, -17.251945, -0.19208056, 25.806864, -17.254608, -0.5581906, 26.203075, -17.251574, -0.5581905, 25.751448, -17.251945, -0.19208056, 25.806864, -17.254608, -0.19208062, 26.203075, -17.254608, -0.5581906, 26.203075, -8.914402, -0.19208062, 26.105959, -8.908827, -0.55819046, 25.276447, -8.914402, -0.5581906, 26.105959, -8.908827, -0.55819046, 25.276447, -8.914402, -0.19208062, 26.105959, -8.910217, -0.1920805, 25.48328, -8.908827, -0.55819046, 25.276447, -8.910217, -0.1920805, 25.48328, -8.908827, -0.28371704, 25.276447, -16.566906, -0.19208038, 24.06379, -17.53112, -0.55819035, 24.038109, -16.566906, -0.55819035, 24.06379, -16.566906, -0.19208038, 24.06379, -17.53112, -0.19208038, 24.038109, -17.53112, -0.55819035, 24.038109, -16.566906, -0.55819035, 24.06379, -10.599218, -0.19208038, 24.089493, -16.566906, -0.19208038, 24.06379, -16.566906, -0.55819035, 24.06379, -8.610281, -0.28371692, 24.09806, -10.599218, -0.19208038, 24.089493, -8.610281, -0.28371692, 24.09806, -16.566906, -0.55819035, 24.06379, -8.610281, -0.55819035, 24.09806, -17.537725, -0.19208044, 25.020779, -16.566906, -0.19208038, 24.06379, -16.573536, -0.1920805, 25.050386, -10.599218, -0.19208038, 24.089493, -16.573536, -0.1920805, 25.050386, -16.566906, -0.19208038, 24.06379, -10.599218, -0.19208038, 24.089493, -8.61586, -0.19208044, 24.928009, -16.573536, -0.1920805, 25.050386, -8.61586, -0.19208044, 24.928009, -10.599218, -0.19208038, 24.089493, -8.611677, -0.19208038, 24.305765, -8.611677, -0.19208038, 24.305765, -10.599218, -0.19208038, 24.089493, -8.610281, -0.28371692, 24.09806, -16.566906, -0.19208038, 24.06379, -17.537725, -0.19208044, 25.020779, -17.02022, -0.19208038, 24.506718, -17.02022, -0.19208038, 24.506718, -17.537725, -0.19208044, 25.020779, -17.535063, -0.19208044, 24.624569, -17.535063, -0.19208044, 24.624569, -17.446188, -0.33171707, 24.51651, -17.02022, -0.19208038, 24.506718, -17.446188, -0.33171707, 24.51651, -17.533224, -0.19208038, 24.350975, -17.02022, -0.19208038, 24.506718, -16.566906, -0.19208038, 24.06379, -17.02022, -0.19208038, 24.506718, -17.533224, -0.19208038, 24.350975, -16.566906, -0.19208038, 24.06379, -17.533224, -0.19208038, 24.350975, -17.53112, -0.19208038, 24.038109, -16.573536, -0.55819046, 25.050386, -17.537725, -0.19208044, 25.020779, -16.573536, -0.1920805, 25.050386, -16.573536, -0.55819046, 25.050386, -17.537725, -0.55819046, 25.020779, -17.537725, -0.19208044, 25.020779, -8.61586, -0.19208044, 24.928009, -16.573536, -0.55819046, 25.050386, -16.573536, -0.1920805, 25.050386, -8.61586, -0.19208044, 24.928009, -8.61586, -0.5581904, 24.928009, -16.573536, -0.55819046, 25.050386, -17.53112, -0.55819035, 24.038109, -16.573536, -0.55819046, 25.050386, -16.566906, -0.55819035, 24.06379, -16.573536, -0.55819046, 25.050386, -17.53112, -0.55819035, 24.038109, -17.534693, -0.55819035, 24.569588, -16.573536, -0.55819046, 25.050386, -17.534693, -0.55819035, 24.569588, -17.537725, -0.55819046, 25.020779, -8.61586, -0.5581904, 24.928009, -16.566906, -0.55819035, 24.06379, -16.573536, -0.55819046, 25.050386, -8.61586, -0.5581904, 24.928009, -8.610281, -0.55819035, 24.09806, -16.566906, -0.55819035, 24.06379, -17.533224, -0.19208038, 24.350975, -17.446188, -0.33171707, 24.51651, -17.534693, -0.55819035, 24.569588, -17.446188, -0.33171707, 24.51651, -17.535063, -0.19208044, 24.624569, -17.534693, -0.55819035, 24.569588, -17.53112, -0.55819035, 24.038109, -17.533224, -0.19208038, 24.350975, -17.534693, -0.55819035, 24.569588, -17.53112, -0.55819035, 24.038109, -17.53112, -0.19208038, 24.038109, -17.533224, -0.19208038, 24.350975, -17.535063, -0.19208044, 24.624569, -17.537725, -0.55819046, 25.020779, -17.534693, -0.55819035, 24.569588, -17.535063, -0.19208044, 24.624569, -17.537725, -0.19208044, 25.020779, -17.537725, -0.55819046, 25.020779, -8.61586, -0.19208044, 24.928009, -8.610281, -0.55819035, 24.09806, -8.61586, -0.5581904, 24.928009, -8.610281, -0.55819035, 24.09806, -8.61586, -0.19208044, 24.928009, -8.611677, -0.19208038, 24.305765, -8.610281, -0.55819035, 24.09806, -8.611677, -0.19208038, 24.305765, -8.610281, -0.28371692, 24.09806, -10.358263, -0.5581907, 27.454252, -17.457956, -0.1920808, 27.566252, -10.358263, -0.19208074, 27.454252, -10.358263, -0.5581907, 27.454252, -17.457956, -0.5581907, 27.566252, -17.457956, -0.1920808, 27.566252, -8.72983, -0.19208074, 27.471743, -10.358263, -0.5581907, 27.454252, -10.358263, -0.19208074, 27.454252, -8.72983, -0.19208074, 27.471743, -8.72983, -0.5581907, 27.471743, -10.358263, -0.5581907, 27.454252, -10.351484, -0.5581906, 26.445835, -17.457956, -0.5581907, 27.566252, -10.358263, -0.5581907, 27.454252, -10.351484, -0.5581906, 26.445835, -17.451353, -0.5581906, 26.58358, -17.457956, -0.5581907, 27.566252, -8.72983, -0.5581907, 27.471743, -10.351484, -0.5581906, 26.445835, -10.358263, -0.5581907, 27.454252, -8.72983, -0.5581907, 27.471743, -8.724255, -0.5581906, 26.642231, -10.351484, -0.5581906, 26.445835, -8.72983, -0.5581907, 27.471743, -8.724255, -0.19208068, 26.642231, -8.724255, -0.5581906, 26.642231, -8.72983, -0.5581907, 27.471743, -8.72983, -0.19208074, 27.471743, -8.724255, -0.19208068, 26.642231, -10.35318, -0.19208068, 26.69805, -10.351484, -0.28371716, 26.445835, -9.944677, -0.19208062, 26.494825, -10.35318, -0.19208068, 26.69805, -12.1264515, -0.19208062, 26.480164, -10.351484, -0.28371716, 26.445835, -10.35318, -0.19208068, 26.69805, -16.120235, -0.19208068, 26.557617, -12.1264515, -0.19208062, 26.480164, -9.944677, -0.19208062, 26.494825, -10.358263, -0.19208074, 27.454252, -10.35318, -0.19208068, 26.69805, -9.944677, -0.19208062, 26.494825, -8.72983, -0.19208074, 27.471743, -10.358263, -0.19208074, 27.454252, -8.72983, -0.19208074, 27.471743, -9.944677, -0.19208062, 26.494825, -8.724255, -0.19208068, 26.642231, -17.457956, -0.1920808, 27.566252, -10.35318, -0.19208068, 26.69805, -10.358263, -0.19208074, 27.454252, -10.35318, -0.19208068, 26.69805, -17.457956, -0.1920808, 27.566252, -16.120235, -0.19208068, 26.557617, -16.120235, -0.19208068, 26.557617, -17.457956, -0.1920808, 27.566252, -17.453001, -0.19208068, 26.828812, -16.120235, -0.19208068, 26.557617, -17.453001, -0.19208068, 26.828812, -17.451353, -0.28371722, 26.58358, -8.724255, -0.19208068, 26.642231, -10.351484, -0.5581906, 26.445835, -8.724255, -0.5581906, 26.642231, -10.351484, -0.5581906, 26.445835, -8.724255, -0.19208068, 26.642231, -9.944677, -0.19208062, 26.494825, -10.351484, -0.5581906, 26.445835, -9.944677, -0.19208062, 26.494825, -10.351484, -0.28371716, 26.445835, -10.351484, -0.28371716, 26.445835, -17.451353, -0.5581906, 26.58358, -10.351484, -0.5581906, 26.445835, -10.351484, -0.28371716, 26.445835, -16.120235, -0.19208068, 26.557617, -17.451353, -0.5581906, 26.58358, -17.451353, -0.5581906, 26.58358, -16.120235, -0.19208068, 26.557617, -17.451353, -0.28371722, 26.58358, -16.120235, -0.19208068, 26.557617, -10.351484, -0.28371716, 26.445835, -12.1264515, -0.19208062, 26.480164, -17.451353, -0.28371722, 26.58358, -17.457956, -0.5581907, 27.566252, -17.451353, -0.5581906, 26.58358, -17.457956, -0.5581907, 27.566252, -17.451353, -0.28371722, 26.58358, -17.453001, -0.19208068, 26.828812, -17.457956, -0.5581907, 27.566252, -17.453001, -0.19208068, 26.828812, -17.457956, -0.1920808, 27.566252, -13.511387, 0.0055903792, 44.578655, -17.616987, 0.37126416, 44.736523, -13.511387, 0.37126416, 44.578655, -13.511387, 0.0055903792, 44.578655, -17.616987, 0.0055903792, 44.736523, -17.616987, 0.37126416, 44.736523, -9.277809, 0.37126416, 44.792564, -13.511387, 0.0055903792, 44.578655, -13.511387, 0.37126416, 44.578655, -9.277809, 0.37126416, 44.792564, -9.277809, 0.0055903792, 44.792564, -13.511387, 0.0055903792, 44.578655, -13.50481, 0.0055904984, 43.59991, -17.616987, 0.0055903792, 44.736523, -13.511387, 0.0055903792, 44.578655, -13.50481, 0.0055904984, 43.59991, -17.61141, 0.0055904984, 43.906574, -17.616987, 0.0055903792, 44.736523, -13.511387, 0.0055903792, 44.578655, -9.270195, 0.0055904984, 43.65935, -13.50481, 0.0055904984, 43.59991, -13.511387, 0.0055903792, 44.578655, -9.274065, 0.005590439, 44.23534, -9.270195, 0.0055904984, 43.65935, -9.274065, 0.005590439, 44.23534, -13.511387, 0.0055903792, 44.578655, -9.277809, 0.0055903792, 44.792564, -17.61141, 0.0055904984, 43.906574, -17.616987, 0.37126416, 44.736523, -17.616987, 0.0055903792, 44.736523, -17.61141, 0.0055904984, 43.906574, -17.61141, 0.37126416, 43.906574, -17.616987, 0.37126416, 44.736523, -9.274517, 0.37126416, 44.30254, -9.433565, 0.26217288, 44.20372, -9.274065, 0.005590439, 44.23534, -13.506453, 0.37126416, 43.84427, -13.50481, 0.28006393, 43.59991, -12.446262, 0.37126428, 43.61444, -13.506453, 0.37126416, 43.84427, -14.531677, 0.37126428, 43.676357, -13.50481, 0.28006393, 43.59991, -13.506453, 0.37126416, 43.84427, -17.61141, 0.37126416, 43.906574, -14.531677, 0.37126428, 43.676357, -13.506453, 0.37126416, 43.84427, -17.616987, 0.37126416, 44.736523, -17.61141, 0.37126416, 43.906574, -17.616987, 0.37126416, 44.736523, -13.506453, 0.37126416, 43.84427, -13.511387, 0.37126416, 44.578655, -12.446262, 0.37126428, 43.61444, -13.511387, 0.37126416, 44.578655, -13.506453, 0.37126416, 43.84427, -13.511387, 0.37126416, 44.578655, -9.274517, 0.37126416, 44.30254, -9.277809, 0.37126416, 44.792564, -13.511387, 0.37126416, 44.578655, -10.001682, 0.37126416, 44.19729, -9.274517, 0.37126416, 44.30254, -12.446262, 0.37126428, 43.61444, -10.001682, 0.37126416, 44.19729, -13.511387, 0.37126416, 44.578655, -9.433565, 0.26217288, 44.20372, -9.274517, 0.37126416, 44.30254, -10.001682, 0.37126416, 44.19729, -9.272657, 0.37126416, 44.02589, -9.433565, 0.26217288, 44.20372, -10.001682, 0.37126416, 44.19729, -10.001682, 0.37126416, 44.19729, -12.446262, 0.37126428, 43.61444, -9.272657, 0.37126416, 44.02589, -9.272657, 0.37126416, 44.02589, -12.446262, 0.37126428, 43.61444, -9.270195, 0.37126428, 43.65935, -9.277809, 0.0055903792, 44.792564, -9.274517, 0.37126416, 44.30254, -9.274065, 0.005590439, 44.23534, -9.277809, 0.0055903792, 44.792564, -9.277809, 0.37126416, 44.792564, -9.274517, 0.37126416, 44.30254, -9.272657, 0.37126416, 44.02589, -9.270195, 0.0055904984, 43.65935, -9.274065, 0.005590439, 44.23534, -9.272657, 0.37126416, 44.02589, -9.270195, 0.37126428, 43.65935, -9.270195, 0.0055904984, 43.65935, -9.433565, 0.26217288, 44.20372, -9.272657, 0.37126416, 44.02589, -9.274065, 0.005590439, 44.23534, -9.270195, 0.37126428, 43.65935, -13.50481, 0.0055904984, 43.59991, -9.270195, 0.0055904984, 43.65935, -13.50481, 0.0055904984, 43.59991, -9.270195, 0.37126428, 43.65935, -12.446262, 0.37126428, 43.61444, -13.50481, 0.0055904984, 43.59991, -12.446262, 0.37126428, 43.61444, -13.50481, 0.28006393, 43.59991, -13.50481, 0.28006393, 43.59991, -17.61141, 0.0055904984, 43.906574, -13.50481, 0.0055904984, 43.59991, -13.50481, 0.28006393, 43.59991, -17.61141, 0.37126416, 43.906574, -17.61141, 0.0055904984, 43.906574, -17.61141, 0.37126416, 43.906574, -13.50481, 0.28006393, 43.59991, -14.531677, 0.37126428, 43.676357, -16.691502, 0.37126452, 41.174942, -17.592882, 0.0055907965, 41.149685, -16.691502, 0.0055907965, 41.174942, -16.691502, 0.37126452, 41.174942, -17.592882, 0.37126452, 41.149685, -17.592882, 0.0055907965, 41.149685, -16.691502, 0.0055907965, 41.174942, -11.113045, 0.37126452, 41.198032, -16.691502, 0.37126452, 41.174942, -16.691502, 0.0055907965, 41.174942, -9.253704, 0.28006417, 41.20573, -11.113045, 0.37126452, 41.198032, -9.253704, 0.28006417, 41.20573, -16.691502, 0.0055907965, 41.174942, -9.253704, 0.0055907965, 41.20573, -17.599485, 0.3712644, 42.132355, -16.691502, 0.37126452, 41.174942, -16.698133, 0.3712644, 42.16154, -11.113045, 0.37126452, 41.198032, -16.698133, 0.3712644, 42.16154, -16.691502, 0.37126452, 41.174942, -11.113045, 0.37126452, 41.198032, -9.259279, 0.3712644, 42.03524, -16.698133, 0.3712644, 42.16154, -9.259279, 0.3712644, 42.03524, -11.113045, 0.37126452, 41.198032, -9.255098, 0.37126452, 41.413, -9.255098, 0.37126452, 41.413, -11.113045, 0.37126452, 41.198032, -9.253704, 0.28006417, 41.20573, -16.691502, 0.37126452, 41.174942, -17.599485, 0.3712644, 42.132355, -17.115143, 0.37126452, 41.617634, -17.115143, 0.37126452, 41.617634, -17.599485, 0.3712644, 42.132355, -17.596823, 0.37126452, 41.736145, -17.596823, 0.37126452, 41.736145, -17.513622, 0.23162776, 41.628048, -17.115143, 0.37126452, 41.617634, -17.513622, 0.23162776, 41.628048, -17.594984, 0.37126452, 41.46255, -17.115143, 0.37126452, 41.617634, -16.691502, 0.37126452, 41.174942, -17.115143, 0.37126452, 41.617634, -17.594984, 0.37126452, 41.46255, -16.691502, 0.37126452, 41.174942, -17.594984, 0.37126452, 41.46255, -17.592882, 0.37126452, 41.149685, -16.698133, 0.0055906773, 42.16154, -17.599485, 0.3712644, 42.132355, -16.698133, 0.3712644, 42.16154, -16.698133, 0.0055906773, 42.16154, -17.599485, 0.0055906773, 42.132355, -17.599485, 0.3712644, 42.132355, -9.259279, 0.3712644, 42.03524, -16.698133, 0.0055906773, 42.16154, -16.698133, 0.3712644, 42.16154, -9.259279, 0.3712644, 42.03524, -9.259279, 0.005590737, 42.03524, -16.698133, 0.0055906773, 42.16154, -17.592882, 0.0055907965, 41.149685, -16.698133, 0.0055906773, 42.16154, -16.691502, 0.0055907965, 41.174942, -16.698133, 0.0055906773, 42.16154, -17.592882, 0.0055907965, 41.149685, -17.59645, 0.005590737, 41.68073, -16.698133, 0.0055906773, 42.16154, -17.59645, 0.005590737, 41.68073, -17.599485, 0.0055906773, 42.132355, -9.259279, 0.005590737, 42.03524, -16.691502, 0.0055907965, 41.174942, -16.698133, 0.0055906773, 42.16154, -9.259279, 0.005590737, 42.03524, -9.253704, 0.0055907965, 41.20573, -16.691502, 0.0055907965, 41.174942, -17.594984, 0.37126452, 41.46255, -17.513622, 0.23162776, 41.628048, -17.59645, 0.005590737, 41.68073, -17.513622, 0.23162776, 41.628048, -17.596823, 0.37126452, 41.736145, -17.59645, 0.005590737, 41.68073, -17.592882, 0.0055907965, 41.149685, -17.594984, 0.37126452, 41.46255, -17.59645, 0.005590737, 41.68073, -17.592882, 0.0055907965, 41.149685, -17.592882, 0.37126452, 41.149685, -17.594984, 0.37126452, 41.46255, -17.596823, 0.37126452, 41.736145, -17.599485, 0.0055906773, 42.132355, -17.59645, 0.005590737, 41.68073, -17.596823, 0.37126452, 41.736145, -17.599485, 0.3712644, 42.132355, -17.599485, 0.0055906773, 42.132355, -9.259279, 0.3712644, 42.03524, -9.253704, 0.0055907965, 41.20573, -9.259279, 0.005590737, 42.03524, -9.253704, 0.0055907965, 41.20573, -9.259279, 0.3712644, 42.03524, -9.255098, 0.37126452, 41.413, -9.253704, 0.0055907965, 41.20573, -9.255098, 0.37126452, 41.413, -9.253704, 0.28006417, 41.20573, -9.466791, -0.19077104, 22.980423, -9.698496, -0.09826177, 22.978865, -9.698496, -0.19077104, 22.978865, -9.466791, -0.19077104, 22.980423, -9.466791, -0.09826177, 22.980423, -9.698496, -0.09826177, 22.978865, -9.466791, -0.09826177, 22.980423, -9.864088, -0.098264754, 47.618515, -9.698496, -0.09826177, 22.978865, -9.466791, -0.09826177, 22.980423, -9.632383, -0.098264754, 47.62007, -9.864088, -0.098264754, 47.618515, -9.632383, -0.098264754, 47.62007, -9.864088, -0.19077402, 47.618515, -9.864088, -0.098264754, 47.618515, -9.632383, -0.098264754, 47.62007, -9.632383, -0.19077402, 47.62007, -9.864088, -0.19077402, 47.618515, -9.632383, -0.19077402, 47.62007, -9.698496, -0.19077104, 22.978865, -9.864088, -0.19077402, 47.618515, -9.632383, -0.19077402, 47.62007, -9.466791, -0.19077104, 22.980423, -9.698496, -0.19077104, 22.978865, -9.698496, -0.19077104, 22.978865, -9.864088, -0.098264754, 47.618515, -9.864088, -0.19077402, 47.618515, -9.698496, -0.19077104, 22.978865, -9.698496, -0.09826177, 22.978865, -9.864088, -0.098264754, 47.618515, -9.632383, -0.19077402, 47.62007, -9.466791, -0.09826177, 22.980423, -9.466791, -0.19077104, 22.980423, -9.632383, -0.19077402, 47.62007, -9.632383, -0.098264754, 47.62007, -9.466791, -0.09826177, 22.980423, -10.65929, 0.00559026, 45.949276, -17.916506, 0.37126392, 46.05978, -10.65929, 0.37126392, 45.949276, -10.65929, 0.00559026, 45.949276, -17.916506, 0.0055902004, 46.05978, -17.916506, 0.37126392, 46.05978, -8.99464, 0.37126392, 45.967007, -10.65929, 0.00559026, 45.949276, -10.65929, 0.37126392, 45.949276, -8.99464, 0.37126392, 45.967007, -8.99464, 0.00559026, 45.967007, -10.65929, 0.00559026, 45.949276, -10.65251, 0.0055903792, 44.940422, -17.916506, 0.0055902004, 46.05978, -10.65929, 0.00559026, 45.949276, -10.65251, 0.0055903792, 44.940422, -17.909903, 0.0055903196, 45.07711, -17.916506, 0.0055902004, 46.05978, -8.99464, 0.00559026, 45.967007, -10.65251, 0.0055903792, 44.940422, -10.65929, 0.00559026, 45.949276, -8.99464, 0.00559026, 45.967007, -8.989063, 0.0055903196, 45.137062, -10.65251, 0.0055903792, 44.940422, -8.99464, 0.00559026, 45.967007, -8.989063, 0.37126404, 45.137062, -8.989063, 0.0055903196, 45.137062, -8.99464, 0.00559026, 45.967007, -8.99464, 0.37126392, 45.967007, -8.989063, 0.37126404, 45.137062, -10.654205, 0.37126404, 45.192635, -10.65251, 0.2800637, 44.940422, -10.236538, 0.37126404, 44.98947, -10.654205, 0.37126404, 45.192635, -12.466749, 0.37126404, 44.974487, -10.65251, 0.2800637, 44.940422, -10.654205, 0.37126404, 45.192635, -16.549116, 0.37126404, 45.05178, -12.466749, 0.37126404, 44.974487, -10.236538, 0.37126404, 44.98947, -10.65929, 0.37126392, 45.949276, -10.654205, 0.37126404, 45.192635, -10.236538, 0.37126404, 44.98947, -8.99464, 0.37126392, 45.967007, -10.65929, 0.37126392, 45.949276, -8.99464, 0.37126392, 45.967007, -10.236538, 0.37126404, 44.98947, -8.989063, 0.37126404, 45.137062, -17.916506, 0.37126392, 46.05978, -10.654205, 0.37126404, 45.192635, -10.65929, 0.37126392, 45.949276, -10.654205, 0.37126404, 45.192635, -17.916506, 0.37126392, 46.05978, -16.549116, 0.37126404, 45.05178, -16.549116, 0.37126404, 45.05178, -17.916506, 0.37126392, 46.05978, -17.911556, 0.37126404, 45.32321, -16.549116, 0.37126404, 45.05178, -17.911556, 0.37126404, 45.32321, -17.909903, 0.2800637, 45.07711, -8.989063, 0.37126404, 45.137062, -10.65251, 0.0055903792, 44.940422, -8.989063, 0.0055903196, 45.137062, -10.65251, 0.0055903792, 44.940422, -8.989063, 0.37126404, 45.137062, -10.236538, 0.37126404, 44.98947, -10.65251, 0.0055903792, 44.940422, -10.236538, 0.37126404, 44.98947, -10.65251, 0.2800637, 44.940422, -10.65251, 0.2800637, 44.940422, -17.909903, 0.0055903196, 45.07711, -10.65251, 0.0055903792, 44.940422, -10.65251, 0.2800637, 44.940422, -16.549116, 0.37126404, 45.05178, -17.909903, 0.0055903196, 45.07711, -17.909903, 0.0055903196, 45.07711, -16.549116, 0.37126404, 45.05178, -17.909903, 0.2800637, 45.07711, -16.549116, 0.37126404, 45.05178, -10.65251, 0.2800637, 44.940422, -12.466749, 0.37126404, 44.974487, -17.909903, 0.2800637, 45.07711, -17.916506, 0.0055902004, 46.05978, -17.909903, 0.0055903196, 45.07711, -17.916506, 0.0055902004, 46.05978, -17.909903, 0.2800637, 45.07711, -17.911556, 0.37126404, 45.32321, -17.916506, 0.0055902004, 46.05978, -17.911556, 0.37126404, 45.32321, -17.916506, 0.37126392, 46.05978, -9.086136, -3.458273, 47.63378, -9.862558, 1.270175, 47.455757, -9.690924, -3.458273, 47.629715, -9.086136, -3.458273, 47.63378, -8.912178, 1.270175, 47.462143, -9.862558, 1.270175, 47.455757, -8.912178, 1.270175, 47.462143, -9.868942, 1.2701747, 48.4057, -9.862558, 1.270175, 47.455757, -8.912178, 1.270175, 47.462143, -8.918562, 1.2701747, 48.41209, -9.868942, 1.2701747, 48.4057, -8.918562, 1.2701747, 48.41209, -9.694984, -3.458273, 48.234066, -9.868942, 1.2701747, 48.4057, -8.918562, 1.2701747, 48.41209, -9.090197, -3.458273, 48.238132, -9.694984, -3.458273, 48.234066, -9.090197, -3.458273, 48.238132, -9.690924, -3.458273, 47.629715, -9.694984, -3.458273, 48.234066, -9.090197, -3.458273, 48.238132, -9.086136, -3.458273, 47.63378, -9.690924, -3.458273, 47.629715, -9.690924, -3.458273, 47.629715, -9.868942, 1.2701747, 48.4057, -9.694984, -3.458273, 48.234066, -9.690924, -3.458273, 47.629715, -9.862558, 1.270175, 47.455757, -9.868942, 1.2701747, 48.4057, -9.090197, -3.458273, 48.238132, -8.912178, 1.270175, 47.462143, -9.086136, -3.458273, 47.63378, -9.090197, -3.458273, 48.238132, -8.918562, 1.2701747, 48.41209, -8.912178, 1.270175, 47.462143, -16.72666, 0.37126392, 46.406403, -17.628038, 0.0055902004, 46.380707, -16.72666, 0.0055902004, 46.406403, -16.72666, 0.37126392, 46.406403, -17.628038, 0.37126392, 46.380707, -17.628038, 0.0055902004, 46.380707, -16.72666, 0.0055902004, 46.406403, -11.148199, 0.37126392, 46.429054, -16.72666, 0.37126392, 46.406403, -16.72666, 0.0055902004, 46.406403, -9.28886, 0.28006357, 46.436752, -11.148199, 0.37126392, 46.429054, -9.28886, 0.28006357, 46.436752, -16.72666, 0.0055902004, 46.406403, -9.28886, 0.0055902004, 46.436752, -17.634645, 0.3712638, 47.363815, -16.72666, 0.37126392, 46.406403, -16.733288, 0.3712638, 47.392563, -11.148199, 0.37126392, 46.429054, -16.733288, 0.3712638, 47.392563, -16.72666, 0.37126392, 46.406403, -11.148199, 0.37126392, 46.429054, -9.294437, 0.3712638, 47.266697, -16.733288, 0.3712638, 47.392563, -9.294437, 0.3712638, 47.266697, -11.148199, 0.37126392, 46.429054, -9.2902565, 0.37126392, 46.644455, -9.2902565, 0.37126392, 46.644455, -11.148199, 0.37126392, 46.429054, -9.28886, 0.28006357, 46.436752, -16.72666, 0.37126392, 46.406403, -17.634645, 0.3712638, 47.363815, -17.150305, 0.3712638, 46.84953, -17.150305, 0.3712638, 46.84953, -17.634645, 0.3712638, 47.363815, -17.631985, 0.3712638, 46.96804, -17.631985, 0.3712638, 46.96804, -17.548779, 0.23162705, 46.85907, -17.150305, 0.3712638, 46.84953, -17.548779, 0.23162705, 46.85907, -17.63014, 0.37126392, 46.693573, -17.150305, 0.3712638, 46.84953, -16.72666, 0.37126392, 46.406403, -17.150305, 0.3712638, 46.84953, -17.63014, 0.37126392, 46.693573, -16.72666, 0.37126392, 46.406403, -17.63014, 0.37126392, 46.693573, -17.628038, 0.37126392, 46.380707, -16.733288, 0.005590081, 47.392563, -17.634645, 0.3712638, 47.363815, -16.733288, 0.3712638, 47.392563, -16.733288, 0.005590081, 47.392563, -17.634645, 0.005590081, 47.363815, -17.634645, 0.3712638, 47.363815, -9.294437, 0.3712638, 47.266697, -16.733288, 0.005590081, 47.392563, -16.733288, 0.3712638, 47.392563, -9.294437, 0.3712638, 47.266697, -9.294437, 0.005590081, 47.266697, -16.733288, 0.005590081, 47.392563, -17.628038, 0.0055902004, 46.380707, -16.733288, 0.005590081, 47.392563, -16.72666, 0.0055902004, 46.406403, -16.733288, 0.005590081, 47.392563, -17.628038, 0.0055902004, 46.380707, -17.631613, 0.005590141, 46.912624, -16.733288, 0.005590081, 47.392563, -17.631613, 0.005590141, 46.912624, -17.634645, 0.005590081, 47.363815, -9.294437, 0.005590081, 47.266697, -16.72666, 0.0055902004, 46.406403, -16.733288, 0.005590081, 47.392563, -9.294437, 0.005590081, 47.266697, -9.28886, 0.0055902004, 46.436752, -16.72666, 0.0055902004, 46.406403, -17.63014, 0.37126392, 46.693573, -17.548779, 0.23162705, 46.85907, -17.631613, 0.005590141, 46.912624, -17.548779, 0.23162705, 46.85907, -17.631985, 0.3712638, 46.96804, -17.631613, 0.005590141, 46.912624, -17.628038, 0.0055902004, 46.380707, -17.63014, 0.37126392, 46.693573, -17.631613, 0.005590141, 46.912624, -17.628038, 0.0055902004, 46.380707, -17.628038, 0.37126392, 46.380707, -17.63014, 0.37126392, 46.693573, -17.631985, 0.3712638, 46.96804, -17.634645, 0.005590081, 47.363815, -17.631613, 0.005590141, 46.912624, -17.631985, 0.3712638, 46.96804, -17.634645, 0.3712638, 47.363815, -17.634645, 0.005590081, 47.363815, -9.294437, 0.3712638, 47.266697, -9.28886, 0.0055902004, 46.436752, -9.294437, 0.005590081, 47.266697, -9.28886, 0.0055902004, 46.436752, -9.294437, 0.3712638, 47.266697, -9.2902565, 0.37126392, 46.644455, -9.28886, 0.0055902004, 46.436752, -9.2902565, 0.37126392, 46.644455, -9.28886, 0.28006357, 46.436752, -16.91135, 0.37126464, 39.993507, -17.875563, 0.0055909753, 39.967392, -16.91135, 0.0055909753, 39.993507, -16.91135, 0.37126464, 39.993507, -17.875563, 0.37126464, 39.967392, -17.875563, 0.0055909753, 39.967392, -16.91135, 0.0055909753, 39.993507, -10.943661, 0.37126464, 40.01878, -16.91135, 0.37126464, 39.993507, -16.91135, 0.0055909753, 39.993507, -8.954723, 0.28006428, 40.027344, -10.943661, 0.37126464, 40.01878, -8.954723, 0.28006428, 40.027344, -16.91135, 0.0055909753, 39.993507, -8.954723, 0.0055909753, 40.027344, -17.882166, 0.37126452, 40.95006, -16.91135, 0.37126464, 39.993507, -16.917978, 0.37126452, 40.97967, -10.943661, 0.37126464, 40.01878, -16.917978, 0.37126452, 40.97967, -16.91135, 0.37126464, 39.993507, -10.943661, 0.37126464, 40.01878, -8.9603, 0.37126452, 40.857292, -16.917978, 0.37126452, 40.97967, -8.9603, 0.37126452, 40.857292, -10.943661, 0.37126464, 40.01878, -8.95612, 0.37126464, 40.23505, -8.95612, 0.37126464, 40.23505, -10.943661, 0.37126464, 40.01878, -8.954723, 0.28006428, 40.027344, -16.91135, 0.37126464, 39.993507, -17.882166, 0.37126452, 40.95006, -17.364662, 0.37126464, 40.436, -17.364662, 0.37126464, 40.436, -17.882166, 0.37126452, 40.95006, -17.879507, 0.37126464, 40.55429, -17.879507, 0.37126464, 40.55429, -17.79063, 0.23162788, 40.445793, -17.364662, 0.37126464, 40.436, -17.79063, 0.23162788, 40.445793, -17.877665, 0.37126464, 40.28026, -17.364662, 0.37126464, 40.436, -16.91135, 0.37126464, 39.993507, -17.364662, 0.37126464, 40.436, -17.877665, 0.37126464, 40.28026, -16.91135, 0.37126464, 39.993507, -17.877665, 0.37126464, 40.28026, -17.875563, 0.37126464, 39.967392, -16.917978, 0.005590856, 40.97967, -17.882166, 0.37126452, 40.95006, -16.917978, 0.37126452, 40.97967, -16.917978, 0.005590856, 40.97967, -17.882166, 0.005590856, 40.95006, -17.882166, 0.37126452, 40.95006, -8.9603, 0.37126452, 40.857292, -16.917978, 0.005590856, 40.97967, -16.917978, 0.37126452, 40.97967, -8.9603, 0.37126452, 40.857292, -8.9603, 0.005590856, 40.857292, -16.917978, 0.005590856, 40.97967, -17.875563, 0.0055909753, 39.967392, -16.917978, 0.005590856, 40.97967, -16.91135, 0.0055909753, 39.993507, -16.917978, 0.005590856, 40.97967, -17.875563, 0.0055909753, 39.967392, -17.879133, 0.0055909157, 40.49887, -16.917978, 0.005590856, 40.97967, -17.879133, 0.0055909157, 40.49887, -17.882166, 0.005590856, 40.95006, -8.9603, 0.005590856, 40.857292, -16.91135, 0.0055909753, 39.993507, -16.917978, 0.005590856, 40.97967, -8.9603, 0.005590856, 40.857292, -8.954723, 0.0055909753, 40.027344, -16.91135, 0.0055909753, 39.993507, -17.877665, 0.37126464, 40.28026, -17.79063, 0.23162788, 40.445793, -17.879133, 0.0055909157, 40.49887, -17.79063, 0.23162788, 40.445793, -17.879507, 0.37126464, 40.55429, -17.879133, 0.0055909157, 40.49887, -17.875563, 0.0055909753, 39.967392, -17.877665, 0.37126464, 40.28026, -17.879133, 0.0055909157, 40.49887, -17.875563, 0.0055909753, 39.967392, -17.875563, 0.37126464, 39.967392, -17.877665, 0.37126464, 40.28026, -17.879507, 0.37126464, 40.55429, -17.882166, 0.005590856, 40.95006, -17.879133, 0.0055909157, 40.49887, -17.879507, 0.37126464, 40.55429, -17.882166, 0.37126452, 40.95006, -17.882166, 0.005590856, 40.95006, -8.9603, 0.37126452, 40.857292, -8.954723, 0.0055909753, 40.027344, -8.9603, 0.005590856, 40.857292, -8.954723, 0.0055909753, 40.027344, -8.9603, 0.37126452, 40.857292, -8.95612, 0.37126464, 40.23505, -8.954723, 0.0055909753, 40.027344, -8.95612, 0.37126464, 40.23505, -8.954723, 0.28006428, 40.027344, -13.4819145, 0.005591035, 39.478943, -17.873325, 0.37126476, 39.634453, -13.4819145, 0.37126476, 39.478943, -13.4819145, 0.005591035, 39.478943, -17.873325, 0.0055909753, 39.634453, -17.873325, 0.37126476, 39.634453, -8.952486, 0.37126476, 39.694405, -13.4819145, 0.005591035, 39.478943, -13.4819145, 0.37126476, 39.478943, -8.952486, 0.37126476, 39.694405, -8.952486, 0.0055909753, 39.694405, -13.4819145, 0.005591035, 39.478943, -13.47533, 0.005591154, 38.49933, -17.873325, 0.0055909753, 39.634453, -13.4819145, 0.005591035, 39.478943, -13.47533, 0.005591154, 38.49933, -17.867746, 0.0055910945, 38.80451, -17.873325, 0.0055909753, 39.634453, -13.4819145, 0.005591035, 39.478943, -8.944866, 0.005591154, 38.560757, -13.47533, 0.005591154, 38.49933, -13.4819145, 0.005591035, 39.478943, -8.948738, 0.005591035, 39.136745, -8.944866, 0.005591154, 38.560757, -8.948738, 0.005591035, 39.136745, -13.4819145, 0.005591035, 39.478943, -8.952486, 0.0055909753, 39.694405, -17.867746, 0.0055910945, 38.80451, -17.873325, 0.37126476, 39.634453, -17.873325, 0.0055909753, 39.634453, -17.867746, 0.0055910945, 38.80451, -17.867746, 0.37126476, 38.80451, -17.873325, 0.37126476, 39.634453, -8.949188, 0.37126476, 39.203945, -9.119587, 0.26217347, 39.105488, -8.948738, 0.005591035, 39.136745, -13.476976, 0.37126487, 38.74412, -13.47533, 0.28006452, 38.49933, -12.3426075, 0.37126487, 38.514793, -13.476976, 0.37126487, 38.74412, -14.573328, 0.37126487, 38.575733, -13.47533, 0.28006452, 38.49933, -13.476976, 0.37126487, 38.74412, -17.867746, 0.37126476, 38.80451, -14.573328, 0.37126487, 38.575733, -13.476976, 0.37126487, 38.74412, -17.873325, 0.37126476, 39.634453, -17.867746, 0.37126476, 38.80451, -17.873325, 0.37126476, 39.634453, -13.476976, 0.37126487, 38.74412, -13.4819145, 0.37126476, 39.478943, -12.3426075, 0.37126487, 38.514793, -13.4819145, 0.37126476, 39.478943, -13.476976, 0.37126487, 38.74412, -13.4819145, 0.37126476, 39.478943, -8.949188, 0.37126476, 39.203945, -8.952486, 0.37126476, 39.694405, -13.4819145, 0.37126476, 39.478943, -9.726973, 0.37126476, 39.09835, -8.949188, 0.37126476, 39.203945, -12.3426075, 0.37126487, 38.514793, -9.726973, 0.37126476, 39.09835, -13.4819145, 0.37126476, 39.478943, -9.119587, 0.26217347, 39.105488, -8.949188, 0.37126476, 39.203945, -9.726973, 0.37126476, 39.09835, -8.94733, 0.37126476, 38.927296, -9.119587, 0.26217347, 39.105488, -9.726973, 0.37126476, 39.09835, -9.726973, 0.37126476, 39.09835, -12.3426075, 0.37126487, 38.514793, -8.94733, 0.37126476, 38.927296, -8.94733, 0.37126476, 38.927296, -12.3426075, 0.37126487, 38.514793, -8.944866, 0.37126487, 38.560757, -8.952486, 0.0055909753, 39.694405, -8.949188, 0.37126476, 39.203945, -8.948738, 0.005591035, 39.136745, -8.952486, 0.0055909753, 39.694405, -8.952486, 0.37126476, 39.694405, -8.949188, 0.37126476, 39.203945, -8.94733, 0.37126476, 38.927296, -8.944866, 0.005591154, 38.560757, -8.948738, 0.005591035, 39.136745, -8.94733, 0.37126476, 38.927296, -8.944866, 0.37126487, 38.560757, -8.944866, 0.005591154, 38.560757, -9.119587, 0.26217347, 39.105488, -8.94733, 0.37126476, 38.927296, -8.948738, 0.005591035, 39.136745, -8.944866, 0.37126487, 38.560757, -13.47533, 0.005591154, 38.49933, -8.944866, 0.005591154, 38.560757, -13.47533, 0.005591154, 38.49933, -8.944866, 0.37126487, 38.560757, -12.3426075, 0.37126487, 38.514793, -13.47533, 0.005591154, 38.49933, -12.3426075, 0.37126487, 38.514793, -13.47533, 0.28006452, 38.49933, -13.47533, 0.28006452, 38.49933, -17.867746, 0.0055910945, 38.80451, -13.47533, 0.005591154, 38.49933, -13.47533, 0.28006452, 38.49933, -17.867746, 0.37126476, 38.80451, -17.867746, 0.0055910945, 38.80451, -17.867746, 0.37126476, 38.80451, -13.47533, 0.28006452, 38.49933, -14.573328, 0.37126487, 38.575733, -16.664772, 0.371265, 37.19757, -17.566149, 0.0055912733, 37.171875, -16.664772, 0.0055912733, 37.19757, -16.664772, 0.371265, 37.19757, -17.566149, 0.371265, 37.171875, -17.566149, 0.0055912733, 37.171875, -16.664772, 0.0055912733, 37.19757, -11.086311, 0.371265, 37.220222, -16.664772, 0.371265, 37.19757, -16.664772, 0.0055912733, 37.19757, -9.226971, 0.28006464, 37.227917, -11.086311, 0.371265, 37.220222, -9.226971, 0.28006464, 37.227917, -16.664772, 0.0055912733, 37.19757, -9.226971, 0.0055912733, 37.227917, -17.572756, 0.37126487, 38.154984, -16.664772, 0.371265, 37.19757, -16.6714, 0.37126487, 38.18373, -11.086311, 0.371265, 37.220222, -16.6714, 0.37126487, 38.18373, -16.664772, 0.371265, 37.19757, -11.086311, 0.371265, 37.220222, -9.23255, 0.37126487, 38.057865, -16.6714, 0.37126487, 38.18373, -9.23255, 0.37126487, 38.057865, -11.086311, 0.371265, 37.220222, -9.228367, 0.371265, 37.435623, -9.228367, 0.371265, 37.435623, -11.086311, 0.371265, 37.220222, -9.226971, 0.28006464, 37.227917, -16.664772, 0.371265, 37.19757, -17.572756, 0.37126487, 38.154984, -17.088415, 0.371265, 37.640697, -17.088415, 0.371265, 37.640697, -17.572756, 0.37126487, 38.154984, -17.570097, 0.371265, 37.75921, -17.570097, 0.371265, 37.75921, -17.486889, 0.23162824, 37.650238, -17.088415, 0.371265, 37.640697, -17.486889, 0.23162824, 37.650238, -17.56825, 0.371265, 37.48474, -17.088415, 0.371265, 37.640697, -16.664772, 0.371265, 37.19757, -17.088415, 0.371265, 37.640697, -17.56825, 0.371265, 37.48474, -16.664772, 0.371265, 37.19757, -17.56825, 0.371265, 37.48474, -17.566149, 0.371265, 37.171875, -16.6714, 0.005591154, 38.18373, -17.572756, 0.37126487, 38.154984, -16.6714, 0.37126487, 38.18373, -16.6714, 0.005591154, 38.18373, -17.572756, 0.005591154, 38.154984, -17.572756, 0.37126487, 38.154984, -9.23255, 0.37126487, 38.057865, -16.6714, 0.005591154, 38.18373, -16.6714, 0.37126487, 38.18373, -9.23255, 0.37126487, 38.057865, -9.23255, 0.0055912137, 38.057865, -16.6714, 0.005591154, 38.18373, -17.566149, 0.0055912733, 37.171875, -16.6714, 0.005591154, 38.18373, -16.664772, 0.0055912733, 37.19757, -16.6714, 0.005591154, 38.18373, -17.566149, 0.0055912733, 37.171875, -17.569721, 0.0055912137, 37.703354, -16.6714, 0.005591154, 38.18373, -17.569721, 0.0055912137, 37.703354, -17.572756, 0.005591154, 38.154984, -9.23255, 0.0055912137, 38.057865, -16.664772, 0.0055912733, 37.19757, -16.6714, 0.005591154, 38.18373, -9.23255, 0.0055912137, 38.057865, -9.226971, 0.0055912733, 37.227917, -16.664772, 0.0055912733, 37.19757, -17.56825, 0.371265, 37.48474, -17.486889, 0.23162824, 37.650238, -17.569721, 0.0055912137, 37.703354, -17.486889, 0.23162824, 37.650238, -17.570097, 0.371265, 37.75921, -17.569721, 0.0055912137, 37.703354, -17.566149, 0.0055912733, 37.171875, -17.56825, 0.371265, 37.48474, -17.569721, 0.0055912137, 37.703354, -17.566149, 0.0055912733, 37.171875, -17.566149, 0.371265, 37.171875, -17.56825, 0.371265, 37.48474, -17.570097, 0.371265, 37.75921, -17.572756, 0.005591154, 38.154984, -17.569721, 0.0055912137, 37.703354, -17.570097, 0.371265, 37.75921, -17.572756, 0.37126487, 38.154984, -17.572756, 0.005591154, 38.154984, -9.23255, 0.37126487, 38.057865, -9.226971, 0.0055912733, 37.227917, -9.23255, 0.0055912137, 38.057865, -9.226971, 0.0055912733, 37.227917, -9.23255, 0.37126487, 38.057865, -9.228367, 0.371265, 37.435623, -9.226971, 0.0055912733, 37.227917, -9.228367, 0.371265, 37.435623, -9.226971, 0.28006464, 37.227917, -16.810944, -3.458273, 47.519463, -17.46208, 1.270175, 47.59407, -17.54053, -3.458273, 47.51456, -16.810944, -3.458273, 47.519463, -16.890453, 1.270175, 47.59791, -17.46208, 1.270175, 47.59407, -16.890453, 1.270175, 47.59791, -17.465921, 1.270175, 48.165695, -17.46208, 1.270175, 47.59407, -16.890453, 1.270175, 47.59791, -16.894295, 1.270175, 48.16954, -17.465921, 1.270175, 48.165695, -16.894295, 1.270175, 48.16954, -17.545431, -3.458273, 48.24415, -17.465921, 1.270175, 48.165695, -16.894295, 1.270175, 48.16954, -16.815845, -3.458273, 48.24905, -17.545431, -3.458273, 48.24415, -16.815845, -3.458273, 48.24905, -17.54053, -3.458273, 47.51456, -17.545431, -3.458273, 48.24415, -16.815845, -3.458273, 48.24905, -16.810944, -3.458273, 47.519463, -17.54053, -3.458273, 47.51456, -17.54053, -3.458273, 47.51456, -17.465921, 1.270175, 48.165695, -17.545431, -3.458273, 48.24415, -17.54053, -3.458273, 47.51456, -17.46208, 1.270175, 47.59407, -17.465921, 1.270175, 48.165695, -16.815845, -3.458273, 48.24905, -16.890453, 1.270175, 47.59791, -16.810944, -3.458273, 47.519463, -16.815845, -3.458273, 48.24905, -16.894295, 1.270175, 48.16954, -16.890453, 1.270175, 47.59791, -9.055229, -3.45827, 22.581722, -9.785252, 1.2701778, 22.576817, -9.785252, -3.45827, 22.576817, -9.055229, -3.45827, 22.581722, -9.055229, 1.2701778, 22.581722, -9.785252, 1.2701778, 22.576817, -9.060133, -3.45827, 23.31131, -9.785252, -3.45827, 22.576817, -9.790155, -3.45827, 23.306404, -9.060133, -3.45827, 23.31131, -9.055229, -3.45827, 22.581722, -9.785252, -3.45827, 22.576817, -9.785252, -3.45827, 22.576817, -9.790155, 1.2701778, 23.306404, -9.790155, -3.45827, 23.306404, -9.785252, -3.45827, 22.576817, -9.785252, 1.2701778, 22.576817, -9.790155, 1.2701778, 23.306404, -9.060133, 0.08806592, 23.31131, -9.242529, 1.2701778, 23.310083, -9.058907, 1.2701778, 23.128912, -9.055229, -3.45827, 22.581722, -9.060133, 0.08806592, 23.31131, -9.058907, 1.2701778, 23.128912, -9.060133, 0.08806592, 23.31131, -9.055229, -3.45827, 22.581722, -9.060133, -3.45827, 23.31131, -9.055229, -3.45827, 22.581722, -9.058907, 1.2701778, 23.128912, -9.055229, 1.2701778, 22.581722, -9.058907, 1.2701778, 23.128912, -9.785252, 1.2701778, 22.576817, -9.055229, 1.2701778, 22.581722, -9.785252, 1.2701778, 22.576817, -9.058907, 1.2701778, 23.128912, -9.242529, 1.2701778, 23.310083, -9.785252, 1.2701778, 22.576817, -9.242529, 1.2701778, 23.310083, -9.790155, 1.2701778, 23.306404, -9.060133, -3.45827, 23.31131, -9.242529, 1.2701778, 23.310083, -9.060133, 0.08806592, 23.31131, -9.242529, 1.2701778, 23.310083, -9.060133, -3.45827, 23.31131, -9.790155, -3.45827, 23.306404, -9.242529, 1.2701778, 23.310083, -9.790155, -3.45827, 23.306404, -9.790155, 1.2701778, 23.306404, -13.449499, 0.0055915117, 35.369823, -17.5551, 0.37126523, 35.52769, -13.449499, 0.37126523, 35.369823, -13.449499, 0.0055915117, 35.369823, -17.5551, 0.0055915117, 35.52769, -17.5551, 0.37126523, 35.52769, -9.215921, 0.37126523, 35.583733, -13.449499, 0.0055915117, 35.369823, -13.449499, 0.37126523, 35.369823, -9.215921, 0.37126523, 35.583733, -9.215921, 0.005591452, 35.583733, -13.449499, 0.0055915117, 35.369823, -13.442921, 0.005591631, 34.39108, -17.5551, 0.0055915117, 35.52769, -13.449499, 0.0055915117, 35.369823, -13.442921, 0.005591631, 34.39108, -17.54952, 0.0055915713, 34.697742, -17.5551, 0.0055915117, 35.52769, -13.449499, 0.0055915117, 35.369823, -9.208305, 0.005591631, 34.45052, -13.442921, 0.005591631, 34.39108, -13.449499, 0.0055915117, 35.369823, -9.212177, 0.0055915713, 35.02651, -9.208305, 0.005591631, 34.45052, -9.212177, 0.0055915713, 35.02651, -13.449499, 0.0055915117, 35.369823, -9.215921, 0.005591452, 35.583733, -17.54952, 0.0055915713, 34.697742, -17.5551, 0.37126523, 35.52769, -17.5551, 0.0055915117, 35.52769, -17.54952, 0.0055915713, 34.697742, -17.54952, 0.37126535, 34.697742, -17.5551, 0.37126523, 35.52769, -9.212622, 0.37126523, 35.092834, -9.371677, 0.26217395, 34.99489, -9.212177, 0.0055915713, 35.02651, -13.444563, 0.37126535, 34.635437, -13.442921, 0.280065, 34.39108, -12.384375, 0.37126535, 34.40561, -13.444563, 0.37126535, 34.635437, -14.469788, 0.37126535, 34.467525, -13.442921, 0.280065, 34.39108, -13.444563, 0.37126535, 34.635437, -17.54952, 0.37126535, 34.697742, -14.469788, 0.37126535, 34.467525, -13.444563, 0.37126535, 34.635437, -17.5551, 0.37126523, 35.52769, -17.54952, 0.37126535, 34.697742, -17.5551, 0.37126523, 35.52769, -13.444563, 0.37126535, 34.635437, -13.449499, 0.37126523, 35.369823, -12.384375, 0.37126535, 34.40561, -13.449499, 0.37126523, 35.369823, -13.444563, 0.37126535, 34.635437, -13.449499, 0.37126523, 35.369823, -9.212622, 0.37126523, 35.092834, -9.215921, 0.37126523, 35.583733, -13.449499, 0.37126523, 35.369823, -9.939795, 0.37126523, 34.988453, -9.212622, 0.37126523, 35.092834, -12.384375, 0.37126535, 34.40561, -9.939795, 0.37126523, 34.988453, -13.449499, 0.37126523, 35.369823, -9.371677, 0.26217395, 34.99489, -9.212622, 0.37126523, 35.092834, -9.939795, 0.37126523, 34.988453, -9.21077, 0.37126535, 34.81706, -9.371677, 0.26217395, 34.99489, -9.939795, 0.37126523, 34.988453, -9.939795, 0.37126523, 34.988453, -12.384375, 0.37126535, 34.40561, -9.21077, 0.37126535, 34.81706, -9.21077, 0.37126535, 34.81706, -12.384375, 0.37126535, 34.40561, -9.208305, 0.37126535, 34.45052, -9.215921, 0.005591452, 35.583733, -9.212622, 0.37126523, 35.092834, -9.212177, 0.0055915713, 35.02651, -9.215921, 0.005591452, 35.583733, -9.215921, 0.37126523, 35.583733, -9.212622, 0.37126523, 35.092834, -9.21077, 0.37126535, 34.81706, -9.208305, 0.005591631, 34.45052, -9.212177, 0.0055915713, 35.02651, -9.21077, 0.37126535, 34.81706, -9.208305, 0.37126535, 34.45052, -9.208305, 0.005591631, 34.45052, -9.371677, 0.26217395, 34.99489, -9.21077, 0.37126535, 34.81706, -9.212177, 0.0055915713, 35.02651, -9.208305, 0.37126535, 34.45052, -13.442921, 0.005591631, 34.39108, -9.208305, 0.005591631, 34.45052, -13.442921, 0.005591631, 34.39108, -9.208305, 0.37126535, 34.45052, -12.384375, 0.37126535, 34.40561, -13.442921, 0.005591631, 34.39108, -12.384375, 0.37126535, 34.40561, -13.442921, 0.280065, 34.39108, -13.442921, 0.280065, 34.39108, -17.54952, 0.0055915713, 34.697742, -13.442921, 0.005591631, 34.39108, -13.442921, 0.280065, 34.39108, -17.54952, 0.37126535, 34.697742, -17.54952, 0.0055915713, 34.697742, -17.54952, 0.37126535, 34.697742, -13.442921, 0.280065, 34.39108, -14.469788, 0.37126535, 34.467525, -10.702703, 0.005590558, 43.383537, -17.802399, 0.37126428, 43.495537, -10.702703, 0.37126428, 43.383537, -10.702703, 0.005590558, 43.383537, -17.802399, 0.005590558, 43.495537, -17.802399, 0.37126428, 43.495537, -9.074274, 0.37126428, 43.401463, -10.702703, 0.005590558, 43.383537, -10.702703, 0.37126428, 43.383537, -9.074274, 0.37126428, 43.401463, -9.074274, 0.005590558, 43.401463, -10.702703, 0.005590558, 43.383537, -10.695927, 0.0055906773, 42.375122, -17.802399, 0.005590558, 43.495537, -10.702703, 0.005590558, 43.383537, -10.695927, 0.0055906773, 42.375122, -17.795794, 0.0055906773, 42.512863, -17.802399, 0.005590558, 43.495537, -9.074274, 0.005590558, 43.401463, -10.695927, 0.0055906773, 42.375122, -10.702703, 0.005590558, 43.383537, -9.074274, 0.005590558, 43.401463, -9.068697, 0.0055906773, 42.571514, -10.695927, 0.0055906773, 42.375122, -9.074274, 0.005590558, 43.401463, -9.068697, 0.3712644, 42.571514, -9.068697, 0.0055906773, 42.571514, -9.074274, 0.005590558, 43.401463, -9.074274, 0.37126428, 43.401463, -9.068697, 0.3712644, 42.571514, -10.697622, 0.3712644, 42.627335, -10.695927, 0.28006405, 42.375122, -10.289118, 0.3712644, 42.42411, -10.697622, 0.3712644, 42.627335, -12.470892, 0.3712644, 42.409447, -10.695927, 0.28006405, 42.375122, -10.697622, 0.3712644, 42.627335, -16.464678, 0.3712644, 42.4869, -12.470892, 0.3712644, 42.409447, -10.289118, 0.3712644, 42.42411, -10.702703, 0.37126428, 43.383537, -10.697622, 0.3712644, 42.627335, -10.289118, 0.3712644, 42.42411, -9.074274, 0.37126428, 43.401463, -10.702703, 0.37126428, 43.383537, -9.074274, 0.37126428, 43.401463, -10.289118, 0.3712644, 42.42411, -9.068697, 0.3712644, 42.571514, -17.802399, 0.37126428, 43.495537, -10.697622, 0.3712644, 42.627335, -10.702703, 0.37126428, 43.383537, -10.697622, 0.3712644, 42.627335, -17.802399, 0.37126428, 43.495537, -16.464678, 0.3712644, 42.4869, -16.464678, 0.3712644, 42.4869, -17.802399, 0.37126428, 43.495537, -17.797441, 0.3712644, 42.758095, -16.464678, 0.3712644, 42.4869, -17.797441, 0.3712644, 42.758095, -17.795794, 0.28006405, 42.512863, -9.068697, 0.3712644, 42.571514, -10.695927, 0.0055906773, 42.375122, -9.068697, 0.0055906773, 42.571514, -10.695927, 0.0055906773, 42.375122, -9.068697, 0.3712644, 42.571514, -10.289118, 0.3712644, 42.42411, -10.695927, 0.0055906773, 42.375122, -10.289118, 0.3712644, 42.42411, -10.695927, 0.28006405, 42.375122, -10.695927, 0.28006405, 42.375122, -17.795794, 0.0055906773, 42.512863, -10.695927, 0.0055906773, 42.375122, -10.695927, 0.28006405, 42.375122, -16.464678, 0.3712644, 42.4869, -17.795794, 0.0055906773, 42.512863, -17.795794, 0.0055906773, 42.512863, -16.464678, 0.3712644, 42.4869, -17.795794, 0.28006405, 42.512863, -16.464678, 0.3712644, 42.4869, -10.695927, 0.28006405, 42.375122, -12.470892, 0.3712644, 42.409447, -17.795794, 0.28006405, 42.512863, -17.802399, 0.005590558, 43.495537, -17.795794, 0.0055906773, 42.512863, -17.802399, 0.005590558, 43.495537, -17.795794, 0.28006405, 42.512863, -17.797441, 0.3712644, 42.758095, -17.802399, 0.005590558, 43.495537, -17.797441, 0.3712644, 42.758095, -17.802399, 0.37126428, 43.495537, -10.597399, 0.005591333, 36.740005, -17.854618, 0.371265, 36.850945, -10.597399, 0.3712651, 36.740005, -10.597399, 0.005591333, 36.740005, -17.854618, 0.005591333, 36.850945, -17.854618, 0.371265, 36.850945, -8.932753, 0.3712651, 36.758175, -10.597399, 0.005591333, 36.740005, -10.597399, 0.3712651, 36.740005, -8.932753, 0.3712651, 36.758175, -8.932753, 0.005591333, 36.758175, -10.597399, 0.005591333, 36.740005, -10.590622, 0.005591452, 35.73159, -17.854618, 0.005591333, 36.850945, -10.597399, 0.005591333, 36.740005, -10.590622, 0.005591452, 35.73159, -17.848013, 0.005591452, 35.868275, -17.854618, 0.005591333, 36.850945, -8.932753, 0.005591333, 36.758175, -10.590622, 0.005591452, 35.73159, -10.597399, 0.005591333, 36.740005, -8.932753, 0.005591333, 36.758175, -8.927174, 0.005591452, 35.928226, -10.590622, 0.005591452, 35.73159, -8.932753, 0.005591333, 36.758175, -8.927174, 0.3712651, 35.928226, -8.927174, 0.005591452, 35.928226, -8.932753, 0.005591333, 36.758175, -8.932753, 0.3712651, 36.758175, -8.927174, 0.3712651, 35.928226, -10.592318, 0.3712651, 35.983803, -10.590622, 0.28006488, 35.73159, -10.17465, 0.37126523, 35.78064, -10.592318, 0.3712651, 35.983803, -12.40486, 0.37126523, 35.76565, -10.590622, 0.28006488, 35.73159, -10.592318, 0.3712651, 35.983803, -16.487228, 0.3712651, 35.842945, -12.40486, 0.37126523, 35.76565, -10.17465, 0.37126523, 35.78064, -10.597399, 0.3712651, 36.740005, -10.592318, 0.3712651, 35.983803, -10.17465, 0.37126523, 35.78064, -8.932753, 0.3712651, 36.758175, -10.597399, 0.3712651, 36.740005, -8.932753, 0.3712651, 36.758175, -10.17465, 0.37126523, 35.78064, -8.927174, 0.3712651, 35.928226, -17.854618, 0.371265, 36.850945, -10.592318, 0.3712651, 35.983803, -10.597399, 0.3712651, 36.740005, -10.592318, 0.3712651, 35.983803, -17.854618, 0.371265, 36.850945, -16.487228, 0.3712651, 35.842945, -16.487228, 0.3712651, 35.842945, -17.854618, 0.371265, 36.850945, -17.849669, 0.3712651, 36.11438, -16.487228, 0.3712651, 35.842945, -17.849669, 0.3712651, 36.11438, -17.848013, 0.28006476, 35.868275, -8.927174, 0.3712651, 35.928226, -10.590622, 0.005591452, 35.73159, -8.927174, 0.005591452, 35.928226, -10.590622, 0.005591452, 35.73159, -8.927174, 0.3712651, 35.928226, -10.17465, 0.37126523, 35.78064, -10.590622, 0.005591452, 35.73159, -10.17465, 0.37126523, 35.78064, -10.590622, 0.28006488, 35.73159, -10.590622, 0.28006488, 35.73159, -17.848013, 0.005591452, 35.868275, -10.590622, 0.005591452, 35.73159, -10.590622, 0.28006488, 35.73159, -16.487228, 0.3712651, 35.842945, -17.848013, 0.005591452, 35.868275, -17.848013, 0.005591452, 35.868275, -16.487228, 0.3712651, 35.842945, -17.848013, 0.28006476, 35.868275, -16.487228, 0.3712651, 35.842945, -10.590622, 0.28006488, 35.73159, -12.40486, 0.37126523, 35.76565, -17.848013, 0.28006476, 35.868275, -17.854618, 0.005591333, 36.850945, -17.848013, 0.005591452, 35.868275, -17.854618, 0.005591333, 36.850945, -17.848013, 0.28006476, 35.868275, -17.849669, 0.3712651, 36.11438, -17.854618, 0.005591333, 36.850945, -17.849669, 0.3712651, 36.11438, -17.854618, 0.371265, 36.850945, -16.774326, -3.4582698, 22.20213, -17.504349, 1.2701781, 22.197224, -17.504349, -3.4582698, 22.197224, -16.774326, -3.4582698, 22.20213, -16.774326, 0.72166747, 22.20213, -17.504349, 1.2701781, 22.197224, -16.774326, 0.72166747, 22.20213, -17.509253, 1.2701778, 22.926807, -17.504349, 1.2701781, 22.197224, -16.774326, 0.72166747, 22.20213, -16.77923, 1.2701778, 22.931713, -17.509253, 1.2701778, 22.926807, -16.77923, 1.2701778, 22.931713, -17.509253, -3.45827, 22.926807, -17.509253, 1.2701778, 22.926807, -16.77923, 1.2701778, 22.931713, -16.77923, -3.45827, 22.931713, -17.509253, -3.45827, 22.926807, -16.77923, -3.45827, 22.931713, -17.504349, -3.4582698, 22.197224, -17.509253, -3.45827, 22.926807, -16.77923, -3.45827, 22.931713, -16.774326, -3.4582698, 22.20213, -17.504349, -3.4582698, 22.197224, -17.504349, -3.4582698, 22.197224, -17.509253, 1.2701778, 22.926807, -17.509253, -3.45827, 22.926807, -17.504349, -3.4582698, 22.197224, -17.504349, 1.2701781, 22.197224, -17.509253, 1.2701778, 22.926807, -16.77923, -3.45827, 22.931713, -16.774326, 0.72166747, 22.20213, -16.774326, -3.4582698, 22.20213, -16.77923, -3.45827, 22.931713, -16.77923, 1.2701778, 22.931713, -16.774326, 0.72166747, 22.20213, -16.92366, -0.19077104, 22.930307, -17.155365, -0.09826177, 22.928751, -17.155365, -0.19077104, 22.928751, -16.92366, -0.19077104, 22.930307, -16.92366, -0.09826177, 22.930307, -17.155365, -0.09826177, 22.928751, -16.92366, -0.09826177, 22.930307, -17.320957, -0.098264754, 47.5684, -17.155365, -0.09826177, 22.928751, -16.92366, -0.09826177, 22.930307, -17.089252, -0.098264754, 47.569958, -17.320957, -0.098264754, 47.5684, -17.089252, -0.098264754, 47.569958, -17.320957, -0.19077402, 47.5684, -17.320957, -0.098264754, 47.5684, -17.089252, -0.098264754, 47.569958, -17.089252, -0.19077402, 47.569958, -17.320957, -0.19077402, 47.5684, -17.089252, -0.19077402, 47.569958, -17.155365, -0.19077104, 22.928751, -17.320957, -0.19077402, 47.5684, -17.089252, -0.19077402, 47.569958, -16.92366, -0.19077104, 22.930307, -17.155365, -0.19077104, 22.928751, -17.155365, -0.19077104, 22.928751, -17.320957, -0.098264754, 47.5684, -17.320957, -0.19077402, 47.5684, -17.155365, -0.19077104, 22.928751, -17.155365, -0.09826177, 22.928751, -17.320957, -0.098264754, 47.5684, -17.089252, -0.19077402, 47.569958, -16.92366, -0.09826177, 22.930307, -16.92366, -0.19077104, 22.930307, -17.089252, -0.19077402, 47.569958, -17.089252, -0.098264754, 47.569958, -16.92366, -0.09826177, 22.930307, -9.130678, -2.9032154, 33.80826, -9.860701, 1.8252325, 33.803356, -9.860701, -2.9032154, 33.803352, -9.130678, -2.9032154, 33.80826, -9.130678, 1.8252325, 33.80826, -9.860701, 1.8252325, 33.803356, -9.135584, -2.9032154, 34.538284, -9.860701, -2.9032154, 33.803352, -9.865606, -2.9032154, 34.53338, -9.135584, -2.9032154, 34.538284, -9.130678, -2.9032154, 33.80826, -9.860701, -2.9032154, 33.803352, -9.860701, -2.9032154, 33.803352, -9.865606, 1.8252323, 34.53338, -9.865606, -2.9032154, 34.53338, -9.860701, -2.9032154, 33.803352, -9.860701, 1.8252325, 33.803356, -9.865606, 1.8252323, 34.53338, -9.135584, 0.64312047, 34.538284, -9.317982, 1.8252323, 34.537056, -9.134359, 1.8252323, 34.35589, -9.130678, -2.9032154, 33.80826, -9.135584, 0.64312047, 34.538284, -9.134359, 1.8252323, 34.35589, -9.135584, 0.64312047, 34.538284, -9.130678, -2.9032154, 33.80826, -9.135584, -2.9032154, 34.538284, -9.130678, -2.9032154, 33.80826, -9.134359, 1.8252323, 34.35589, -9.130678, 1.8252325, 33.80826, -9.134359, 1.8252323, 34.35589, -9.860701, 1.8252325, 33.803356, -9.130678, 1.8252325, 33.80826, -9.860701, 1.8252325, 33.803356, -9.134359, 1.8252323, 34.35589, -9.317982, 1.8252323, 34.537056, -9.860701, 1.8252325, 33.803356, -9.317982, 1.8252323, 34.537056, -9.865606, 1.8252323, 34.53338, -9.135584, -2.9032154, 34.538284, -9.317982, 1.8252323, 34.537056, -9.135584, 0.64312047, 34.538284, -9.317982, 1.8252323, 34.537056, -9.135584, -2.9032154, 34.538284, -9.865606, -2.9032154, 34.53338, -9.317982, 1.8252323, 34.537056, -9.865606, -2.9032154, 34.53338, -9.865606, 1.8252323, 34.53338, -9.18182, -3.7899094, 41.417854, -9.91184, 0.9385386, 41.41295, -9.91184, -3.7899094, 41.41295, -9.18182, -3.7899094, 41.417854, -9.18182, 0.9385386, 41.417854, -9.91184, 0.9385386, 41.41295, -9.186722, -3.7899094, 42.147438, -9.91184, -3.7899094, 41.41295, -9.916744, -3.7899094, 42.142532, -9.186722, -3.7899094, 42.147438, -9.18182, -3.7899094, 41.417854, -9.91184, -3.7899094, 41.41295, -9.91184, -3.7899094, 41.41295, -9.916744, 0.9385385, 42.142532, -9.916744, -3.7899094, 42.142532, -9.91184, -3.7899094, 41.41295, -9.91184, 0.9385386, 41.41295, -9.916744, 0.9385385, 42.142532, -9.186722, -0.24357355, 42.147438, -9.36912, 0.9385385, 42.146214, -9.185497, 0.9385385, 41.965042, -9.18182, -3.7899094, 41.417854, -9.186722, -0.24357355, 42.147438, -9.185497, 0.9385385, 41.965042, -9.186722, -0.24357355, 42.147438, -9.18182, -3.7899094, 41.417854, -9.186722, -3.7899094, 42.147438, -9.18182, -3.7899094, 41.417854, -9.185497, 0.9385385, 41.965042, -9.18182, 0.9385386, 41.417854, -9.185497, 0.9385385, 41.965042, -9.91184, 0.9385386, 41.41295, -9.18182, 0.9385386, 41.417854, -9.91184, 0.9385386, 41.41295, -9.185497, 0.9385385, 41.965042, -9.36912, 0.9385385, 42.146214, -9.91184, 0.9385386, 41.41295, -9.36912, 0.9385385, 42.146214, -9.916744, 0.9385385, 42.142532, -9.186722, -3.7899094, 42.147438, -9.36912, 0.9385385, 42.146214, -9.186722, -0.24357355, 42.147438, -9.36912, 0.9385385, 42.146214, -9.186722, -3.7899094, 42.147438, -9.916744, -3.7899094, 42.142532, -9.36912, 0.9385385, 42.146214, -9.916744, -3.7899094, 42.142532, -9.916744, 0.9385385, 42.142532, -17.459593, -3.5494714, 33.653664, -18.189178, 1.1789763, 33.64876, -18.189178, -3.5494714, 33.64876, -17.459593, -3.5494714, 33.653664, -17.459593, 1.1789763, 33.653664, -18.189178, 1.1789763, 33.64876, -17.4645, -3.5494716, 34.383686, -18.189178, -3.5494714, 33.64876, -18.194086, -3.5494716, 34.378784, -17.4645, -3.5494716, 34.383686, -17.459593, -3.5494714, 33.653664, -18.189178, -3.5494714, 33.64876, -18.189178, -3.5494714, 33.64876, -18.194086, 1.178976, 34.378784, -18.194086, -3.5494716, 34.378784, -18.189178, -3.5494714, 33.64876, -18.189178, 1.1789763, 33.64876, -18.194086, 1.178976, 34.378784, -17.4645, -0.0031356215, 34.383686, -17.646896, 1.178976, 34.38246, -17.463274, 1.1789763, 34.20129, -17.459593, -3.5494714, 33.653664, -17.4645, -0.0031356215, 34.383686, -17.463274, 1.1789763, 34.20129, -17.4645, -0.0031356215, 34.383686, -17.459593, -3.5494714, 33.653664, -17.4645, -3.5494716, 34.383686, -17.459593, -3.5494714, 33.653664, -17.463274, 1.1789763, 34.20129, -17.459593, 1.1789763, 33.653664, -17.463274, 1.1789763, 34.20129, -18.189178, 1.1789763, 33.64876, -17.459593, 1.1789763, 33.653664, -18.189178, 1.1789763, 33.64876, -17.463274, 1.1789763, 34.20129, -17.646896, 1.178976, 34.38246, -18.189178, 1.1789763, 33.64876, -17.646896, 1.178976, 34.38246, -18.194086, 1.178976, 34.378784, -17.4645, -3.5494716, 34.383686, -17.646896, 1.178976, 34.38246, -17.4645, -0.0031356215, 34.383686, -17.646896, 1.178976, 34.38246, -17.4645, -3.5494716, 34.383686, -18.194086, -3.5494716, 34.378784, -17.646896, 1.178976, 34.38246, -18.194086, -3.5494716, 34.378784, -18.194086, 1.178976, 34.378784, -17.511396, -3.5494723, 41.361874, -18.240982, 1.1789753, 41.35697, -18.240982, -3.5494723, 41.35697, -17.511396, -3.5494723, 41.361874, -17.511396, 1.1789753, 41.361874, -18.240982, 1.1789753, 41.35697, -17.5163, -3.5494723, 42.09146, -18.240982, -3.5494723, 41.35697, -18.245886, -3.5494723, 42.086555, -17.5163, -3.5494723, 42.09146, -17.511396, -3.5494723, 41.361874, -18.240982, -3.5494723, 41.35697, -18.240982, -3.5494723, 41.35697, -18.245886, 1.1789753, 42.086555, -18.245886, -3.5494723, 42.086555, -18.240982, -3.5494723, 41.35697, -18.240982, 1.1789753, 41.35697, -18.245886, 1.1789753, 42.086555, -17.5163, -0.0031365752, 42.09146, -17.698698, 1.1789753, 42.090233, -17.515074, 1.1789753, 41.909065, -17.511396, -3.5494723, 41.361874, -17.5163, -0.0031365752, 42.09146, -17.515074, 1.1789753, 41.909065, -17.5163, -0.0031365752, 42.09146, -17.511396, -3.5494723, 41.361874, -17.5163, -3.5494723, 42.09146, -17.511396, -3.5494723, 41.361874, -17.515074, 1.1789753, 41.909065, -17.511396, 1.1789753, 41.361874, -17.515074, 1.1789753, 41.909065, -18.240982, 1.1789753, 41.35697, -17.511396, 1.1789753, 41.361874, -18.240982, 1.1789753, 41.35697, -17.515074, 1.1789753, 41.909065, -17.698698, 1.1789753, 42.090233, -18.240982, 1.1789753, 41.35697, -17.698698, 1.1789753, 42.090233, -18.245886, 1.1789753, 42.086555, -17.5163, -3.5494723, 42.09146, -17.698698, 1.1789753, 42.090233, -17.5163, -0.0031365752, 42.09146, -17.698698, 1.1789753, 42.090233, -17.5163, -3.5494723, 42.09146, -18.245886, -3.5494723, 42.086555, -17.698698, 1.1789753, 42.090233, -18.245886, -3.5494723, 42.086555, -18.245886, 1.1789753, 42.086555, -12.414185, -22.456995, 9.651152, -10.526909, -22.242655, 9.475412, -12.414185, -22.326996, 9.5211525, -12.414185, -22.456995, 9.651152, -10.526909, -22.391994, 9.624752, -10.526909, -22.242655, 9.475412, -19.07084, -22.391996, 9.624752, -12.414185, -22.456995, 9.651152, -12.414185, -22.326996, 9.5211525, -19.07084, -22.391996, 9.624752, -12.414185, -22.326996, 9.5211525, -19.07084, -22.242657, 9.475412, -12.414185, -22.326996, 9.5211525, -10.526909, -22.03864, 9.420736, -12.414185, -22.14938, 9.473576, -12.414185, -22.326996, 9.5211525, -10.526909, -22.242655, 9.475412, -10.526909, -22.03864, 9.420736, -19.07084, -22.242657, 9.475412, -12.414185, -22.326996, 9.5211525, -12.414185, -22.14938, 9.473576, -19.07084, -22.242657, 9.475412, -12.414185, -22.14938, 9.473576, -19.07084, -22.03864, 9.420735, -12.414185, -22.14938, 9.473576, -10.526909, -21.834621, 9.475412, -12.414185, -21.971802, 9.5211525, -12.414185, -22.14938, 9.473576, -10.526909, -22.03864, 9.420736, -10.526909, -21.834621, 9.475412, -19.07084, -22.03864, 9.420735, -12.414185, -22.14938, 9.473576, -12.414185, -21.971802, 9.5211525, -19.07084, -22.03864, 9.420735, -12.414185, -21.971802, 9.5211525, -19.07084, -21.834623, 9.475412, -12.414185, -21.971802, 9.5211525, -10.526909, -21.685282, 9.624752, -12.414185, -21.841803, 9.651152, -12.414185, -21.971802, 9.5211525, -10.526909, -21.834621, 9.475412, -10.526909, -21.685282, 9.624752, -19.07084, -21.834623, 9.475412, -12.414185, -21.971802, 9.5211525, -12.414185, -21.841803, 9.651152, -19.07084, -21.834623, 9.475412, -12.414185, -21.841803, 9.651152, -19.07084, -21.685282, 9.624752, -12.414185, -21.841803, 9.651152, -10.526909, -21.630606, 9.82877, -12.414185, -21.794186, 9.82877, -12.414185, -21.841803, 9.651152, -10.526909, -21.685282, 9.624752, -10.526909, -21.630606, 9.82877, -19.07084, -21.685282, 9.624752, -12.414185, -21.841803, 9.651152, -12.414185, -21.794186, 9.82877, -19.07084, -21.685282, 9.624752, -12.414185, -21.794186, 9.82877, -19.07084, -21.630606, 9.828769, -12.414185, -21.794186, 9.82877, -10.526909, -21.685282, 10.032785, -12.414185, -21.841803, 10.006346, -12.414185, -21.794186, 9.82877, -10.526909, -21.630606, 9.82877, -10.526909, -21.685282, 10.032785, -19.07084, -21.630606, 9.828769, -12.414185, -21.794186, 9.82877, -12.414185, -21.841803, 10.006346, -19.07084, -21.630606, 9.828769, -12.414185, -21.841803, 10.006346, -19.07084, -21.685282, 10.032785, -12.414185, -21.841803, 10.006346, -10.526909, -21.834621, 10.182125, -12.414185, -21.971802, 10.136345, -12.414185, -21.841803, 10.006346, -10.526909, -21.685282, 10.032785, -10.526909, -21.834621, 10.182125, -19.07084, -21.685282, 10.032785, -12.414185, -21.841803, 10.006346, -12.414185, -21.971802, 10.136345, -19.07084, -21.685282, 10.032785, -12.414185, -21.971802, 10.136345, -19.07084, -21.834623, 10.182125, -12.414185, -21.971802, 10.136345, -10.526909, -22.03864, 10.236803, -12.414185, -22.149378, 10.183962, -12.414185, -21.971802, 10.136345, -10.526909, -21.834621, 10.182125, -10.526909, -22.03864, 10.236803, -19.07084, -21.834623, 10.182125, -12.414185, -21.971802, 10.136345, -12.414185, -22.149378, 10.183962, -19.07084, -21.834623, 10.182125, -12.414185, -22.149378, 10.183962, -19.07084, -22.03864, 10.236801, -12.414185, -22.149378, 10.183962, -10.526909, -22.242655, 10.182125, -12.414185, -22.326996, 10.136345, -12.414185, -22.149378, 10.183962, -10.526909, -22.03864, 10.236803, -10.526909, -22.242655, 10.182125, -19.07084, -22.03864, 10.236801, -12.414185, -22.149378, 10.183962, -12.414185, -22.326996, 10.136345, -19.07084, -22.03864, 10.236801, -12.414185, -22.326996, 10.136345, -19.07084, -22.242657, 10.182125, -12.414185, -22.326996, 10.136345, -10.526909, -22.391994, 10.032785, -12.414185, -22.456995, 10.006346, -12.414185, -22.326996, 10.136345, -10.526909, -22.242655, 10.182125, -10.526909, -22.391994, 10.032785, -19.07084, -22.242657, 10.182125, -12.414185, -22.326996, 10.136345, -12.414185, -22.456995, 10.006346, -19.07084, -22.242657, 10.182125, -12.414185, -22.456995, 10.006346, -19.07084, -22.391996, 10.032785, -12.414185, -22.456995, 10.006346, -10.526909, -22.446672, 9.82877, -12.414185, -22.504572, 9.82877, -12.414185, -22.456995, 10.006346, -10.526909, -22.391994, 10.032785, -10.526909, -22.446672, 9.82877, -19.07084, -22.391996, 10.032785, -12.414185, -22.456995, 10.006346, -12.414185, -22.504572, 9.82877, -19.07084, -22.391996, 10.032785, -12.414185, -22.504572, 9.82877, -19.07084, -22.446672, 9.828769, -12.414185, -22.504572, 9.82877, -10.526909, -22.391994, 9.624752, -12.414185, -22.456995, 9.651152, -12.414185, -22.504572, 9.82877, -10.526909, -22.446672, 9.82877, -10.526909, -22.391994, 9.624752, -19.07084, -22.446672, 9.828769, -12.414185, -22.504572, 9.82877, -12.414185, -22.456995, 9.651152, -19.07084, -22.446672, 9.828769, -12.414185, -22.456995, 9.651152, -19.07084, -22.391996, 9.624752, -19.07084, -22.242657, 9.475412, -19.693949, -22.03864, 9.828769, -19.07084, -22.391996, 9.624752, -19.07084, -22.391996, 9.624752, -19.693949, -22.03864, 9.828769, -19.07084, -22.446672, 9.828769, -19.07084, -22.446672, 9.828769, -19.693949, -22.03864, 9.828769, -19.07084, -22.391996, 10.032785, -19.07084, -22.391996, 10.032785, -19.693949, -22.03864, 9.828769, -19.07084, -22.242657, 10.182125, -19.07084, -22.03864, 9.420735, -19.693949, -22.03864, 9.828769, -19.07084, -22.242657, 9.475412, -19.07084, -21.834623, 9.475412, -19.693949, -22.03864, 9.828769, -19.07084, -22.03864, 9.420735, -19.07084, -21.685282, 9.624752, -19.693949, -22.03864, 9.828769, -19.07084, -21.834623, 9.475412, -19.07084, -21.630606, 9.828769, -19.693949, -22.03864, 9.828769, -19.07084, -21.685282, 9.624752, -19.07084, -21.685282, 10.032785, -19.693949, -22.03864, 9.828769, -19.07084, -21.630606, 9.828769, -19.07084, -21.834623, 10.182125, -19.693949, -22.03864, 9.828769, -19.07084, -21.685282, 10.032785, -19.07084, -22.03864, 10.236801, -19.693949, -22.03864, 9.828769, -19.07084, -21.834623, 10.182125, -19.07084, -22.242657, 10.182125, -19.693949, -22.03864, 9.828769, -19.07084, -22.03864, 10.236801, -10.526909, -22.391994, 9.624752, -9.89662, -22.03864, 9.82877, -10.526909, -22.242655, 9.475412, -10.526909, -22.446672, 9.82877, -9.89662, -22.03864, 9.82877, -10.526909, -22.391994, 9.624752, -10.526909, -22.391994, 10.032785, -9.89662, -22.03864, 9.82877, -10.526909, -22.446672, 9.82877, -10.526909, -22.242655, 10.182125, -9.89662, -22.03864, 9.82877, -10.526909, -22.391994, 10.032785, -10.526909, -22.242655, 9.475412, -9.89662, -22.03864, 9.82877, -10.526909, -22.03864, 9.420736, -10.526909, -22.03864, 9.420736, -9.89662, -22.03864, 9.82877, -10.526909, -21.834621, 9.475412, -10.526909, -21.834621, 9.475412, -9.89662, -22.03864, 9.82877, -10.526909, -21.685282, 9.624752, -10.526909, -21.685282, 9.624752, -9.89662, -22.03864, 9.82877, -10.526909, -21.630606, 9.82877, -10.526909, -21.630606, 9.82877, -9.89662, -22.03864, 9.82877, -10.526909, -21.685282, 10.032785, -10.526909, -21.685282, 10.032785, -9.89662, -22.03864, 9.82877, -10.526909, -21.834621, 10.182125, -10.526909, -21.834621, 10.182125, -9.89662, -22.03864, 9.82877, -10.526909, -22.03864, 10.236803, -10.526909, -22.03864, 10.236803, -9.89662, -22.03864, 9.82877, -10.526909, -22.242655, 10.182125, -14.262087, -22.348663, 7.7697797, -10.526909, -22.242655, 7.6348023, -14.262087, -22.188835, 7.609912, -14.262087, -22.348663, 7.7697797, -10.526909, -22.391994, 7.7841425, -10.526909, -22.242655, 7.6348023, -19.07084, -22.391996, 7.784142, -14.262087, -22.348663, 7.7697797, -14.262087, -22.188835, 7.609912, -19.07084, -22.391996, 7.784142, -14.262087, -22.188835, 7.609912, -19.07084, -22.242657, 7.634802, -14.262087, -22.188835, 7.609912, -10.526909, -22.03864, 7.580126, -14.262087, -21.970457, 7.5514, -14.262087, -22.188835, 7.609912, -10.526909, -22.242655, 7.6348023, -10.526909, -22.03864, 7.580126, -19.07084, -22.242657, 7.634802, -14.262087, -22.188835, 7.609912, -14.262087, -21.970457, 7.5514, -19.07084, -22.242657, 7.634802, -14.262087, -21.970457, 7.5514, -19.07084, -22.03864, 7.5801253, -14.262087, -21.970457, 7.5514, -10.526909, -21.834621, 7.6348023, -14.262087, -21.752077, 7.609912, -14.262087, -21.970457, 7.5514, -10.526909, -22.03864, 7.580126, -10.526909, -21.834621, 7.6348023, -19.07084, -22.03864, 7.5801253, -14.262087, -21.970457, 7.5514, -14.262087, -21.752077, 7.609912, -19.07084, -22.03864, 7.5801253, -14.262087, -21.752077, 7.609912, -19.07084, -21.834623, 7.634802, -14.262087, -21.752077, 7.609912, -10.526909, -21.685282, 7.7841425, -14.262087, -21.592209, 7.769779, -14.262087, -21.752077, 7.609912, -10.526909, -21.834621, 7.6348023, -10.526909, -21.685282, 7.7841425, -19.07084, -21.834623, 7.634802, -14.262087, -21.752077, 7.609912, -14.262087, -21.592209, 7.769779, -19.07084, -21.834623, 7.634802, -14.262087, -21.592209, 7.769779, -19.07084, -21.685282, 7.784142, -14.262087, -21.592209, 7.769779, -10.526909, -21.630606, 7.988159, -14.262087, -21.533697, 7.9881587, -14.262087, -21.592209, 7.769779, -10.526909, -21.685282, 7.7841425, -10.526909, -21.630606, 7.988159, -19.07084, -21.685282, 7.784142, -14.262087, -21.592209, 7.769779, -14.262087, -21.533697, 7.9881587, -19.07084, -21.685282, 7.784142, -14.262087, -21.533697, 7.9881587, -19.07084, -21.630606, 7.9881587, -14.262087, -21.533697, 7.9881587, -10.526909, -21.685282, 8.192176, -14.262087, -21.592209, 8.206538, -14.262087, -21.533697, 7.9881587, -10.526909, -21.630606, 7.988159, -10.526909, -21.685282, 8.192176, -19.07084, -21.630606, 7.9881587, -14.262087, -21.533697, 7.9881587, -14.262087, -21.592209, 8.206538, -19.07084, -21.630606, 7.9881587, -14.262087, -21.592209, 8.206538, -19.07084, -21.685282, 8.192175, -14.262087, -21.592209, 8.206538, -10.526909, -21.834621, 8.3415165, -14.262087, -21.752077, 8.366365, -14.262087, -21.592209, 8.206538, -10.526909, -21.685282, 8.192176, -10.526909, -21.834621, 8.3415165, -19.07084, -21.685282, 8.192175, -14.262087, -21.592209, 8.206538, -14.262087, -21.752077, 8.366365, -19.07084, -21.685282, 8.192175, -14.262087, -21.752077, 8.366365, -19.07084, -21.834623, 8.341516, -14.262087, -21.752077, 8.366365, -10.526909, -22.03864, 8.396193, -14.262087, -21.970457, 8.424917, -14.262087, -21.752077, 8.366365, -10.526909, -21.834621, 8.3415165, -10.526909, -22.03864, 8.396193, -19.07084, -21.834623, 8.341516, -14.262087, -21.752077, 8.366365, -14.262087, -21.970457, 8.424917, -19.07084, -21.834623, 8.341516, -14.262087, -21.970457, 8.424917, -19.07084, -22.03864, 8.396193, -14.262087, -21.970457, 8.424917, -10.526909, -22.242655, 8.3415165, -14.262087, -22.188835, 8.366365, -14.262087, -21.970457, 8.424917, -10.526909, -22.03864, 8.396193, -10.526909, -22.242655, 8.3415165, -19.07084, -22.03864, 8.396193, -14.262087, -21.970457, 8.424917, -14.262087, -22.188835, 8.366365, -19.07084, -22.03864, 8.396193, -14.262087, -22.188835, 8.366365, -19.07084, -22.242657, 8.341516, -14.262087, -22.188835, 8.366365, -10.526909, -22.391994, 8.192176, -14.262087, -22.348663, 8.206538, -14.262087, -22.188835, 8.366365, -10.526909, -22.242655, 8.3415165, -10.526909, -22.391994, 8.192176, -19.07084, -22.242657, 8.341516, -14.262087, -22.188835, 8.366365, -14.262087, -22.348663, 8.206538, -19.07084, -22.242657, 8.341516, -14.262087, -22.348663, 8.206538, -19.07084, -22.391996, 8.192175, -14.262087, -22.348663, 8.206538, -10.526909, -22.446672, 7.988159, -14.262087, -22.407175, 7.9881587, -14.262087, -22.348663, 8.206538, -10.526909, -22.391994, 8.192176, -10.526909, -22.446672, 7.988159, -19.07084, -22.391996, 8.192175, -14.262087, -22.348663, 8.206538, -14.262087, -22.407175, 7.9881587, -19.07084, -22.391996, 8.192175, -14.262087, -22.407175, 7.9881587, -19.07084, -22.446672, 7.9881587, -14.262087, -22.407175, 7.9881587, -10.526909, -22.391994, 7.7841425, -14.262087, -22.348663, 7.7697797, -14.262087, -22.407175, 7.9881587, -10.526909, -22.446672, 7.988159, -10.526909, -22.391994, 7.7841425, -19.07084, -22.446672, 7.9881587, -14.262087, -22.407175, 7.9881587, -14.262087, -22.348663, 7.7697797, -19.07084, -22.446672, 7.9881587, -14.262087, -22.348663, 7.7697797, -19.07084, -22.391996, 7.784142, -19.07084, -22.242657, 7.634802, -19.693949, -22.03864, 7.9881587, -19.07084, -22.391996, 7.784142, -19.07084, -22.391996, 7.784142, -19.693949, -22.03864, 7.9881587, -19.07084, -22.446672, 7.9881587, -19.07084, -22.446672, 7.9881587, -19.693949, -22.03864, 7.9881587, -19.07084, -22.391996, 8.192175, -19.07084, -22.391996, 8.192175, -19.693949, -22.03864, 7.9881587, -19.07084, -22.242657, 8.341516, -19.07084, -22.03864, 7.5801253, -19.693949, -22.03864, 7.9881587, -19.07084, -22.242657, 7.634802, -19.07084, -21.834623, 7.634802, -19.693949, -22.03864, 7.9881587, -19.07084, -22.03864, 7.5801253, -19.07084, -21.685282, 7.784142, -19.693949, -22.03864, 7.9881587, -19.07084, -21.834623, 7.634802, -19.07084, -21.630606, 7.9881587, -19.693949, -22.03864, 7.9881587, -19.07084, -21.685282, 7.784142, -19.07084, -21.685282, 8.192175, -19.693949, -22.03864, 7.9881587, -19.07084, -21.630606, 7.9881587, -19.07084, -21.834623, 8.341516, -19.693949, -22.03864, 7.9881587, -19.07084, -21.685282, 8.192175, -19.07084, -22.03864, 8.396193, -19.693949, -22.03864, 7.9881587, -19.07084, -21.834623, 8.341516, -19.07084, -22.242657, 8.341516, -19.693949, -22.03864, 7.9881587, -19.07084, -22.03864, 8.396193, -10.526909, -22.391994, 7.7841425, -9.89662, -22.03864, 7.988159, -10.526909, -22.242655, 7.6348023, -10.526909, -22.446672, 7.988159, -9.89662, -22.03864, 7.988159, -10.526909, -22.391994, 7.7841425, -10.526909, -22.391994, 8.192176, -9.89662, -22.03864, 7.988159, -10.526909, -22.446672, 7.988159, -10.526909, -22.242655, 8.3415165, -9.89662, -22.03864, 7.988159, -10.526909, -22.391994, 8.192176, -10.526909, -22.242655, 7.6348023, -9.89662, -22.03864, 7.988159, -10.526909, -22.03864, 7.580126, -10.526909, -22.03864, 7.580126, -9.89662, -22.03864, 7.988159, -10.526909, -21.834621, 7.6348023, -10.526909, -21.834621, 7.6348023, -9.89662, -22.03864, 7.988159, -10.526909, -21.685282, 7.7841425, -10.526909, -21.685282, 7.7841425, -9.89662, -22.03864, 7.988159, -10.526909, -21.630606, 7.988159, -10.526909, -21.630606, 7.988159, -9.89662, -22.03864, 7.988159, -10.526909, -21.685282, 8.192176, -10.526909, -21.685282, 8.192176, -9.89662, -22.03864, 7.988159, -10.526909, -21.834621, 8.3415165, -10.526909, -21.834621, 8.3415165, -9.89662, -22.03864, 7.988159, -10.526909, -22.03864, 8.396193, -10.526909, -22.03864, 8.396193, -9.89662, -22.03864, 7.988159, -10.526909, -22.242655, 8.3415165, -12.004166, -22.046726, 10.088959, -12.124956, -19.902678, 9.968168, -12.124956, -22.046726, 9.968168, -12.004166, -22.046726, 10.088959, -12.004166, -19.902678, 10.088959, -12.124956, -19.902678, 9.968168, -12.124956, -22.046726, 9.968168, -12.28997, -19.902678, 9.923944, -12.28997, -22.046726, 9.923944, -12.124956, -22.046726, 9.968168, -12.124956, -19.902678, 9.968168, -12.28997, -19.902678, 9.923944, -12.28997, -22.046726, 9.923944, -12.454985, -19.902678, 9.968168, -12.454985, -22.046726, 9.968168, -12.28997, -22.046726, 9.923944, -12.28997, -19.902678, 9.923944, -12.454985, -19.902678, 9.968168, -12.454985, -22.046726, 9.968168, -12.575773, -19.902678, 10.088959, -12.575773, -22.046726, 10.088959, -12.454985, -22.046726, 9.968168, -12.454985, -19.902678, 9.968168, -12.575773, -19.902678, 10.088959, -12.575773, -22.046726, 10.088959, -12.619997, -19.902678, 10.253973, -12.619997, -22.046726, 10.253973, -12.575773, -22.046726, 10.088959, -12.575773, -19.902678, 10.088959, -12.619997, -19.902678, 10.253973, -12.619997, -22.046726, 10.253973, -12.575773, -19.902678, 10.418985, -12.575773, -22.046726, 10.418987, -12.619997, -22.046726, 10.253973, -12.619997, -19.902678, 10.253973, -12.575773, -19.902678, 10.418985, -12.575773, -22.046726, 10.418987, -12.454985, -19.902678, 10.539776, -12.454985, -22.046726, 10.539778, -12.575773, -22.046726, 10.418987, -12.575773, -19.902678, 10.418985, -12.454985, -19.902678, 10.539776, -12.454985, -22.046726, 10.539778, -12.28997, -19.902678, 10.584, -12.28997, -22.046726, 10.584002, -12.454985, -22.046726, 10.539778, -12.454985, -19.902678, 10.539776, -12.28997, -19.902678, 10.584, -12.28997, -22.046726, 10.584002, -12.124956, -19.902678, 10.539776, -12.124956, -22.046726, 10.539778, -12.28997, -22.046726, 10.584002, -12.28997, -19.902678, 10.584, -12.124956, -19.902678, 10.539776, -12.124956, -22.046726, 10.539778, -12.004166, -19.902678, 10.418985, -12.004166, -22.046726, 10.418987, -12.124956, -22.046726, 10.539778, -12.124956, -19.902678, 10.539776, -12.004166, -19.902678, 10.418985, -12.004166, -22.046726, 10.418987, -11.959942, -19.902678, 10.253973, -11.959942, -22.046726, 10.253973, -12.004166, -22.046726, 10.418987, -12.004166, -19.902678, 10.418985, -11.959942, -19.902678, 10.253973, -11.959942, -22.046726, 10.253973, -12.004166, -19.902678, 10.088959, -12.004166, -22.046726, 10.088959, -11.959942, -22.046726, 10.253973, -11.959942, -19.902678, 10.253973, -12.004166, -19.902678, 10.088959, -12.124956, -22.046726, 9.968168, -12.28997, -22.046726, 10.253973, -12.004166, -22.046726, 10.088959, -12.004166, -22.046726, 10.088959, -12.28997, -22.046726, 10.253973, -11.959942, -22.046726, 10.253973, -12.28997, -22.046726, 9.923944, -12.28997, -22.046726, 10.253973, -12.124956, -22.046726, 9.968168, -11.959942, -22.046726, 10.253973, -12.28997, -22.046726, 10.253973, -12.004166, -22.046726, 10.418987, -12.454985, -22.046726, 9.968168, -12.28997, -22.046726, 10.253973, -12.28997, -22.046726, 9.923944, -12.004166, -22.046726, 10.418987, -12.28997, -22.046726, 10.253973, -12.124956, -22.046726, 10.539778, -12.575773, -22.046726, 10.088959, -12.28997, -22.046726, 10.253973, -12.454985, -22.046726, 9.968168, -12.124956, -22.046726, 10.539778, -12.28997, -22.046726, 10.253973, -12.28997, -22.046726, 10.584002, -12.619997, -22.046726, 10.253973, -12.28997, -22.046726, 10.253973, -12.575773, -22.046726, 10.088959, -12.28997, -22.046726, 10.584002, -12.28997, -22.046726, 10.253973, -12.454985, -22.046726, 10.539778, -12.575773, -22.046726, 10.418987, -12.28997, -22.046726, 10.253973, -12.619997, -22.046726, 10.253973, -12.454985, -22.046726, 10.539778, -12.28997, -22.046726, 10.253973, -12.575773, -22.046726, 10.418987, -12.004166, -19.902678, 10.088959, -12.28997, -19.456135, 10.253971, -12.124956, -19.902678, 9.968168, -12.124956, -19.902678, 9.968168, -12.28997, -19.456135, 10.253971, -12.28997, -19.902678, 9.923944, -11.959942, -19.902678, 10.253973, -12.28997, -19.456135, 10.253971, -12.004166, -19.902678, 10.088959, -12.28997, -19.902678, 9.923944, -12.28997, -19.456135, 10.253971, -12.454985, -19.902678, 9.968168, -12.004166, -19.902678, 10.418985, -12.28997, -19.456135, 10.253971, -11.959942, -19.902678, 10.253973, -12.454985, -19.902678, 9.968168, -12.28997, -19.456135, 10.253971, -12.575773, -19.902678, 10.088959, -12.124956, -19.902678, 10.539776, -12.28997, -19.456135, 10.253971, -12.004166, -19.902678, 10.418985, -12.575773, -19.902678, 10.088959, -12.28997, -19.456135, 10.253971, -12.619997, -19.902678, 10.253973, -12.28997, -19.902678, 10.584, -12.28997, -19.456135, 10.253971, -12.124956, -19.902678, 10.539776, -12.619997, -19.902678, 10.253973, -12.28997, -19.456135, 10.253971, -12.575773, -19.902678, 10.418985, -12.454985, -19.902678, 10.539776, -12.28997, -19.456135, 10.253971, -12.28997, -19.902678, 10.584, -12.575773, -19.902678, 10.418985, -12.28997, -19.456135, 10.253971, -12.454985, -19.902678, 10.539776, -12.004166, -22.046726, 6.859756, -12.124956, -19.902678, 6.7389646, -12.124956, -22.046726, 6.7389655, -12.004166, -22.046726, 6.859756, -12.004166, -19.902678, 6.859755, -12.124956, -19.902678, 6.7389646, -12.124956, -22.046726, 6.7389655, -12.28997, -19.902678, 6.6947412, -12.28997, -22.046726, 6.6947417, -12.124956, -22.046726, 6.7389655, -12.124956, -19.902678, 6.7389646, -12.28997, -19.902678, 6.6947412, -12.28997, -22.046726, 6.6947417, -12.454985, -19.902678, 6.7389646, -12.454985, -22.046726, 6.7389655, -12.28997, -22.046726, 6.6947417, -12.28997, -19.902678, 6.6947412, -12.454985, -19.902678, 6.7389646, -12.454985, -22.046726, 6.7389655, -12.575773, -19.902678, 6.859755, -12.575773, -22.046726, 6.859756, -12.454985, -22.046726, 6.7389655, -12.454985, -19.902678, 6.7389646, -12.575773, -19.902678, 6.859755, -12.575773, -22.046726, 6.859756, -12.619997, -19.902678, 7.024769, -12.619997, -22.046726, 7.02477, -12.575773, -22.046726, 6.859756, -12.575773, -19.902678, 6.859755, -12.619997, -19.902678, 7.024769, -12.619997, -22.046726, 7.02477, -12.575773, -19.902678, 7.1897826, -12.575773, -22.046726, 7.1897836, -12.619997, -22.046726, 7.02477, -12.619997, -19.902678, 7.024769, -12.575773, -19.902678, 7.1897826, -12.575773, -22.046726, 7.1897836, -12.454985, -19.902678, 7.310573, -12.454985, -22.046726, 7.3105736, -12.575773, -22.046726, 7.1897836, -12.575773, -19.902678, 7.1897826, -12.454985, -19.902678, 7.310573, -12.454985, -22.046726, 7.3105736, -12.28997, -19.902678, 7.354797, -12.28997, -22.046726, 7.3547974, -12.454985, -22.046726, 7.3105736, -12.454985, -19.902678, 7.310573, -12.28997, -19.902678, 7.354797, -12.28997, -22.046726, 7.3547974, -12.124956, -19.902678, 7.310573, -12.124956, -22.046726, 7.3105736, -12.28997, -22.046726, 7.3547974, -12.28997, -19.902678, 7.354797, -12.124956, -19.902678, 7.310573, -12.124956, -22.046726, 7.3105736, -12.004166, -19.902678, 7.1897826, -12.004166, -22.046726, 7.1897836, -12.124956, -22.046726, 7.3105736, -12.124956, -19.902678, 7.310573, -12.004166, -19.902678, 7.1897826, -12.004166, -22.046726, 7.1897836, -11.959942, -19.902678, 7.024769, -11.959942, -22.046726, 7.02477, -12.004166, -22.046726, 7.1897836, -12.004166, -19.902678, 7.1897826, -11.959942, -19.902678, 7.024769, -11.959942, -22.046726, 7.02477, -12.004166, -19.902678, 6.859755, -12.004166, -22.046726, 6.859756, -11.959942, -22.046726, 7.02477, -11.959942, -19.902678, 7.024769, -12.004166, -19.902678, 6.859755, -12.124956, -22.046726, 6.7389655, -12.28997, -22.046726, 7.02477, -12.004166, -22.046726, 6.859756, -12.004166, -22.046726, 6.859756, -12.28997, -22.046726, 7.02477, -11.959942, -22.046726, 7.02477, -12.28997, -22.046726, 6.6947417, -12.28997, -22.046726, 7.02477, -12.124956, -22.046726, 6.7389655, -11.959942, -22.046726, 7.02477, -12.28997, -22.046726, 7.02477, -12.004166, -22.046726, 7.1897836, -12.454985, -22.046726, 6.7389655, -12.28997, -22.046726, 7.02477, -12.28997, -22.046726, 6.6947417, -12.004166, -22.046726, 7.1897836, -12.28997, -22.046726, 7.02477, -12.124956, -22.046726, 7.3105736, -12.575773, -22.046726, 6.859756, -12.28997, -22.046726, 7.02477, -12.454985, -22.046726, 6.7389655, -12.124956, -22.046726, 7.3105736, -12.28997, -22.046726, 7.02477, -12.28997, -22.046726, 7.3547974, -12.619997, -22.046726, 7.02477, -12.28997, -22.046726, 7.02477, -12.575773, -22.046726, 6.859756, -12.28997, -22.046726, 7.3547974, -12.28997, -22.046726, 7.02477, -12.454985, -22.046726, 7.3105736, -12.575773, -22.046726, 7.1897836, -12.28997, -22.046726, 7.02477, -12.619997, -22.046726, 7.02477, -12.454985, -22.046726, 7.3105736, -12.28997, -22.046726, 7.02477, -12.575773, -22.046726, 7.1897836, -12.004166, -19.902678, 6.859755, -12.28997, -19.456135, 7.024769, -12.124956, -19.902678, 6.7389646, -12.124956, -19.902678, 6.7389646, -12.28997, -19.456135, 7.024769, -12.28997, -19.902678, 6.6947412, -11.959942, -19.902678, 7.024769, -12.28997, -19.456135, 7.024769, -12.004166, -19.902678, 6.859755, -12.28997, -19.902678, 6.6947412, -12.28997, -19.456135, 7.024769, -12.454985, -19.902678, 6.7389646, -12.004166, -19.902678, 7.1897826, -12.28997, -19.456135, 7.024769, -11.959942, -19.902678, 7.024769, -12.454985, -19.902678, 6.7389646, -12.28997, -19.456135, 7.024769, -12.575773, -19.902678, 6.859755, -12.124956, -19.902678, 7.310573, -12.28997, -19.456135, 7.024769, -12.004166, -19.902678, 7.1897826, -12.575773, -19.902678, 6.859755, -12.28997, -19.456135, 7.024769, -12.619997, -19.902678, 7.024769, -12.28997, -19.902678, 7.354797, -12.28997, -19.456135, 7.024769, -12.124956, -19.902678, 7.310573, -12.619997, -19.902678, 7.024769, -12.28997, -19.456135, 7.024769, -12.575773, -19.902678, 7.1897826, -12.454985, -19.902678, 7.310573, -12.28997, -19.456135, 7.024769, -12.28997, -19.902678, 7.354797, -12.575773, -19.902678, 7.1897826, -12.28997, -19.456135, 7.024769, -12.454985, -19.902678, 7.310573, -12.917406, -22.143438, 6.933488, -12.989674, -20.442587, 6.8666687, -12.989674, -22.143438, 6.8666697, -12.917406, -22.143438, 6.933488, -12.917406, -20.442587, 6.933487, -12.989674, -20.442587, 6.8666687, -12.989674, -22.143438, 6.8666697, -13.088398, -20.442587, 6.8422055, -13.088398, -22.143438, 6.842206, -12.989674, -22.143438, 6.8666697, -12.989674, -20.442587, 6.8666687, -13.088398, -20.442587, 6.8422055, -13.088398, -22.143438, 6.842206, -13.187122, -20.442587, 6.8666687, -13.187122, -22.143438, 6.8666697, -13.088398, -22.143438, 6.842206, -13.088398, -20.442587, 6.8422055, -13.187122, -20.442587, 6.8666687, -13.187122, -22.143438, 6.8666697, -13.259388, -20.442587, 6.933487, -13.259388, -22.143438, 6.933488, -13.187122, -22.143438, 6.8666697, -13.187122, -20.442587, 6.8666687, -13.259388, -20.442587, 6.933487, -13.259388, -22.143438, 6.933488, -13.285847, -20.442587, 7.024769, -13.285847, -22.143438, 7.02477, -13.259388, -22.143438, 6.933488, -13.259388, -20.442587, 6.933487, -13.285847, -20.442587, 7.024769, -13.285847, -22.143438, 7.02477, -13.259388, -20.442587, 7.1160507, -13.259388, -22.143438, 7.116051, -13.285847, -22.143438, 7.02477, -13.285847, -20.442587, 7.024769, -13.259388, -20.442587, 7.1160507, -13.259388, -22.143438, 7.116051, -13.187122, -20.442587, 7.182869, -13.187122, -22.143438, 7.18287, -13.259388, -22.143438, 7.116051, -13.259388, -20.442587, 7.1160507, -13.187122, -20.442587, 7.182869, -13.187122, -22.143438, 7.18287, -13.088398, -20.442587, 7.2073326, -13.088398, -22.143438, 7.207333, -13.187122, -22.143438, 7.18287, -13.187122, -20.442587, 7.182869, -13.088398, -20.442587, 7.2073326, -13.088398, -22.143438, 7.207333, -12.989674, -20.442587, 7.182869, -12.989674, -22.143438, 7.18287, -13.088398, -22.143438, 7.207333, -13.088398, -20.442587, 7.2073326, -12.989674, -20.442587, 7.182869, -12.989674, -22.143438, 7.18287, -12.917406, -20.442587, 7.1160507, -12.917406, -22.143438, 7.116051, -12.989674, -22.143438, 7.18287, -12.989674, -20.442587, 7.182869, -12.917406, -20.442587, 7.1160507, -12.917406, -22.143438, 7.116051, -12.890947, -20.442587, 7.024769, -12.890947, -22.143438, 7.02477, -12.917406, -22.143438, 7.116051, -12.917406, -20.442587, 7.1160507, -12.890947, -20.442587, 7.024769, -12.890947, -22.143438, 7.02477, -12.917406, -20.442587, 6.933487, -12.917406, -22.143438, 6.933488, -12.890947, -22.143438, 7.02477, -12.890947, -20.442587, 7.024769, -12.917406, -20.442587, 6.933487, -12.989674, -22.143438, 6.8666697, -13.088398, -22.143438, 7.02477, -12.917406, -22.143438, 6.933488, -12.917406, -22.143438, 6.933488, -13.088398, -22.143438, 7.02477, -12.890947, -22.143438, 7.02477, -13.088398, -22.143438, 6.842206, -13.088398, -22.143438, 7.02477, -12.989674, -22.143438, 6.8666697, -12.890947, -22.143438, 7.02477, -13.088398, -22.143438, 7.02477, -12.917406, -22.143438, 7.116051, -13.187122, -22.143438, 6.8666697, -13.088398, -22.143438, 7.02477, -13.088398, -22.143438, 6.842206, -12.917406, -22.143438, 7.116051, -13.088398, -22.143438, 7.02477, -12.989674, -22.143438, 7.18287, -13.259388, -22.143438, 6.933488, -13.088398, -22.143438, 7.02477, -13.187122, -22.143438, 6.8666697, -12.989674, -22.143438, 7.18287, -13.088398, -22.143438, 7.02477, -13.088398, -22.143438, 7.207333, -13.285847, -22.143438, 7.02477, -13.088398, -22.143438, 7.02477, -13.259388, -22.143438, 6.933488, -13.088398, -22.143438, 7.207333, -13.088398, -22.143438, 7.02477, -13.187122, -22.143438, 7.18287, -13.259388, -22.143438, 7.116051, -13.088398, -22.143438, 7.02477, -13.285847, -22.143438, 7.02477, -13.187122, -22.143438, 7.18287, -13.088398, -22.143438, 7.02477, -13.259388, -22.143438, 7.116051, -12.917406, -20.442587, 6.933487, -13.088398, -20.08835, 7.024769, -12.989674, -20.442587, 6.8666687, -12.989674, -20.442587, 6.8666687, -13.088398, -20.08835, 7.024769, -13.088398, -20.442587, 6.8422055, -12.890947, -20.442587, 7.024769, -13.088398, -20.08835, 7.024769, -12.917406, -20.442587, 6.933487, -13.088398, -20.442587, 6.8422055, -13.088398, -20.08835, 7.024769, -13.187122, -20.442587, 6.8666687, -12.917406, -20.442587, 7.1160507, -13.088398, -20.08835, 7.024769, -12.890947, -20.442587, 7.024769, -13.187122, -20.442587, 6.8666687, -13.088398, -20.08835, 7.024769, -13.259388, -20.442587, 6.933487, -12.989674, -20.442587, 7.182869, -13.088398, -20.08835, 7.024769, -12.917406, -20.442587, 7.1160507, -13.259388, -20.442587, 6.933487, -13.088398, -20.08835, 7.024769, -13.285847, -20.442587, 7.024769, -13.088398, -20.442587, 7.2073326, -13.088398, -20.08835, 7.024769, -12.989674, -20.442587, 7.182869, -13.285847, -20.442587, 7.024769, -13.088398, -20.08835, 7.024769, -13.259388, -20.442587, 7.1160507, -13.187122, -20.442587, 7.182869, -13.088398, -20.08835, 7.024769, -13.088398, -20.442587, 7.2073326, -13.259388, -20.442587, 7.1160507, -13.088398, -20.08835, 7.024769, -13.187122, -20.442587, 7.182869, 7.7560043, -21.843687, -1.5963855, 7.8594513, -21.712654, -1.5136316, 7.8594513, -21.889545, -1.5601864, 7.7560043, -21.843687, -1.5963855, 7.7198067, -21.712654, -1.5601864, 7.8594513, -21.712654, -1.5136316, 7.7198067, -21.889545, -1.6998296, 7.7560043, -21.843687, -1.5963855, 7.8594513, -21.889545, -1.5601864, 7.7198067, -21.889545, -1.6998296, 7.7198067, -21.712654, -1.5601864, 7.7560043, -21.843687, -1.5963855, 7.7198067, -21.712654, -1.5601864, 7.8594513, -21.392513, -1.5493045, 7.8594513, -21.712654, -1.5136316, 7.8594513, -21.712654, -1.5136316, 7.8594513, -21.392513, -1.5493045, 7.9628983, -21.392513, -1.5855265, 7.7198067, -21.712654, -1.5601864, 7.7560043, -21.392513, -1.5855265, 7.8594513, -21.392513, -1.5493045, 7.8594513, -21.712654, -1.5136316, 7.9628983, -21.392513, -1.5855265, 7.999096, -21.712654, -1.5601864, 7.8594513, -21.889545, -1.5601864, 7.8594513, -21.712654, -1.5136316, 7.999096, -21.712654, -1.5601864, 7.999096, -21.712654, -1.5601864, 7.9628983, -21.392513, -1.5855265, 7.999096, -21.392513, -1.6889691, 7.673259, -21.712656, -1.6998296, 7.7560043, -21.392513, -1.5855265, 7.7198067, -21.712654, -1.5601864, 7.7198067, -21.889545, -1.6998296, 7.673259, -21.712656, -1.6998296, 7.7198067, -21.712654, -1.5601864, 7.673259, -21.712656, -1.6998296, 7.7198067, -21.392513, -1.6889691, 7.7560043, -21.392513, -1.5855265, 7.999096, -21.712654, -1.5601864, 7.999096, -21.392513, -1.6889691, 8.045644, -21.712656, -1.6998296, 8.045644, -21.712656, -1.6998296, 7.999096, -21.392513, -1.6889691, 7.9628983, -21.392513, -1.7924135, 7.8594513, -21.889545, -1.5601864, 7.999096, -21.712654, -1.5601864, 7.9628983, -21.843687, -1.5963855, 7.9628983, -21.843687, -1.5963855, 7.999096, -21.712654, -1.5601864, 8.045644, -21.712656, -1.6998296, 7.7198067, -21.889545, -1.6998296, 7.8594513, -21.889545, -1.5601864, 7.8594513, -21.948505, -1.6998296, 7.8594513, -21.948505, -1.6998296, 7.8594513, -21.889545, -1.5601864, 7.9628983, -21.843687, -1.5963855, 7.7560043, -21.843687, -1.8032722, 7.673259, -21.712656, -1.6998296, 7.7198067, -21.889545, -1.6998296, 7.7560043, -21.843687, -1.8032722, 7.7198067, -21.889545, -1.6998296, 7.8594513, -21.948505, -1.6998296, 7.7198067, -21.712656, -1.8394728, 7.7198067, -21.392513, -1.6889691, 7.673259, -21.712656, -1.6998296, 7.7560043, -21.843687, -1.8032722, 7.7198067, -21.712656, -1.8394728, 7.673259, -21.712656, -1.6998296, 7.7198067, -21.712656, -1.8394728, 7.7560043, -21.392513, -1.7924135, 7.7198067, -21.392513, -1.6889691, 7.7560043, -21.843687, -1.8032722, 7.8594513, -21.948505, -1.6998296, 7.8594513, -21.889545, -1.8394728, 7.7198067, -21.712656, -1.8394728, 7.7560043, -21.843687, -1.8032722, 7.8594513, -21.889545, -1.8394728, 7.8594513, -21.948505, -1.6998296, 7.9628983, -21.843687, -1.5963855, 7.999096, -21.889545, -1.6998296, 7.8594513, -21.889545, -1.8394728, 7.8594513, -21.948505, -1.6998296, 7.999096, -21.889545, -1.6998296, 7.9628983, -21.843687, -1.5963855, 8.045644, -21.712656, -1.6998296, 7.999096, -21.889545, -1.6998296, 7.8594513, -21.889545, -1.8394728, 7.999096, -21.889545, -1.6998296, 7.9628983, -21.843687, -1.8032722, 7.7560043, -21.392513, -1.7924135, 7.7198067, -21.712656, -1.8394728, 7.8594513, -21.712656, -1.8860278, 7.7198067, -21.712656, -1.8394728, 7.8594513, -21.889545, -1.8394728, 7.8594513, -21.712656, -1.8860278, 7.8594513, -21.712656, -1.8860278, 7.8594513, -21.889545, -1.8394728, 7.9628983, -21.843687, -1.8032722, 7.7560043, -21.392513, -1.7924135, 7.8594513, -21.712656, -1.8860278, 7.8594513, -21.392513, -1.8286123, 7.999096, -21.889545, -1.6998296, 8.045644, -21.712656, -1.6998296, 7.999096, -21.712656, -1.8394728, 7.999096, -21.889545, -1.6998296, 7.999096, -21.712656, -1.8394728, 7.9628983, -21.843687, -1.8032722, 7.8594513, -21.712656, -1.8860278, 7.9628983, -21.843687, -1.8032722, 7.999096, -21.712656, -1.8394728, 7.8594513, -21.392513, -1.8286123, 7.8594513, -21.712656, -1.8860278, 7.999096, -21.712656, -1.8394728, 8.045644, -21.712656, -1.6998296, 7.9628983, -21.392513, -1.7924135, 7.999096, -21.712656, -1.8394728, 7.8594513, -21.392513, -1.8286123, 7.999096, -21.712656, -1.8394728, 7.9628983, -21.392513, -1.7924135, 7.597028, -21.442598, -1.34617, 7.8594513, -21.312298, -1.1490684, 7.8594513, -21.49598, -1.2661133, 7.597028, -21.442598, -1.34617, 7.505172, -21.314877, -1.2550125, 7.8594513, -21.312298, -1.1490684, 7.8594513, -21.49598, -1.2661133, 7.8594513, -21.312298, -1.1490684, 8.213731, -21.314877, -1.2550125, 7.505172, -21.409956, -1.6840334, 7.505172, -21.314877, -1.2550125, 7.597028, -21.442598, -1.34617, 7.505172, -21.409956, -1.6840334, 7.597028, -21.442598, -1.34617, 7.8594513, -21.49598, -1.2661133, 7.505172, -21.409956, -1.6840334, 7.387093, -21.22724, -1.6845381, 7.505172, -21.314877, -1.2550125, 7.8594513, -21.49598, -1.2661133, 8.213731, -21.314877, -1.2550125, 8.121874, -21.442598, -1.34617, 8.121874, -21.442598, -1.34617, 8.213731, -21.314877, -1.2550125, 8.331823, -21.22724, -1.6845381, 7.505172, -21.409956, -1.6840334, 7.8594513, -21.49598, -1.2661133, 7.8594513, -21.492252, -1.6844721, 7.8594513, -21.492252, -1.6844721, 7.8594513, -21.49598, -1.2661133, 8.121874, -21.442598, -1.34617, 7.597028, -21.339413, -1.9443204, 7.387093, -21.22724, -1.6845381, 7.505172, -21.409956, -1.6840334, 7.597028, -21.339413, -1.9443204, 7.505172, -21.409956, -1.6840334, 7.8594513, -21.492252, -1.6844721, 7.597028, -21.339413, -1.9443204, 7.505172, -21.222763, -2.0274482, 7.387093, -21.22724, -1.6845381, 8.121874, -21.442598, -1.34617, 8.331823, -21.22724, -1.6845381, 8.213731, -21.409956, -1.6840334, 7.8594513, -21.492252, -1.6844721, 8.121874, -21.442598, -1.34617, 8.213731, -21.409956, -1.6840334, 8.213731, -21.409956, -1.6840334, 8.331823, -21.22724, -1.6845381, 8.213731, -21.222763, -2.0274482, 7.597028, -21.339413, -1.9443204, 7.8594513, -21.492252, -1.6844721, 7.8594513, -21.4032, -2.0336347, 7.505172, -21.222763, -2.0274482, 7.597028, -21.339413, -1.9443204, 7.8594513, -21.4032, -2.0336347, 7.8594513, -21.4032, -2.0336347, 7.8594513, -21.492252, -1.6844721, 8.213731, -21.409956, -1.6840334, 7.505172, -21.222763, -2.0274482, 7.8594513, -21.4032, -2.0336347, 7.8594513, -21.227156, -2.1436162, 8.213731, -21.409956, -1.6840334, 8.213731, -21.222763, -2.0274482, 8.121874, -21.339413, -1.9443204, 7.8594513, -21.4032, -2.0336347, 8.213731, -21.409956, -1.6840334, 8.121874, -21.339413, -1.9443204, 7.8594513, -21.227156, -2.1436162, 8.121874, -21.339413, -1.9443204, 8.213731, -21.222763, -2.0274482, 7.8594513, -21.227156, -2.1436162, 7.8594513, -21.4032, -2.0336347, 8.121874, -21.339413, -1.9443204, 7.8594513, -21.312298, -1.1490684, 8.121874, -21.111137, -1.4137647, 8.213731, -21.314877, -1.2550125, 8.213731, -21.314877, -1.2550125, 8.121874, -21.111137, -1.4137647, 8.213731, -21.024748, -1.6911418, 7.8594513, -21.312298, -1.1490684, 7.8594513, -21.038862, -1.3319969, 8.121874, -21.111137, -1.4137647, 7.8594513, -21.038862, -1.3319969, 8.213731, -21.024748, -1.6911418, 8.121874, -21.111137, -1.4137647, 8.213731, -21.314877, -1.2550125, 8.213731, -21.024748, -1.6911418, 8.331823, -21.22724, -1.6845381, 7.505172, -21.314877, -1.2550125, 7.8594513, -21.038862, -1.3319969, 7.8594513, -21.312298, -1.1490684, 7.505172, -21.314877, -1.2550125, 7.597028, -21.111137, -1.4137647, 7.8594513, -21.038862, -1.3319969, 7.387093, -21.22724, -1.6845381, 7.597028, -21.111137, -1.4137647, 7.505172, -21.314877, -1.2550125, 7.8594513, -21.038862, -1.3319969, 7.8594513, -20.941704, -1.6914926, 8.213731, -21.024748, -1.6911418, 7.597028, -21.111137, -1.4137647, 7.8594513, -20.941704, -1.6914926, 7.8594513, -21.038862, -1.3319969, 8.331823, -21.22724, -1.6845381, 8.213731, -21.024748, -1.6911418, 8.121874, -21.092463, -1.9232578, 7.8594513, -20.941704, -1.6914926, 8.121874, -21.092463, -1.9232578, 8.213731, -21.024748, -1.6911418, 8.331823, -21.22724, -1.6845381, 8.121874, -21.092463, -1.9232578, 8.213731, -21.222763, -2.0274482, 7.387093, -21.22724, -1.6845381, 7.505172, -21.024748, -1.6911418, 7.597028, -21.111137, -1.4137647, 7.597028, -21.111137, -1.4137647, 7.505172, -21.024748, -1.6911418, 7.8594513, -20.941704, -1.6914926, 7.505172, -21.222763, -2.0274482, 7.505172, -21.024748, -1.6911418, 7.387093, -21.22724, -1.6845381, 7.8594513, -21.02841, -2.012529, 8.213731, -21.222763, -2.0274482, 8.121874, -21.092463, -1.9232578, 7.8594513, -20.941704, -1.6914926, 7.8594513, -21.02841, -2.012529, 8.121874, -21.092463, -1.9232578, 7.505172, -21.024748, -1.6911418, 7.8594513, -21.02841, -2.012529, 7.8594513, -20.941704, -1.6914926, 7.8594513, -21.02841, -2.012529, 7.8594513, -21.227156, -2.1436162, 8.213731, -21.222763, -2.0274482, 7.505172, -21.222763, -2.0274482, 7.597028, -21.092463, -1.9232578, 7.505172, -21.024748, -1.6911418, 7.505172, -21.024748, -1.6911418, 7.597028, -21.092463, -1.9232578, 7.8594513, -21.02841, -2.012529, 7.597028, -21.092463, -1.9232578, 7.505172, -21.222763, -2.0274482, 7.8594513, -21.227156, -2.1436162, 7.597028, -21.092463, -1.9232578, 7.8594513, -21.227156, -2.1436162, 7.8594513, -21.02841, -2.012529, 7.7984443, -21.95358, -2.1591706, 7.8594513, -21.864279, -2.1397543, 7.8594513, -21.971691, -2.1298366, 7.7984443, -21.95358, -2.1591706, 7.7770944, -21.873667, -2.165554, 7.8594513, -21.864279, -2.1397543, 7.7770944, -21.999872, -2.2072396, 7.7984443, -21.95358, -2.1591706, 7.8594513, -21.971691, -2.1298366, 7.7770944, -21.999872, -2.2072396, 7.7770944, -21.873667, -2.165554, 7.7984443, -21.95358, -2.1591706, 7.7770944, -21.873667, -2.165554, 7.8594513, -21.694061, -2.224133, 7.8594513, -21.864279, -2.1397543, 7.8594513, -21.864279, -2.1397543, 7.8594513, -21.694061, -2.224133, 7.9204583, -21.701368, -2.2442064, 7.7770944, -21.873667, -2.165554, 7.7984443, -21.701368, -2.2442064, 7.8594513, -21.694061, -2.224133, 7.8594513, -21.864279, -2.1397543, 7.9204583, -21.701368, -2.2442064, 7.941808, -21.873667, -2.165554, 7.8594513, -21.971691, -2.1298366, 7.8594513, -21.864279, -2.1397543, 7.941808, -21.873667, -2.165554, 7.941808, -21.873667, -2.165554, 7.9204583, -21.701368, -2.2442064, 7.941808, -21.722242, -2.3015342, 7.7496324, -21.901846, -2.2429338, 7.7984443, -21.701368, -2.2442064, 7.7770944, -21.873667, -2.165554, 7.7770944, -21.999872, -2.2072396, 7.7496324, -21.901846, -2.2429338, 7.7770944, -21.873667, -2.165554, 7.7496324, -21.901846, -2.2429338, 7.7770944, -21.722242, -2.3015342, 7.7984443, -21.701368, -2.2442064, 7.941808, -21.873667, -2.165554, 7.941808, -21.722242, -2.3015342, 7.96927, -21.901846, -2.2429338, 7.96927, -21.901846, -2.2429338, 7.941808, -21.722242, -2.3015342, 7.9204583, -21.743116, -2.3588605, 7.8594513, -21.971691, -2.1298366, 7.941808, -21.873667, -2.165554, 7.9204583, -21.95358, -2.1591706, 7.9204583, -21.95358, -2.1591706, 7.941808, -21.873667, -2.165554, 7.96927, -21.901846, -2.2429338, 7.7770944, -21.999872, -2.2072396, 7.8594513, -21.971691, -2.1298366, 7.8594513, -22.032564, -2.1953483, 7.8594513, -22.032564, -2.1953483, 7.8594513, -21.971691, -2.1298366, 7.9204583, -21.95358, -2.1591706, 7.7984443, -21.995344, -2.2738247, 7.7496324, -21.901846, -2.2429338, 7.7770944, -21.999872, -2.2072396, 7.7984443, -21.995344, -2.2738247, 7.7770944, -21.999872, -2.2072396, 7.8594513, -22.032564, -2.1953483, 7.7770944, -21.930044, -2.320335, 7.7770944, -21.722242, -2.3015342, 7.7496324, -21.901846, -2.2429338, 7.7984443, -21.995344, -2.2738247, 7.7770944, -21.930044, -2.320335, 7.7496324, -21.901846, -2.2429338, 7.7770944, -21.930044, -2.320335, 7.7984443, -21.743116, -2.3588605, 7.7770944, -21.722242, -2.3015342, 7.7984443, -21.995344, -2.2738247, 7.8594513, -22.032564, -2.1953483, 7.8594513, -22.02807, -2.2846193, 7.7770944, -21.930044, -2.320335, 7.7984443, -21.995344, -2.2738247, 7.8594513, -22.02807, -2.2846193, 7.8594513, -22.032564, -2.1953483, 7.9204583, -21.95358, -2.1591706, 7.941808, -21.999872, -2.2072396, 7.8594513, -22.02807, -2.2846193, 7.8594513, -22.032564, -2.1953483, 7.941808, -21.999872, -2.2072396, 7.9204583, -21.95358, -2.1591706, 7.96927, -21.901846, -2.2429338, 7.941808, -21.999872, -2.2072396, 7.8594513, -22.02807, -2.2846193, 7.941808, -21.999872, -2.2072396, 7.9204583, -21.995344, -2.2738247, 7.7984443, -21.743116, -2.3588605, 7.7770944, -21.930044, -2.320335, 7.8594513, -21.939434, -2.3461366, 7.7770944, -21.930044, -2.320335, 7.8594513, -22.02807, -2.2846193, 7.8594513, -21.939434, -2.3461366, 7.8594513, -21.939434, -2.3461366, 7.8594513, -22.02807, -2.2846193, 7.9204583, -21.995344, -2.2738247, 7.7984443, -21.743116, -2.3588605, 7.8594513, -21.939434, -2.3461366, 7.8594513, -21.750423, -2.3789139, 7.941808, -21.999872, -2.2072396, 7.96927, -21.901846, -2.2429338, 7.941808, -21.930044, -2.320335, 7.941808, -21.999872, -2.2072396, 7.941808, -21.930044, -2.320335, 7.9204583, -21.995344, -2.2738247, 7.8594513, -21.939434, -2.3461366, 7.9204583, -21.995344, -2.2738247, 7.941808, -21.930044, -2.320335, 7.8594513, -21.750423, -2.3789139, 7.8594513, -21.939434, -2.3461366, 7.941808, -21.930044, -2.320335, 7.96927, -21.901846, -2.2429338, 7.9204583, -21.743116, -2.3588605, 7.941808, -21.930044, -2.320335, 7.8594513, -21.750423, -2.3789139, 7.941808, -21.930044, -2.320335, 7.9204583, -21.743116, -2.3588605, 7.7046704, -21.680813, -2.1014476, 7.8594513, -21.56882, -2.0185182, 7.8594513, -21.694244, -2.0463152, 7.7046704, -21.680813, -2.1014476, 7.6505113, -21.591642, -2.0767002, 7.8594513, -21.56882, -2.0185182, 7.8594513, -21.694244, -2.0463152, 7.8594513, -21.56882, -2.0185182, 8.068392, -21.591642, -2.0767002, 7.6505113, -21.730915, -2.2952814, 7.6505113, -21.591642, -2.0767002, 7.7046704, -21.680813, -2.1014476, 7.6505113, -21.730915, -2.2952814, 7.7046704, -21.680813, -2.1014476, 7.8594513, -21.694244, -2.0463152, 7.6505113, -21.730915, -2.2952814, 7.5808535, -21.62976, -2.3324466, 7.6505113, -21.591642, -2.0767002, 7.8594513, -21.694244, -2.0463152, 8.068392, -21.591642, -2.0767002, 8.014233, -21.680813, -2.1014476, 8.014233, -21.680813, -2.1014476, 8.068392, -21.591642, -2.0767002, 8.138048, -21.62976, -2.3324466, 7.6505113, -21.730915, -2.2952814, 7.8594513, -21.694244, -2.0463152, 7.8594513, -21.776608, -2.278893, 7.8594513, -21.776608, -2.278893, 7.8594513, -21.694244, -2.0463152, 8.014233, -21.680813, -2.1014476, 7.7046704, -21.744349, -2.4537702, 7.5808535, -21.62976, -2.3324466, 7.6505113, -21.730915, -2.2952814, 7.7046704, -21.744349, -2.4537702, 7.6505113, -21.730915, -2.2952814, 7.8594513, -21.776608, -2.278893, 7.7046704, -21.744349, -2.4537702, 7.6505113, -21.696476, -2.5233836, 7.5808535, -21.62976, -2.3324466, 8.014233, -21.680813, -2.1014476, 8.138048, -21.62976, -2.3324466, 8.068392, -21.730915, -2.2952814, 7.8594513, -21.776608, -2.278893, 8.014233, -21.680813, -2.1014476, 8.068392, -21.730915, -2.2952814, 8.068392, -21.730915, -2.2952814, 8.138048, -21.62976, -2.3324466, 8.068392, -21.696476, -2.5233836, 7.7046704, -21.744349, -2.4537702, 7.8594513, -21.776608, -2.278893, 7.8594513, -21.79773, -2.490386, 7.6505113, -21.696476, -2.5233836, 7.7046704, -21.744349, -2.4537702, 7.8594513, -21.79773, -2.490386, 7.8594513, -21.79773, -2.490386, 7.8594513, -21.776608, -2.278893, 8.068392, -21.730915, -2.2952814, 7.6505113, -21.696476, -2.5233836, 7.8594513, -21.79773, -2.490386, 7.8594513, -21.722359, -2.586853, 8.068392, -21.730915, -2.2952814, 8.068392, -21.696476, -2.5233836, 8.014233, -21.744349, -2.4537702, 7.8594513, -21.79773, -2.490386, 8.068392, -21.730915, -2.2952814, 8.014233, -21.744349, -2.4537702, 7.8594513, -21.722359, -2.586853, 8.014233, -21.744349, -2.4537702, 8.068392, -21.696476, -2.5233836, 7.8594513, -21.722359, -2.586853, 7.8594513, -21.79773, -2.490386, 8.014233, -21.744349, -2.4537702, 7.8594513, -21.56882, -2.0185182, 8.014233, -21.510761, -2.2058134, 8.068392, -21.591642, -2.0767002, 8.068392, -21.591642, -2.0767002, 8.014233, -21.510761, -2.2058134, 8.068392, -21.51887, -2.3769612, 7.8594513, -21.56882, -2.0185182, 7.8594513, -21.4542, -2.1750765, 8.014233, -21.510761, -2.2058134, 7.8594513, -21.4542, -2.1750765, 8.068392, -21.51887, -2.3769612, 8.014233, -21.510761, -2.2058134, 8.068392, -21.591642, -2.0767002, 8.068392, -21.51887, -2.3769612, 8.138048, -21.62976, -2.3324466, 7.6505113, -21.591642, -2.0767002, 7.8594513, -21.4542, -2.1750765, 7.8594513, -21.56882, -2.0185182, 7.6505113, -21.591642, -2.0767002, 7.7046704, -21.510761, -2.2058134, 7.8594513, -21.4542, -2.1750765, 7.5808535, -21.62976, -2.3324466, 7.7046704, -21.510761, -2.2058134, 7.6505113, -21.591642, -2.0767002, 7.8594513, -21.4542, -2.1750765, 7.8594513, -21.47291, -2.3939195, 8.068392, -21.51887, -2.3769612, 7.7046704, -21.510761, -2.2058134, 7.8594513, -21.47291, -2.3939195, 7.8594513, -21.4542, -2.1750765, 8.138048, -21.62976, -2.3324466, 8.068392, -21.51887, -2.3769612, 8.014233, -21.603245, -2.491922, 7.8594513, -21.47291, -2.3939195, 8.014233, -21.603245, -2.491922, 8.068392, -21.51887, -2.3769612, 8.138048, -21.62976, -2.3324466, 8.014233, -21.603245, -2.491922, 8.068392, -21.696476, -2.5233836, 7.5808535, -21.62976, -2.3324466, 7.6505113, -21.51887, -2.3769612, 7.7046704, -21.510761, -2.2058134, 7.7046704, -21.510761, -2.2058134, 7.6505113, -21.51887, -2.3769612, 7.8594513, -21.47291, -2.3939195, 7.6505113, -21.696476, -2.5233836, 7.6505113, -21.51887, -2.3769612, 7.5808535, -21.62976, -2.3324466, 7.8594513, -21.585766, -2.5543175, 8.068392, -21.696476, -2.5233836, 8.014233, -21.603245, -2.491922, 7.8594513, -21.47291, -2.3939195, 7.8594513, -21.585766, -2.5543175, 8.014233, -21.603245, -2.491922, 7.6505113, -21.51887, -2.3769612, 7.8594513, -21.585766, -2.5543175, 7.8594513, -21.47291, -2.3939195, 7.8594513, -21.585766, -2.5543175, 7.8594513, -21.722359, -2.586853, 8.068392, -21.696476, -2.5233836, 7.6505113, -21.696476, -2.5233836, 7.7046704, -21.603245, -2.491922, 7.6505113, -21.51887, -2.3769612, 7.6505113, -21.51887, -2.3769612, 7.7046704, -21.603245, -2.491922, 7.8594513, -21.585766, -2.5543175, 7.7046704, -21.603245, -2.491922, 7.6505113, -21.696476, -2.5233836, 7.8594513, -21.722359, -2.586853, 7.7046704, -21.603245, -2.491922, 7.8594513, -21.722359, -2.586853, 7.8594513, -21.585766, -2.5543175, -3.1935778, 3.8995895, -1.8595574, -2.9311557, 3.6870894, -1.6457057, -2.9311557, 3.8599355, -1.9618971, -3.1935778, 3.8995895, -1.8595574, -3.2854238, 3.7667134, -1.6248863, -2.9311557, 3.6870894, -1.6457057, -2.9311557, 3.8599355, -1.9618971, -2.9311557, 3.6870894, -1.6457057, -2.5768762, 3.7667134, -1.6248863, -2.9311557, 3.8599355, -1.9618971, -2.5768762, 3.7667134, -1.6248863, -2.6687322, 3.8995895, -1.8595574, -2.9311557, 3.6870894, -1.6457057, -2.6687322, 3.6513314, -1.305656, -2.5768762, 3.7667134, -1.6248863, -2.9311557, 3.6870894, -1.6457057, -2.9311557, 3.579767, -1.2413225, -2.6687322, 3.6513314, -1.305656, -3.2854238, 3.7667134, -1.6248863, -2.9311557, 3.579767, -1.2413225, -2.9311557, 3.6870894, -1.6457057, -3.2854238, 3.7667134, -1.6248863, -3.1935778, 3.6513314, -1.305656, -2.9311557, 3.579767, -1.2413225, -3.0859365, 3.6335888, -2.4534812, -2.9311557, 3.5589857, -2.2920804, -2.9311557, 3.5909512, -2.5021868, -3.0859365, 3.6335888, -2.4534812, -3.1401029, 3.6073158, -2.2966237, -2.9311557, 3.5589857, -2.2920804, -2.9311557, 3.5909512, -2.5021868, -2.9311557, 3.5589857, -2.2920804, -2.722207, 3.6073158, -2.2966237, -2.9311557, 3.5909512, -2.5021868, -2.722207, 3.6073158, -2.2966237, -2.7763748, 3.6335888, -2.4534812, -2.9311557, 3.5589857, -2.2920804, -2.7763748, 3.6077993, -2.096417, -2.722207, 3.6073158, -2.2966237, -2.9311557, 3.5589857, -2.2920804, -2.9311557, 3.5811186, -2.046321, -2.7763748, 3.6077993, -2.096417, -3.1401029, 3.6073158, -2.2966237, -2.9311557, 3.5811186, -2.046321, -2.9311557, 3.5589857, -2.2920804, -3.1401029, 3.6073158, -2.2966237, -3.0859365, 3.6077993, -2.096417, -2.9311557, 3.5811186, -2.046321, -3.1935806, 3.6513338, -1.3056598, -2.931157, 3.7287045, -1.0824075, -2.931157, 3.5797653, -1.2413273, -2.931157, 3.5797653, -1.2413273, -2.931157, 3.7287045, -1.0824075, -2.5768776, 3.7524617, -1.1856833, -3.1935806, 3.6513338, -1.3056598, -3.2854366, 3.7524617, -1.1856833, -2.931157, 3.7287045, -1.0824075, -2.931157, 3.5797653, -1.2413273, -2.5768776, 3.7524617, -1.1856833, -2.668735, 3.6513338, -1.3056598, -3.2854366, 3.766717, -1.6248941, -3.2854366, 3.7524617, -1.1856833, -3.1935806, 3.6513338, -1.3056598, -2.668735, 3.6513338, -1.3056598, -2.5768776, 3.7524617, -1.1856833, -2.4587846, 3.9438598, -1.5800798, -3.2854366, 3.766717, -1.6248941, -3.4035153, 3.9438598, -1.5800798, -3.2854366, 3.7524617, -1.1856833, -2.668735, 3.6513338, -1.3056598, -2.4587846, 3.9438598, -1.5800798, -2.5768776, 3.766717, -1.6248941, -3.1935806, 3.8995967, -1.8595555, -3.4035153, 3.9438598, -1.5800798, -3.2854366, 3.766717, -1.6248941, -2.5768776, 3.766717, -1.6248941, -2.4587846, 3.9438598, -1.5800798, -2.5768776, 4.0332108, -1.9111595, -3.1935806, 3.8995967, -1.8595555, -3.2854366, 4.0332108, -1.9111595, -3.4035153, 3.9438598, -1.5800798, -2.5768776, 3.766717, -1.6248941, -2.5768776, 4.0332108, -1.9111595, -2.668735, 3.8995967, -1.8595555, -3.2854366, 4.0332108, -1.9111595, -3.1935806, 3.8995967, -1.8595555, -2.931157, 3.8599317, -1.9618936, -2.931157, 4.0577416, -2.0247965, -2.668735, 3.8995967, -1.8595555, -2.5768776, 4.0332108, -1.9111595, -3.2854366, 4.0332108, -1.9111595, -2.931157, 3.8599317, -1.9618936, -2.931157, 4.0577416, -2.0247965, -2.931157, 4.0577416, -2.0247965, -2.931157, 3.8599317, -1.9618936, -2.668735, 3.8995967, -1.8595555, -2.931157, 3.7287045, -1.0824075, -2.668735, 3.9892077, -1.288969, -2.5768776, 3.7524617, -1.1856833, -2.5768776, 3.7524617, -1.1856833, -2.668735, 3.9892077, -1.288969, -2.5768776, 4.141674, -1.5362737, -2.931157, 3.7287045, -1.0824075, -2.931157, 4.0389485, -1.191823, -2.668735, 3.9892077, -1.288969, -2.931157, 4.0389485, -1.191823, -2.5768776, 4.141674, -1.5362737, -2.668735, 3.9892077, -1.288969, -2.5768776, 3.7524617, -1.1856833, -2.5768776, 4.141674, -1.5362737, -2.4587846, 3.9438598, -1.5800798, -3.2854366, 3.7524617, -1.1856833, -2.931157, 4.0389485, -1.191823, -2.931157, 3.7287045, -1.0824075, -3.2854366, 3.7524617, -1.1856833, -3.1935806, 3.9892077, -1.288969, -2.931157, 4.0389485, -1.191823, -3.4035153, 3.9438598, -1.5800798, -3.1935806, 3.9892077, -1.288969, -3.2854366, 3.7524617, -1.1856833, -2.931157, 4.0389485, -1.191823, -2.931157, 4.2222066, -1.5160246, -2.5768776, 4.141674, -1.5362737, -3.1935806, 3.9892077, -1.288969, -2.931157, 4.2222066, -1.5160246, -2.931157, 4.0389485, -1.191823, -2.4587846, 3.9438598, -1.5800798, -2.5768776, 4.141674, -1.5362737, -2.668735, 4.1336055, -1.777915, -2.931157, 4.2222066, -1.5160246, -2.668735, 4.1336055, -1.777915, -2.5768776, 4.141674, -1.5362737, -2.4587846, 3.9438598, -1.5800798, -2.668735, 4.1336055, -1.777915, -2.5768776, 4.0332108, -1.9111595, -3.4035153, 3.9438598, -1.5800798, -3.2854366, 4.141674, -1.5362737, -3.1935806, 3.9892077, -1.288969, -3.1935806, 3.9892077, -1.288969, -3.2854366, 4.141674, -1.5362737, -2.931157, 4.2222066, -1.5160246, -3.2854366, 4.0332108, -1.9111595, -3.2854366, 4.141674, -1.5362737, -3.4035153, 3.9438598, -1.5800798, -2.931157, 4.2177896, -1.8485272, -2.5768776, 4.0332108, -1.9111595, -2.668735, 4.1336055, -1.777915, -2.931157, 4.2222066, -1.5160246, -2.931157, 4.2177896, -1.8485272, -2.668735, 4.1336055, -1.777915, -3.2854366, 4.141674, -1.5362737, -2.931157, 4.2177896, -1.8485272, -2.931157, 4.2222066, -1.5160246, -2.931157, 4.2177896, -1.8485272, -2.931157, 4.0577416, -2.0247965, -2.5768776, 4.0332108, -1.9111595, -3.2854366, 4.0332108, -1.9111595, -3.1935806, 4.1336055, -1.777915, -3.2854366, 4.141674, -1.5362737, -3.2854366, 4.141674, -1.5362737, -3.1935806, 4.1336055, -1.777915, -2.931157, 4.2177896, -1.8485272, -3.1935806, 4.1336055, -1.777915, -3.2854366, 4.0332108, -1.9111595, -2.931157, 4.0577416, -2.0247965, -3.1935806, 4.1336055, -1.777915, -2.931157, 4.0577416, -2.0247965, -2.931157, 4.2177896, -1.8485272, -3.034594, 3.324797, -1.6474974, -2.9311528, 3.431215, -1.5348387, -2.9311528, 3.2713995, -1.6237905, -3.034594, 3.324797, -1.6474974, -3.0708017, 3.4427564, -1.5799403, -2.9311528, 3.431215, -1.5348387, -3.0708017, 3.3060207, -1.7590828, -3.034594, 3.324797, -1.6474974, -2.9311528, 3.2713995, -1.6237905, -3.0708017, 3.3060207, -1.7590828, -3.0708017, 3.4427564, -1.5799403, -3.034594, 3.324797, -1.6474974, -3.0708017, 3.4427564, -1.5799403, -2.9311528, 3.7502108, -1.490046, -2.9311528, 3.431215, -1.5348387, -2.9311528, 3.431215, -1.5348387, -2.9311528, 3.7502108, -1.490046, -2.8277116, 3.759186, -1.5251193, -3.0708017, 3.4427564, -1.5799403, -3.034594, 3.759186, -1.5251193, -2.9311528, 3.7502108, -1.490046, -2.9311528, 3.431215, -1.5348387, -2.8277116, 3.759186, -1.5251193, -2.791504, 3.4427564, -1.5799403, -2.9311528, 3.2713995, -1.6237905, -2.9311528, 3.431215, -1.5348387, -2.791504, 3.4427564, -1.5799403, -2.791504, 3.4427564, -1.5799403, -2.8277116, 3.759186, -1.5251193, -2.791504, 3.784829, -1.6253259, -3.1173468, 3.477378, -1.7152324, -3.034594, 3.759186, -1.5251193, -3.0708017, 3.4427564, -1.5799403, -3.0708017, 3.3060207, -1.7590828, -3.1173468, 3.477378, -1.7152324, -3.0708017, 3.4427564, -1.5799403, -3.1173468, 3.477378, -1.7152324, -3.0708017, 3.784829, -1.6253259, -3.034594, 3.759186, -1.5251193, -2.791504, 3.4427564, -1.5799403, -2.791504, 3.784829, -1.6253259, -2.7449532, 3.477378, -1.7152324, -2.7449532, 3.477378, -1.7152324, -2.791504, 3.784829, -1.6253259, -2.8277116, 3.8104749, -1.7255449, -2.9311528, 3.2713995, -1.6237905, -2.791504, 3.4427564, -1.5799403, -2.8277116, 3.324797, -1.6474974, -2.8277116, 3.324797, -1.6474974, -2.791504, 3.4427564, -1.5799403, -2.7449532, 3.477378, -1.7152324, -3.0708017, 3.3060207, -1.7590828, -2.9311528, 3.2713995, -1.6237905, -2.9311528, 3.2488918, -1.7737021, -2.9311528, 3.2488918, -1.7737021, -2.9311528, 3.2713995, -1.6237905, -2.8277116, 3.324797, -1.6474974, -3.034594, 3.3760855, -1.8479218, -3.1173468, 3.477378, -1.7152324, -3.0708017, 3.3060207, -1.7590828, -3.034594, 3.3760855, -1.8479218, -3.0708017, 3.3060207, -1.7590828, -2.9311528, 3.2488918, -1.7737021, -3.0708017, 3.511996, -1.8505123, -3.0708017, 3.784829, -1.6253259, -3.1173468, 3.477378, -1.7152324, -3.034594, 3.3760855, -1.8479218, -3.0708017, 3.511996, -1.8505123, -3.1173468, 3.477378, -1.7152324, -3.0708017, 3.511996, -1.8505123, -3.034594, 3.8104749, -1.7255449, -3.0708017, 3.784829, -1.6253259, -3.034594, 3.3760855, -1.8479218, -2.9311528, 3.2488918, -1.7737021, -2.9311528, 3.3406389, -1.8943624, -3.0708017, 3.511996, -1.8505123, -3.034594, 3.3760855, -1.8479218, -2.9311528, 3.3406389, -1.8943624, -2.9311528, 3.2488918, -1.7737021, -2.8277116, 3.324797, -1.6474974, -2.791504, 3.3060207, -1.7590828, -2.9311528, 3.3406389, -1.8943624, -2.9311528, 3.2488918, -1.7737021, -2.791504, 3.3060207, -1.7590828, -2.8277116, 3.324797, -1.6474974, -2.7449532, 3.477378, -1.7152324, -2.791504, 3.3060207, -1.7590828, -2.9311528, 3.3406389, -1.8943624, -2.791504, 3.3060207, -1.7590828, -2.8277116, 3.3760855, -1.8479218, -3.034594, 3.8104749, -1.7255449, -3.0708017, 3.511996, -1.8505123, -2.9311528, 3.5235374, -1.8956139, -3.0708017, 3.511996, -1.8505123, -2.9311528, 3.3406389, -1.8943624, -2.9311528, 3.5235374, -1.8956139, -2.9311528, 3.5235374, -1.8956139, -2.9311528, 3.3406389, -1.8943624, -2.8277116, 3.3760855, -1.8479218, -3.034594, 3.8104749, -1.7255449, -2.9311528, 3.5235374, -1.8956139, -2.9311528, 3.8194501, -1.7606182, -2.791504, 3.3060207, -1.7590828, -2.7449532, 3.477378, -1.7152324, -2.791504, 3.511996, -1.8505123, -2.791504, 3.3060207, -1.7590828, -2.791504, 3.511996, -1.8505123, -2.8277116, 3.3760855, -1.8479218, -2.9311528, 3.5235374, -1.8956139, -2.8277116, 3.3760855, -1.8479218, -2.791504, 3.511996, -1.8505123, -2.9311528, 3.8194501, -1.7606182, -2.9311528, 3.5235374, -1.8956139, -2.791504, 3.511996, -1.8505123, -2.7449532, 3.477378, -1.7152324, -2.8277116, 3.8104749, -1.7255449, -2.791504, 3.511996, -1.8505123, -2.9311528, 3.8194501, -1.7606182, -2.791504, 3.511996, -1.8505123, -2.8277116, 3.8104749, -1.7255449, -2.9921641, 3.357846, -2.219953, -2.9311528, 3.439559, -2.179008, -2.9311528, 3.3330348, -2.1960335, -2.9921641, 3.357846, -2.219953, -3.0135126, 3.436851, -2.2063189, -2.9311528, 3.439559, -2.179008, -3.0135126, 3.324916, -2.2780023, -2.9921641, 3.357846, -2.219953, -2.9311528, 3.3330348, -2.1960335, -3.0135126, 3.324916, -2.2780023, -3.0135126, 3.436851, -2.2063189, -2.9921641, 3.357846, -2.219953, -3.0135126, 3.436851, -2.2063189, -2.9311528, 3.6253839, -2.2185574, -2.9311528, 3.439559, -2.179008, -2.9311528, 3.439559, -2.179008, -2.9311528, 3.6253839, -2.2185574, -2.8701415, 3.6232789, -2.239798, -3.0135126, 3.436851, -2.2063189, -2.9921641, 3.6232789, -2.239798, -2.9311528, 3.6253839, -2.2185574, -2.9311528, 3.439559, -2.179008, -2.8701415, 3.6232789, -2.239798, -2.848793, 3.436851, -2.2063189, -2.9311528, 3.3330348, -2.1960335, -2.9311528, 3.439559, -2.179008, -2.848793, 3.436851, -2.2063189, -2.848793, 3.436851, -2.2063189, -2.8701415, 3.6232789, -2.239798, -2.848793, 3.617262, -2.3005147, -3.0409703, 3.428742, -2.2882862, -2.9921641, 3.6232789, -2.239798, -3.0135126, 3.436851, -2.2063189, -3.0135126, 3.324916, -2.2780023, -3.0409703, 3.428742, -2.2882862, -3.0135126, 3.436851, -2.2063189, -3.0409703, 3.428742, -2.2882862, -3.0135126, 3.617262, -2.3005147, -2.9921641, 3.6232789, -2.239798, -2.848793, 3.436851, -2.2063189, -2.848793, 3.617262, -2.3005147, -2.8213353, 3.428742, -2.2882862, -2.8213353, 3.428742, -2.2882862, -2.848793, 3.617262, -2.3005147, -2.8701415, 3.6112561, -2.361228, -2.9311528, 3.3330348, -2.1960335, -2.848793, 3.436851, -2.2063189, -2.8701415, 3.357846, -2.219953, -2.8701415, 3.357846, -2.219953, -2.848793, 3.436851, -2.2063189, -2.8213353, 3.428742, -2.2882862, -3.0135126, 3.324916, -2.2780023, -2.9311528, 3.3330348, -2.1960335, -2.9311528, 3.2903087, -2.274579, -2.9311528, 3.2903087, -2.274579, -2.9311528, 3.3330348, -2.1960335, -2.8701415, 3.357846, -2.219953, -2.9921641, 3.345823, -2.3413825, -3.0409703, 3.428742, -2.2882862, -3.0135126, 3.324916, -2.2780023, -2.9921641, 3.345823, -2.3413825, -3.0135126, 3.324916, -2.2780023, -2.9311528, 3.2903087, -2.274579, -3.0135126, 3.4206212, -2.370242, -3.0135126, 3.617262, -2.3005147, -3.0409703, 3.428742, -2.2882862, -2.9921641, 3.345823, -2.3413825, -3.0135126, 3.4206212, -2.370242, -3.0409703, 3.428742, -2.2882862, -3.0135126, 3.4206212, -2.370242, -2.9921641, 3.6112561, -2.361228, -3.0135126, 3.617262, -2.3005147, -2.9921641, 3.345823, -2.3413825, -2.9311528, 3.2903087, -2.274579, -2.9311528, 3.3167953, -2.3599591, -3.0135126, 3.4206212, -2.370242, -2.9921641, 3.345823, -2.3413825, -2.9311528, 3.3167953, -2.3599591, -2.9311528, 3.2903087, -2.274579, -2.8701415, 3.357846, -2.219953, -2.848793, 3.324916, -2.2780023, -2.9311528, 3.3167953, -2.3599591, -2.9311528, 3.2903087, -2.274579, -2.848793, 3.324916, -2.2780023, -2.8701415, 3.357846, -2.219953, -2.8213353, 3.428742, -2.2882862, -2.848793, 3.324916, -2.2780023, -2.9311528, 3.3167953, -2.3599591, -2.848793, 3.324916, -2.2780023, -2.8701415, 3.345823, -2.3413825, -2.9921641, 3.6112561, -2.361228, -3.0135126, 3.4206212, -2.370242, -2.9311528, 3.417915, -2.3975658, -3.0135126, 3.4206212, -2.370242, -2.9311528, 3.3167953, -2.3599591, -2.9311528, 3.417915, -2.3975658, -2.9311528, 3.417915, -2.3975658, -2.9311528, 3.3167953, -2.3599591, -2.8701415, 3.345823, -2.3413825, -2.9921641, 3.6112561, -2.361228, -2.9311528, 3.417915, -2.3975658, -2.9311528, 3.609141, -2.3824706, -2.848793, 3.324916, -2.2780023, -2.8213353, 3.428742, -2.2882862, -2.848793, 3.4206212, -2.370242, -2.848793, 3.324916, -2.2780023, -2.848793, 3.4206212, -2.370242, -2.8701415, 3.345823, -2.3413825, -2.9311528, 3.417915, -2.3975658, -2.8701415, 3.345823, -2.3413825, -2.848793, 3.4206212, -2.370242, -2.9311528, 3.609141, -2.3824706, -2.9311528, 3.417915, -2.3975658, -2.848793, 3.4206212, -2.370242, -2.8213353, 3.428742, -2.2882862, -2.8701415, 3.6112561, -2.361228, -2.848793, 3.4206212, -2.370242, -2.9311528, 3.609141, -2.3824706, -2.848793, 3.4206212, -2.370242, -2.8701415, 3.6112561, -2.361228, -3.085928, 3.6077986, -2.0964143, -2.9311543, 3.695728, -1.988307, -2.9311543, 3.5811217, -2.0463157, -2.9311543, 3.5811217, -2.0463157, -2.9311543, 3.695728, -1.988307, -2.7222042, 3.6880543, -2.0503378, -3.085928, 3.6077986, -2.0964143, -3.1401043, 3.6880543, -2.0503378, -2.9311543, 3.695728, -1.988307, -2.9311543, 3.5811217, -2.0463157, -2.7222042, 3.6880543, -2.0503378, -2.7763805, 3.6077986, -2.0964143, -3.1401043, 3.6073084, -2.2966123, -3.1401043, 3.6880543, -2.0503378, -3.085928, 3.6077986, -2.0964143, -2.7763805, 3.6077986, -2.0964143, -2.7222042, 3.6880543, -2.0503378, -2.6525536, 3.7145243, -2.3075337, -3.1401043, 3.6073084, -2.2966123, -3.2097464, 3.7145243, -2.3075337, -3.1401043, 3.6880543, -2.0503378, -2.7763805, 3.6077986, -2.0964143, -2.6525536, 3.7145243, -2.3075337, -2.7222042, 3.6073084, -2.2966123, -3.085928, 3.6335886, -2.4534798, -3.2097464, 3.7145243, -2.3075337, -3.1401043, 3.6073084, -2.2966123, -2.7222042, 3.6073084, -2.2966123, -2.6525536, 3.7145243, -2.3075337, -2.7222042, 3.697219, -2.5090442, -3.085928, 3.6335886, -2.4534798, -3.1401043, 3.697219, -2.5090442, -3.2097464, 3.7145243, -2.3075337, -2.7222042, 3.6073084, -2.2966123, -2.7222042, 3.697219, -2.5090442, -2.7763805, 3.6335886, -2.4534798, -3.1401043, 3.697219, -2.5090442, -3.085928, 3.6335886, -2.4534798, -2.9311543, 3.5909529, -2.5021935, -2.9311543, 3.687878, -2.5769706, -2.7763805, 3.6335886, -2.4534798, -2.7222042, 3.697219, -2.5090442, -3.1401043, 3.697219, -2.5090442, -2.9311543, 3.5909529, -2.5021935, -2.9311543, 3.687878, -2.5769706, -2.9311543, 3.687878, -2.5769706, -2.9311543, 3.5909529, -2.5021935, -2.7763805, 3.6335886, -2.4534798, -2.9311543, 3.695728, -1.988307, -2.7763805, 3.7984102, -2.1553535, -2.7222042, 3.6880543, -2.0503378, -2.7222042, 3.6880543, -2.0503378, -2.7763805, 3.7984102, -2.1553535, -2.7222042, 3.8329906, -2.3231764, -2.9311543, 3.695728, -1.988307, -2.9311543, 3.845578, -2.1115575, -2.7763805, 3.7984102, -2.1553535, -2.9311543, 3.845578, -2.1115575, -2.7222042, 3.8329906, -2.3231764, -2.7763805, 3.7984102, -2.1553535, -2.7222042, 3.6880543, -2.0503378, -2.7222042, 3.8329906, -2.3231764, -2.6525536, 3.7145243, -2.3075337, -3.1401043, 3.6880543, -2.0503378, -2.9311543, 3.845578, -2.1115575, -2.9311543, 3.695728, -1.988307, -3.1401043, 3.6880543, -2.0503378, -3.085928, 3.7984102, -2.1553535, -2.9311543, 3.845578, -2.1115575, -3.2097464, 3.7145243, -2.3075337, -3.085928, 3.7984102, -2.1553535, -3.1401043, 3.6880543, -2.0503378, -2.9311543, 3.845578, -2.1115575, -2.9311543, 3.8817086, -2.3282094, -2.7222042, 3.8329906, -2.3231764, -3.085928, 3.7984102, -2.1553535, -2.9311543, 3.8817086, -2.3282094, -2.9311543, 3.845578, -2.1115575, -2.6525536, 3.7145243, -2.3075337, -2.7222042, 3.8329906, -2.3231764, -2.7763805, 3.7797475, -2.4554658, -2.9311543, 3.8817086, -2.3282094, -2.7763805, 3.7797475, -2.4554658, -2.7222042, 3.8329906, -2.3231764, -2.6525536, 3.7145243, -2.3075337, -2.7763805, 3.7797475, -2.4554658, -2.7222042, 3.697219, -2.5090442, -3.2097464, 3.7145243, -2.3075337, -3.1401043, 3.8329906, -2.3231764, -3.085928, 3.7984102, -2.1553535, -3.085928, 3.7984102, -2.1553535, -3.1401043, 3.8329906, -2.3231764, -2.9311543, 3.8817086, -2.3282094, -3.1401043, 3.697219, -2.5090442, -3.1401043, 3.8329906, -2.3231764, -3.2097464, 3.7145243, -2.3075337, -2.9311543, 3.812152, -2.5115814, -2.7222042, 3.697219, -2.5090442, -2.7763805, 3.7797475, -2.4554658, -2.9311543, 3.8817086, -2.3282094, -2.9311543, 3.812152, -2.5115814, -2.7763805, 3.7797475, -2.4554658, -3.1401043, 3.8329906, -2.3231764, -2.9311543, 3.812152, -2.5115814, -2.9311543, 3.8817086, -2.3282094, -2.9311543, 3.812152, -2.5115814, -2.9311543, 3.687878, -2.5769706, -2.7222042, 3.697219, -2.5090442, -3.1401043, 3.697219, -2.5090442, -3.085928, 3.7797475, -2.4554658, -3.1401043, 3.8329906, -2.3231764, -3.1401043, 3.8329906, -2.3231764, -3.085928, 3.7797475, -2.4554658, -2.9311543, 3.812152, -2.5115814, -3.085928, 3.7797475, -2.4554658, -3.1401043, 3.697219, -2.5090442, -2.9311543, 3.687878, -2.5769706, -3.085928, 3.7797475, -2.4554658, -2.9311543, 3.687878, -2.5769706, -2.9311543, 3.812152, -2.5115814, -16.306217, 3.8995876, -8.693363, -16.639778, 3.6870875, -8.751106, -16.484772, 3.8599336, -8.475517, -16.306217, 3.8995876, -8.693363, -16.341208, 3.7667115, -8.942926, -16.639778, 3.6870875, -8.751106, -16.484772, 3.8599336, -8.475517, -16.639778, 3.6870875, -8.751106, -16.958773, 3.7667115, -8.595573, -16.484772, 3.8599336, -8.475517, -16.958773, 3.7667115, -8.595573, -16.763668, 3.8995876, -8.436068, -16.639778, 3.6870875, -8.751106, -17.035208, 3.6513293, -8.918842, -16.958773, 3.7667115, -8.595573, -16.639778, 3.6870875, -8.751106, -16.83802, 3.579765, -9.103563, -17.035208, 3.6513293, -8.918842, -16.341208, 3.7667115, -8.942926, -16.83802, 3.579765, -9.103563, -16.639778, 3.6870875, -8.751106, -16.341208, 3.7667115, -8.942926, -16.577757, 3.6513293, -9.17614, -16.83802, 3.579765, -9.103563, -16.108875, 3.6335871, -8.122934, -16.322905, 3.5589838, -8.187733, -16.219904, 3.5909493, -8.004604, -16.108875, 3.6335871, -8.122934, -16.138561, 3.607314, -8.286205, -16.322905, 3.5589838, -8.187733, -16.219904, 3.5909493, -8.004604, -16.322905, 3.5589838, -8.187733, -16.502796, 3.607314, -8.081339, -16.219904, 3.5909493, -8.004604, -16.502796, 3.607314, -8.081339, -16.378687, 3.6335871, -7.9711776, -16.322905, 3.5589838, -8.187733, -16.553732, 3.6077974, -8.2823925, -16.502796, 3.607314, -8.081339, -16.322905, 3.5589838, -8.187733, -16.443384, 3.5811167, -8.401934, -16.553732, 3.6077974, -8.2823925, -16.138561, 3.607314, -8.286205, -16.443384, 3.5811167, -8.401934, -16.322905, 3.5589838, -8.187733, -16.138561, 3.607314, -8.286205, -16.28392, 3.6077974, -8.434149, -16.443384, 3.5811167, -8.401934, -16.577753, 3.651332, -9.176138, -16.915924, 3.7287025, -9.242073, -16.838017, 3.5797634, -9.1035595, -16.838017, 3.5797634, -9.1035595, -16.915924, 3.7287025, -9.242073, -17.174082, 3.7524595, -8.97838, -16.577753, 3.651332, -9.176138, -16.556507, 3.7524595, -9.325739, -16.915924, 3.7287025, -9.242073, -16.838017, 3.5797634, -9.1035595, -17.174082, 3.7524595, -8.97838, -17.035204, 3.651332, -8.91884, -16.341192, 3.766715, -8.942926, -16.556507, 3.7524595, -9.325739, -16.577753, 3.651332, -9.176138, -17.035204, 3.651332, -8.91884, -17.174082, 3.7524595, -8.97838, -17.083666, 3.943858, -8.576735, -16.341192, 3.766715, -8.942926, -16.260246, 3.943858, -9.039871, -16.556507, 3.7524595, -9.325739, -17.035204, 3.651332, -8.91884, -17.083666, 3.943858, -8.576735, -16.958767, 3.7667148, -8.595568, -16.306215, 3.8995948, -8.693367, -16.260246, 3.943858, -9.039871, -16.341192, 3.766715, -8.942926, -16.958767, 3.7667148, -8.595568, -17.083666, 3.943858, -8.576735, -16.81843, 4.033209, -8.346062, -16.306215, 3.8995948, -8.693367, -16.200855, 4.033209, -8.6934185, -16.260246, 3.943858, -9.039871, -16.958767, 3.7667148, -8.595568, -16.81843, 4.033209, -8.346062, -16.763666, 3.8995945, -8.4360695, -16.200855, 4.033209, -8.6934185, -16.306215, 3.8995948, -8.693367, -16.484772, 3.8599298, -8.475521, -16.453936, 4.0577397, -8.420694, -16.763666, 3.8995945, -8.4360695, -16.81843, 4.033209, -8.346062, -16.200855, 4.033209, -8.6934185, -16.484772, 3.8599298, -8.475521, -16.453936, 4.0577397, -8.420694, -16.453936, 4.0577397, -8.420694, -16.484772, 3.8599298, -8.475521, -16.763666, 3.8995945, -8.4360695, -16.915924, 3.7287025, -9.242073, -17.043386, 3.9892056, -8.933388, -17.174082, 3.7524595, -8.97838, -17.174082, 3.7524595, -8.97838, -17.043386, 3.9892056, -8.933388, -17.00221, 4.141672, -8.672808, -16.915924, 3.7287025, -9.242073, -16.862286, 4.038946, -9.146708, -17.043386, 3.9892056, -8.933388, -16.862286, 4.038946, -9.146708, -17.00221, 4.141672, -8.672808, -17.043386, 3.9892056, -8.933388, -17.174082, 3.7524595, -8.97838, -17.00221, 4.141672, -8.672808, -17.083666, 3.943858, -8.576735, -16.556507, 3.7524595, -9.325739, -16.862286, 4.038946, -9.146708, -16.915924, 3.7287025, -9.242073, -16.556507, 3.7524595, -9.325739, -16.585936, 3.9892056, -9.190685, -16.862286, 4.038946, -9.146708, -16.260246, 3.943858, -9.039871, -16.585936, 3.9892056, -9.190685, -16.556507, 3.7524595, -9.325739, -16.862286, 4.038946, -9.146708, -16.703352, 4.2222047, -8.864136, -17.00221, 4.141672, -8.672808, -16.585936, 3.9892056, -9.190685, -16.703352, 4.2222047, -8.864136, -16.862286, 4.038946, -9.146708, -17.083666, 3.943858, -8.576735, -17.00221, 4.141672, -8.672808, -16.803688, 4.1336036, -8.507227, -16.703352, 4.2222047, -8.864136, -16.803688, 4.1336036, -8.507227, -17.00221, 4.141672, -8.672808, -17.083666, 3.943858, -8.576735, -16.803688, 4.1336036, -8.507227, -16.81843, 4.033209, -8.346062, -16.260246, 3.943858, -9.039871, -16.384638, 4.141672, -9.020166, -16.585936, 3.9892056, -9.190685, -16.585936, 3.9892056, -9.190685, -16.384638, 4.141672, -9.020166, -16.703352, 4.2222047, -8.864136, -16.200855, 4.033209, -8.6934185, -16.384638, 4.141672, -9.020166, -16.260246, 3.943858, -9.039871, -16.540348, 4.2177877, -8.574329, -16.81843, 4.033209, -8.346062, -16.803688, 4.1336036, -8.507227, -16.703352, 4.2222047, -8.864136, -16.540348, 4.2177877, -8.574329, -16.803688, 4.1336036, -8.507227, -16.384638, 4.141672, -9.020166, -16.540348, 4.2177877, -8.574329, -16.703352, 4.2222047, -8.864136, -16.540348, 4.2177877, -8.574329, -16.453936, 4.0577397, -8.420694, -16.81843, 4.033209, -8.346062, -16.200855, 4.033209, -8.6934185, -16.346237, 4.1336036, -8.764523, -16.384638, 4.141672, -9.020166, -16.384638, 4.141672, -9.020166, -16.346237, 4.1336036, -8.764523, -16.540348, 4.2177877, -8.574329, -16.346237, 4.1336036, -8.764523, -16.200855, 4.033209, -8.6934185, -16.453936, 4.0577397, -8.420694, -16.346237, 4.1336036, -8.764523, -16.453936, 4.0577397, -8.420694, -16.540348, 4.2177877, -8.574329, -16.548744, 3.324795, -8.800255, -16.694132, 3.4312131, -8.847736, -16.650524, 3.2713976, -8.770206, -16.548744, 3.324795, -8.800255, -16.550304, 3.4427547, -8.876886, -16.694132, 3.4312131, -8.847736, -16.462482, 3.3060188, -8.720747, -16.548744, 3.324795, -8.800255, -16.650524, 3.2713976, -8.770206, -16.462482, 3.3060188, -8.720747, -16.550304, 3.4427547, -8.876886, -16.548744, 3.324795, -8.800255, -16.550304, 3.4427547, -8.876886, -16.716091, 3.7502089, -8.886778, -16.694132, 3.4312131, -8.847736, -16.694132, 3.4312131, -8.847736, -16.716091, 3.7502089, -8.886778, -16.789055, 3.7591841, -8.805498, -16.550304, 3.4427547, -8.876886, -16.608738, 3.7591841, -8.906918, -16.716091, 3.7502089, -8.886778, -16.694132, 3.4312131, -8.847736, -16.789055, 3.7591841, -8.805498, -16.793737, 3.4427545, -8.739965, -16.650524, 3.2713976, -8.770206, -16.694132, 3.4312131, -8.847736, -16.793737, 3.4427545, -8.739965, -16.793737, 3.4427545, -8.739965, -16.789055, 3.7591841, -8.805498, -16.771488, 3.784827, -8.700407, -16.44341, 3.477376, -8.781784, -16.608738, 3.7591841, -8.906918, -16.550304, 3.4427547, -8.876886, -16.462482, 3.3060188, -8.720747, -16.44341, 3.477376, -8.781784, -16.550304, 3.4427547, -8.876886, -16.44341, 3.477376, -8.781784, -16.528055, 3.784827, -8.837328, -16.608738, 3.7591841, -8.906918, -16.793737, 3.4427545, -8.739965, -16.771488, 3.784827, -8.700407, -16.767986, 3.477376, -8.599226, -16.767986, 3.477376, -8.599226, -16.771488, 3.784827, -8.700407, -16.6908, 3.810473, -8.630808, -16.650524, 3.2713976, -8.770206, -16.793737, 3.4427545, -8.739965, -16.729061, 3.324795, -8.698833, -16.729061, 3.324795, -8.698833, -16.793737, 3.4427545, -8.739965, -16.767986, 3.477376, -8.599226, -16.462482, 3.3060188, -8.720747, -16.650524, 3.2713976, -8.770206, -16.577034, 3.2488902, -8.639545, -16.577034, 3.2488902, -8.639545, -16.650524, 3.2713976, -8.770206, -16.729061, 3.324795, -8.698833, -16.450489, 3.3760836, -8.6255665, -16.44341, 3.477376, -8.781784, -16.462482, 3.3060188, -8.720747, -16.450489, 3.3760836, -8.6255665, -16.462482, 3.3060188, -8.720747, -16.577034, 3.2488902, -8.639545, -16.417662, 3.5119941, -8.641058, -16.528055, 3.784827, -8.837328, -16.44341, 3.477376, -8.781784, -16.450489, 3.3760836, -8.6255665, -16.417662, 3.5119941, -8.641058, -16.44341, 3.477376, -8.781784, -16.417662, 3.5119941, -8.641058, -16.510483, 3.810473, -8.732229, -16.528055, 3.784827, -8.837328, -16.450489, 3.3760836, -8.6255665, -16.577034, 3.2488902, -8.639545, -16.517881, 3.340637, -8.534378, -16.417662, 3.5119941, -8.641058, -16.450489, 3.3760836, -8.6255665, -16.517881, 3.340637, -8.534378, -16.577034, 3.2488902, -8.639545, -16.729061, 3.324795, -8.698833, -16.705917, 3.3060188, -8.583826, -16.517881, 3.340637, -8.534378, -16.577034, 3.2488902, -8.639545, -16.705917, 3.3060188, -8.583826, -16.729061, 3.324795, -8.698833, -16.767986, 3.477376, -8.599226, -16.705917, 3.3060188, -8.583826, -16.517881, 3.340637, -8.534378, -16.705917, 3.3060188, -8.583826, -16.630806, 3.3760836, -8.524145, -16.510483, 3.810473, -8.732229, -16.417662, 3.5119941, -8.641058, -16.517267, 3.5235355, -8.533287, -16.417662, 3.5119941, -8.641058, -16.517881, 3.340637, -8.534378, -16.517267, 3.5235355, -8.533287, -16.517267, 3.5235355, -8.533287, -16.517881, 3.340637, -8.534378, -16.630806, 3.3760836, -8.524145, -16.510483, 3.810473, -8.732229, -16.517267, 3.5235355, -8.533287, -16.583447, 3.8194482, -8.6509495, -16.705917, 3.3060188, -8.583826, -16.767986, 3.477376, -8.599226, -16.661095, 3.5119941, -8.504137, -16.705917, 3.3060188, -8.583826, -16.661095, 3.5119941, -8.504137, -16.630806, 3.3760836, -8.524145, -16.517267, 3.5235355, -8.533287, -16.630806, 3.3760836, -8.524145, -16.661095, 3.5119941, -8.504137, -16.583447, 3.8194482, -8.6509495, -16.517267, 3.5235355, -8.533287, -16.661095, 3.5119941, -8.504137, -16.767986, 3.477376, -8.599226, -16.6908, 3.810473, -8.630808, -16.661095, 3.5119941, -8.504137, -16.583447, 3.8194482, -8.6509495, -16.661095, 3.5119941, -8.504137, -16.6908, 3.810473, -8.630808, -16.30509, 3.357844, -8.280506, -16.37834, 3.439557, -8.2862835, -16.369993, 3.333033, -8.271444, -16.30509, 3.357844, -8.280506, -16.293167, 3.436849, -8.302855, -16.37834, 3.439557, -8.2862835, -16.258024, 3.324914, -8.240377, -16.30509, 3.357844, -8.280506, -16.369993, 3.333033, -8.271444, -16.258024, 3.324914, -8.240377, -16.293167, 3.436849, -8.302855, -16.30509, 3.357844, -8.280506, -16.293167, 3.436849, -8.302855, -16.358952, 3.625382, -8.251812, -16.37834, 3.439557, -8.2862835, -16.37834, 3.439557, -8.2862835, -16.358952, 3.625382, -8.251812, -16.401714, 3.6232772, -8.20339, -16.293167, 3.436849, -8.302855, -16.29536, 3.6232772, -8.263208, -16.358952, 3.625382, -8.251812, -16.37834, 3.439557, -8.2862835, -16.401714, 3.6232772, -8.20339, -16.436735, 3.436849, -8.222103, -16.369993, 3.333033, -8.271444, -16.37834, 3.439557, -8.2862835, -16.436735, 3.436849, -8.222103, -16.436735, 3.436849, -8.222103, -16.401714, 3.6232772, -8.20339, -16.390556, 3.61726, -8.140003, -16.229052, 3.4287403, -8.244873, -16.29536, 3.6232772, -8.263208, -16.293167, 3.436849, -8.302855, -16.258024, 3.324914, -8.240377, -16.229052, 3.4287403, -8.244873, -16.293167, 3.436849, -8.302855, -16.229052, 3.4287403, -8.244873, -16.246988, 3.61726, -8.220755, -16.29536, 3.6232772, -8.263208, -16.436735, 3.436849, -8.222103, -16.390556, 3.61726, -8.140003, -16.420483, 3.4287403, -8.137201, -16.420483, 3.4287403, -8.137201, -16.390556, 3.61726, -8.140003, -16.342186, 3.6112542, -8.097551, -16.369993, 3.333033, -8.271444, -16.436735, 3.436849, -8.222103, -16.411444, 3.357844, -8.220686, -16.411444, 3.357844, -8.220686, -16.436735, 3.436849, -8.222103, -16.420483, 3.4287403, -8.137201, -16.258024, 3.324914, -8.240377, -16.369993, 3.333033, -8.271444, -16.331488, 3.290307, -8.202984, -16.331488, 3.290307, -8.202984, -16.369993, 3.333033, -8.271444, -16.411444, 3.357844, -8.220686, -16.245562, 3.3458211, -8.174669, -16.229052, 3.4287403, -8.244873, -16.258024, 3.324914, -8.240377, -16.245562, 3.3458211, -8.174669, -16.258024, 3.324914, -8.240377, -16.331488, 3.290307, -8.202984, -16.212805, 3.4206195, -8.159981, -16.246988, 3.61726, -8.220755, -16.229052, 3.4287403, -8.244873, -16.245562, 3.3458211, -8.174669, -16.212805, 3.4206195, -8.159981, -16.229052, 3.4287403, -8.244873, -16.212805, 3.4206195, -8.159981, -16.235832, 3.6112542, -8.1573715, -16.246988, 3.61726, -8.220755, -16.245562, 3.3458211, -8.174669, -16.331488, 3.290307, -8.202984, -16.28963, 3.3167934, -8.128569, -16.212805, 3.4206195, -8.159981, -16.245562, 3.3458211, -8.174669, -16.28963, 3.3167934, -8.128569, -16.331488, 3.290307, -8.202984, -16.411444, 3.357844, -8.220686, -16.401594, 3.324914, -8.159626, -16.28963, 3.3167934, -8.128569, -16.331488, 3.290307, -8.202984, -16.401594, 3.324914, -8.159626, -16.411444, 3.357844, -8.220686, -16.420483, 3.4287403, -8.137201, -16.401594, 3.324914, -8.159626, -16.28963, 3.3167934, -8.128569, -16.401594, 3.324914, -8.159626, -16.351915, 3.3458211, -8.114849, -16.235832, 3.6112542, -8.1573715, -16.212805, 3.4206195, -8.159981, -16.271194, 3.4179132, -8.095791, -16.212805, 3.4206195, -8.159981, -16.28963, 3.3167934, -8.128569, -16.271194, 3.4179132, -8.095791, -16.271194, 3.4179132, -8.095791, -16.28963, 3.3167934, -8.128569, -16.351915, 3.3458211, -8.114849, -16.235832, 3.6112542, -8.1573715, -16.271194, 3.4179132, -8.095791, -16.278595, 3.6091392, -8.108948, -16.401594, 3.324914, -8.159626, -16.420483, 3.4287403, -8.137201, -16.356375, 3.4206195, -8.079229, -16.401594, 3.324914, -8.159626, -16.356375, 3.4206195, -8.079229, -16.351915, 3.3458211, -8.114849, -16.271194, 3.4179132, -8.095791, -16.351915, 3.3458211, -8.114849, -16.356375, 3.4206195, -8.079229, -16.278595, 3.6091392, -8.108948, -16.271194, 3.4179132, -8.095791, -16.356375, 3.4206195, -8.079229, -16.420483, 3.4287403, -8.137201, -16.342186, 3.6112542, -8.097551, -16.356375, 3.4206195, -8.079229, -16.278595, 3.6091392, -8.108948, -16.356375, 3.4206195, -8.079229, -16.342186, 3.6112542, -8.097551, -16.283928, 3.6077967, -8.434147, -16.471827, 3.6957262, -8.4524975, -16.443388, 3.5811198, -8.4019375, -16.443388, 3.5811198, -8.4019375, -16.471827, 3.6957262, -8.4524975, -16.623535, 3.6880524, -8.295998, -16.283928, 3.6077967, -8.434147, -16.259296, 3.6880524, -8.500866, -16.471827, 3.6957262, -8.4524975, -16.443388, 3.5811198, -8.4019375, -16.623535, 3.6880524, -8.295998, -16.553728, 3.6077967, -8.282396, -16.138565, 3.6073067, -8.286215, -16.259296, 3.6880524, -8.500866, -16.283928, 3.6077967, -8.434147, -16.553728, 3.6077967, -8.282396, -16.623535, 3.6880524, -8.295998, -16.558157, 3.7145224, -8.0376835, -16.138565, 3.6073067, -8.286215, -16.072512, 3.7145224, -8.310837, -16.259296, 3.6880524, -8.500866, -16.553728, 3.6077967, -8.282396, -16.558157, 3.7145224, -8.0376835, -16.502804, 3.6073067, -8.0813465, -16.108883, 3.6335866, -8.122932, -16.072512, 3.7145224, -8.310837, -16.138565, 3.6073067, -8.286215, -16.502804, 3.6073067, -8.0813465, -16.558157, 3.7145224, -8.0376835, -16.398663, 3.697217, -7.896193, -16.108883, 3.6335866, -8.122932, -16.034424, 3.697217, -8.101061, -16.072512, 3.7145224, -8.310837, -16.502804, 3.6073067, -8.0813465, -16.398663, 3.697217, -7.896193, -16.378683, 3.6335866, -7.9711814, -16.034424, 3.697217, -8.101061, -16.108883, 3.6335866, -8.122932, -16.219902, 3.5909512, -8.004599, -16.183245, 3.687876, -7.939423, -16.378683, 3.6335866, -7.9711814, -16.398663, 3.697217, -7.896193, -16.034424, 3.697217, -8.101061, -16.219902, 3.5909512, -8.004599, -16.183245, 3.687876, -7.939423, -16.183245, 3.687876, -7.939423, -16.219902, 3.5909512, -8.004599, -16.378683, 3.6335866, -7.9711814, -16.471827, 3.6957262, -8.4524975, -16.524834, 3.7984083, -8.231026, -16.623535, 3.6880524, -8.295998, -16.623535, 3.6880524, -8.295998, -16.524834, 3.7984083, -8.231026, -16.48978, 3.8329887, -8.058193, -16.471827, 3.6957262, -8.4524975, -16.411404, 3.845576, -8.345074, -16.524834, 3.7984083, -8.231026, -16.411404, 3.845576, -8.345074, -16.48978, 3.8329887, -8.058193, -16.524834, 3.7984083, -8.231026, -16.623535, 3.6880524, -8.295998, -16.48978, 3.8329887, -8.058193, -16.558157, 3.7145224, -8.0376835, -16.259296, 3.6880524, -8.500866, -16.411404, 3.845576, -8.345074, -16.471827, 3.6957262, -8.4524975, -16.259296, 3.6880524, -8.500866, -16.255035, 3.7984083, -8.382776, -16.411404, 3.845576, -8.345074, -16.072512, 3.7145224, -8.310837, -16.255035, 3.7984083, -8.382776, -16.259296, 3.6880524, -8.500866, -16.411404, 3.845576, -8.345074, -16.305195, 3.881707, -8.156242, -16.48978, 3.8329887, -8.058193, -16.255035, 3.7984083, -8.382776, -16.305195, 3.881707, -8.156242, -16.411404, 3.845576, -8.345074, -16.558157, 3.7145224, -8.0376835, -16.48978, 3.8329887, -8.058193, -16.377708, 3.7797456, -7.9694505, -16.305195, 3.881707, -8.156242, -16.377708, 3.7797456, -7.9694505, -16.48978, 3.8329887, -8.058193, -16.558157, 3.7145224, -8.0376835, -16.377708, 3.7797456, -7.9694505, -16.398663, 3.697217, -7.896193, -16.072512, 3.7145224, -8.310837, -16.125544, 3.8329887, -8.263062, -16.255035, 3.7984083, -8.382776, -16.255035, 3.7984083, -8.382776, -16.125544, 3.8329887, -8.263062, -16.305195, 3.881707, -8.156242, -16.034424, 3.697217, -8.101061, -16.125544, 3.8329887, -8.263062, -16.072512, 3.7145224, -8.310837, -16.2153, 3.8121502, -7.9964156, -16.398663, 3.697217, -7.896193, -16.377708, 3.7797456, -7.9694505, -16.305195, 3.881707, -8.156242, -16.2153, 3.8121502, -7.9964156, -16.377708, 3.7797456, -7.9694505, -16.125544, 3.8329887, -8.263062, -16.2153, 3.8121502, -7.9964156, -16.305195, 3.881707, -8.156242, -16.2153, 3.8121502, -7.9964156, -16.183245, 3.687876, -7.939423, -16.398663, 3.697217, -7.896193, -16.034424, 3.697217, -8.101061, -16.10791, 3.7797456, -8.121201, -16.125544, 3.8329887, -8.263062, -16.125544, 3.8329887, -8.263062, -16.10791, 3.7797456, -8.121201, -16.2153, 3.8121502, -7.9964156, -16.10791, 3.7797456, -8.121201, -16.034424, 3.697217, -8.101061, -16.183245, 3.687876, -7.939423, -16.10791, 3.7797456, -8.121201, -16.183245, 3.687876, -7.939423, -16.2153, 3.8121502, -7.9964156, -7.086538, 9.0531025, 23.60468, -7.939819, 8.776951, 23.477636, -7.89693, 8.906714, 23.382841, -7.086538, 9.0531025, 23.60468, -7.1825204, 8.414909, 24.04463, -7.939819, 8.776951, 23.477636, -6.961786, 8.872082, 22.884235, -7.2900796, 8.958991, 23.246565, -7.2749104, 8.879946, 22.997782, -6.961786, 8.872082, 22.884235, -7.086538, 9.0531025, 23.60468, -7.2900796, 8.958991, 23.246565, -3.8860898, 6.9690914, 23.62356, -4.5446486, 7.7101564, 23.798996, -4.6237864, 7.655897, 23.206228, -3.8860898, 6.9690914, 23.62356, -3.8783517, 6.953966, 23.7307, -4.5446486, 7.7101564, 23.798996, -4.5446486, 7.7101564, 23.798996, -4.823362, 7.815732, 23.123234, -4.6237864, 7.655897, 23.206228, -4.5446486, 7.7101564, 23.798996, -4.899761, 7.976705, 23.460878, -4.823362, 7.815732, 23.123234, -4.899761, 7.976705, 23.460878, -4.5446486, 7.7101564, 23.798996, -5.2783494, 8.404766, 23.83276, -4.899761, 7.976705, 23.460878, -5.2783494, 8.404766, 23.83276, -5.3587403, 8.23149, 22.899555, -4.899761, 7.976705, 23.460878, -5.3587403, 8.23149, 22.899555, -5.0027404, 7.957439, 23.048418, -5.3587403, 8.23149, 22.899555, -5.5498886, 8.408549, 23.109316, -5.46656, 8.274823, 22.889973, -5.3587403, 8.23149, 22.899555, -5.2783494, 8.404766, 23.83276, -5.5498886, 8.408549, 23.109316, -6.134132, 8.667774, 22.83608, -6.545529, 8.861699, 23.146437, -6.347387, 8.715011, 22.84835, -6.134132, 8.667774, 22.83608, -6.2255015, 8.901371, 23.757235, -6.545529, 8.861699, 23.146437, -3.9216104, 6.895205, 23.810205, -4.5446486, 7.7101564, 23.798996, -3.8783517, 6.953966, 23.7307, -4.5446486, 7.7101564, 23.798996, -3.9216104, 6.895205, 23.810205, -4.32818, 7.252925, 23.874067, -4.32818, 7.252925, 23.874067, -3.9216104, 6.895205, 23.810205, -4.2883744, 7.0421944, 23.9518, -4.5446486, 7.7101564, 23.798996, -5.1261964, 7.754751, 24.078827, -5.2783494, 8.404766, 23.83276, -4.5446486, 7.7101564, 23.798996, -4.9056582, 7.542821, 24.083008, -5.1261964, 7.754751, 24.078827, -5.6396194, 7.681168, 24.366714, -6.2255015, 8.901371, 23.757235, -5.2783494, 8.404766, 23.83276, -6.2255015, 8.901371, 23.757235, -5.6396194, 7.681168, 24.366714, -6.046368, 7.8717294, 24.331638, -6.2255015, 8.901371, 23.757235, -6.046368, 7.8717294, 24.331638, -6.119331, 8.248653, 24.137571, -6.7146773, 8.708108, 23.8794, -7.086538, 9.0531025, 23.60468, -6.2255015, 8.901371, 23.757235, -7.086538, 9.0531025, 23.60468, -6.7146773, 8.708108, 23.8794, -7.0044417, 8.349307, 24.104824, -7.086538, 9.0531025, 23.60468, -7.0044417, 8.349307, 24.104824, -7.1825204, 8.414909, 24.04463, -6.2255015, 8.901371, 23.757235, -5.5498886, 8.408549, 23.109316, -5.2783494, 8.404766, 23.83276, -5.953386, 8.703066, 23.382, -5.5498886, 8.408549, 23.109316, -6.2255015, 8.901371, 23.757235, -5.5498886, 8.408549, 23.109316, -5.953386, 8.703066, 23.382, -5.8336997, 8.425533, 22.857294, -5.5498886, 8.408549, 23.109316, -5.8336997, 8.425533, 22.857294, -5.650786, 8.361013, 22.874142, -6.2255015, 8.901371, 23.757235, -5.9328055, 8.570662, 22.853338, -5.953386, 8.703066, 23.382, -6.2255015, 8.901371, 23.757235, -6.134132, 8.667774, 22.83608, -5.9328055, 8.570662, 22.853338, -6.119331, 8.248653, 24.137571, -6.4117084, 8.071957, 24.301369, -6.2255015, 8.901371, 23.757235, -6.119331, 8.248653, 24.137571, -6.237124, 7.9882894, 24.31635, -6.4117084, 8.071957, 24.301369, -7.086538, 9.0531025, 23.60468, -6.545529, 8.861699, 23.146437, -6.2255015, 8.901371, 23.757235, -6.545529, 8.861699, 23.146437, -7.086538, 9.0531025, 23.60468, -6.961786, 8.872082, 22.884235, -6.545529, 8.861699, 23.146437, -6.961786, 8.872082, 22.884235, -6.66091, 8.812481, 22.867199, -6.7146773, 8.708108, 23.8794, -6.4117084, 8.071957, 24.301369, -6.875569, 8.223388, 24.143358, -6.7146773, 8.708108, 23.8794, -6.2255015, 8.901371, 23.757235, -6.4117084, 8.071957, 24.301369, -7.89693, 8.906714, 23.382841, -7.2900796, 8.958991, 23.246565, -7.086538, 9.0531025, 23.60468, -7.2900796, 8.958991, 23.246565, -7.89693, 8.906714, 23.382841, -7.890431, 8.871303, 23.22045, -7.2900796, 8.958991, 23.246565, -7.890431, 8.871303, 23.22045, -7.484878, 8.8925495, 23.073898, -5.1261964, 7.754751, 24.078827, -5.6396194, 7.681168, 24.366714, -5.2783494, 8.404766, 23.83276, -5.1261964, 7.754751, 24.078827, -5.328693, 7.544381, 24.286783, -5.6396194, 7.681168, 24.366714, -4.9056582, 7.542821, 24.083008, -5.1280284, 7.4066963, 24.234045, -5.1261964, 7.754751, 24.078827, -4.9056582, 7.542821, 24.083008, -5.0212817, 7.379748, 24.207062, -5.1280284, 7.4066963, 24.234045, -4.9056582, 7.542821, 24.083008, -4.5446486, 7.7101564, 23.798996, -4.804087, 7.2903175, 24.15141, -4.32818, 7.252925, 23.874067, -4.804087, 7.2903175, 24.15141, -4.5446486, 7.7101564, 23.798996, -4.32818, 7.252925, 23.874067, -4.4737935, 7.144829, 24.02375, -4.804087, 7.2903175, 24.15141, -8.469263, 8.833129, 23.903402, -7.7564616, 8.8668, 23.1668, -7.7024093, 8.948188, 23.326448, -8.469263, 8.833129, 23.903402, -8.792166, 8.398045, 23.2395, -7.7564616, 8.8668, 23.1668, -7.939649, 8.319658, 24.36807, -8.019506, 8.693222, 23.9122, -7.814714, 8.502601, 24.04807, -7.939649, 8.319658, 24.36807, -8.469263, 8.833129, 23.903402, -8.019506, 8.693222, 23.9122, -10.742025, 5.3900876, 25.65433, -10.427696, 6.4519587, 25.310745, -9.86124, 6.2139215, 25.59392, -10.742025, 5.3900876, 25.65433, -10.839937, 5.4104342, 25.586737, -10.427696, 6.4519587, 25.310745, -10.427696, 6.4519587, 25.310745, -9.6490345, 6.4258494, 25.54795, -9.86124, 6.2139215, 25.59392, -10.427696, 6.4519587, 25.310745, -9.8865795, 6.741735, 25.338587, -9.6490345, 6.4258494, 25.54795, -9.8865795, 6.741735, 25.338587, -10.427696, 6.4519587, 25.310745, -9.939594, 7.468337, 24.97506, -9.8865795, 6.741735, 25.338587, -9.939594, 7.468337, 24.97506, -9.078133, 6.986556, 25.424217, -9.8865795, 6.741735, 25.338587, -9.078133, 6.986556, 25.424217, -9.458067, 6.6153927, 25.506601, -9.078133, 6.986556, 25.424217, -9.126747, 7.324047, 25.200722, -8.992649, 7.0699244, 25.362774, -9.078133, 6.986556, 25.424217, -9.939594, 7.468337, 24.97506, -9.126747, 7.324047, 25.200722, -8.48115, 7.732603, 24.99367, -8.460228, 8.226148, 24.537516, -8.340681, 7.875271, 24.831814, -8.48115, 7.732603, 24.99367, -9.20825, 8.35639, 24.448666, -8.460228, 8.226148, 24.537516, -10.878963, 5.39891, 25.472626, -10.427696, 6.4519587, 25.310745, -10.839937, 5.4104342, 25.586737, -10.427696, 6.4519587, 25.310745, -10.878963, 5.39891, 25.472626, -10.647385, 5.933868, 25.242573, -10.647385, 5.933868, 25.242573, -10.878963, 5.39891, 25.472626, -10.741941, 5.7328005, 25.138784, -10.427696, 6.4519587, 25.310745, -10.264824, 6.8368664, 24.649649, -9.939594, 7.468337, 24.97506, -10.427696, 6.4519587, 25.310745, -10.423819, 6.5334654, 24.740517, -10.264824, 6.8368664, 24.649649, -10.154926, 7.080727, 23.99825, -9.20825, 8.35639, 24.448666, -9.939594, 7.468337, 24.97506, -9.20825, 8.35639, 24.448666, -10.154926, 7.080727, 23.99825, -9.836353, 7.4272223, 23.768997, -9.20825, 8.35639, 24.448666, -9.836353, 7.4272223, 23.768997, -9.618614, 7.784581, 23.992184, -8.975733, 8.430757, 23.866367, -8.469263, 8.833129, 23.903402, -9.20825, 8.35639, 24.448666, -8.469263, 8.833129, 23.903402, -8.975733, 8.430757, 23.866367, -8.970964, 8.277404, 23.316208, -8.469263, 8.833129, 23.903402, -8.970964, 8.277404, 23.316208, -8.792166, 8.398045, 23.2395, -9.20825, 8.35639, 24.448666, -9.126747, 7.324047, 25.200722, -9.939594, 7.468337, 24.97506, -9.079264, 7.896097, 24.814846, -9.126747, 7.324047, 25.200722, -9.20825, 8.35639, 24.448666, -9.126747, 7.324047, 25.200722, -9.079264, 7.896097, 24.814846, -8.702198, 7.3584385, 25.153973, -9.126747, 7.324047, 25.200722, -8.702198, 7.3584385, 25.153973, -8.848383, 7.227356, 25.258986, -9.20825, 8.35639, 24.448666, -8.638075, 7.5534444, 25.106686, -9.079264, 7.896097, 24.814846, -9.20825, 8.35639, 24.448666, -8.48115, 7.732603, 24.99367, -8.638075, 7.5534444, 25.106686, -9.618614, 7.784581, 23.992184, -9.555359, 7.787944, 23.5661, -9.20825, 8.35639, 24.448666, -9.618614, 7.784581, 23.992184, -9.691614, 7.632971, 23.664253, -9.555359, 7.787944, 23.5661, -8.469263, 8.833129, 23.903402, -8.460228, 8.226148, 24.537516, -9.20825, 8.35639, 24.448666, -8.460228, 8.226148, 24.537516, -8.469263, 8.833129, 23.903402, -7.939649, 8.319658, 24.36807, -8.460228, 8.226148, 24.537516, -7.939649, 8.319658, 24.36807, -8.1381, 8.120274, 24.596785, -8.975733, 8.430757, 23.866367, -9.555359, 7.787944, 23.5661, -9.08618, 8.070576, 23.364449, -8.975733, 8.430757, 23.866367, -9.20825, 8.35639, 24.448666, -9.555359, 7.787944, 23.5661, -7.7024093, 8.948188, 23.326448, -8.019506, 8.693222, 23.9122, -8.469263, 8.833129, 23.903402, -8.019506, 8.693222, 23.9122, -7.7024093, 8.948188, 23.326448, -7.56801, 8.854256, 23.41686, -8.019506, 8.693222, 23.9122, -7.56801, 8.854256, 23.41686, -7.7330556, 8.652529, 23.835022, -10.264824, 6.8368664, 24.649649, -10.154926, 7.080727, 23.99825, -9.939594, 7.468337, 24.97506, -10.264824, 6.8368664, 24.649649, -10.304508, 6.7830296, 24.250587, -10.154926, 7.080727, 23.99825, -10.423819, 6.5334654, 24.740517, -10.394926, 6.530945, 24.408104, -10.264824, 6.8368664, 24.649649, -10.423819, 6.5334654, 24.740517, -10.448719, 6.45382, 24.497004, -10.394926, 6.530945, 24.408104, -10.423819, 6.5334654, 24.740517, -10.427696, 6.4519587, 25.310745, -10.554361, 6.258996, 24.674118, -10.647385, 5.933868, 25.242573, -10.554361, 6.258996, 24.674118, -10.427696, 6.4519587, 25.310745, -10.647385, 5.933868, 25.242573, -10.675802, 5.9358487, 24.97307, -10.554361, 6.258996, 24.674118, -8.476179, 9.272415, 23.238873, -7.6357913, 8.757383, 23.254288, -7.649511, 8.880784, 23.39443, -8.476179, 9.272415, 23.238873, -8.520136, 8.706246, 22.59189, -7.6357913, 8.757383, 23.254288, -8.663195, 8.882344, 23.984976, -8.29641, 9.024534, 23.588913, -8.32843, 8.858036, 23.843124, -8.663195, 8.882344, 23.984976, -8.476179, 9.272415, 23.238873, -8.29641, 9.024534, 23.588913, -12.152782, 7.7485704, 22.986092, -11.339409, 8.419413, 22.91118, -11.277941, 8.172669, 23.538235, -12.152782, 7.7485704, 22.986092, -12.161711, 7.7648354, 22.866318, -11.339409, 8.419413, 22.91118, -11.339409, 8.419413, 22.91118, -11.045532, 8.272723, 23.64941, -11.277941, 8.172669, 23.538235, -11.339409, 8.419413, 22.91118, -10.934807, 8.523428, 23.311256, -11.045532, 8.272723, 23.64941, -10.934807, 8.523428, 23.311256, -11.339409, 8.419413, 22.91118, -10.46002, 9.004967, 22.97543, -10.934807, 8.523428, 23.311256, -10.46002, 9.004967, 22.97543, -10.420095, 8.532848, 23.947876, -10.934807, 8.523428, 23.311256, -10.420095, 8.532848, 23.947876, -10.836201, 8.361673, 23.749374, -10.420095, 8.532848, 23.947876, -10.197205, 8.742559, 23.74126, -10.297283, 8.550915, 23.961588, -10.420095, 8.532848, 23.947876, -10.46002, 9.004967, 22.97543, -10.197205, 8.742559, 23.74126, -9.557905, 8.814163, 24.051355, -9.097107, 9.033176, 23.725506, -9.326338, 8.822926, 24.033926, -9.557905, 8.814163, 24.051355, -9.396812, 9.323492, 23.09922, -9.097107, 9.033176, 23.725506, -12.129438, 7.7163987, 22.76176, -11.339409, 8.419413, 22.91118, -12.161711, 7.7648354, 22.866318, -11.339409, 8.419413, 22.91118, -12.129438, 7.7163987, 22.76176, -11.6488285, 8.007675, 22.739779, -11.6488285, 8.007675, 22.739779, -12.129438, 7.7163987, 22.76176, -11.723551, 7.814352, 22.611177, -11.339409, 8.419413, 22.91118, -10.748026, 8.414791, 22.562681, -10.46002, 9.004967, 22.97543, -11.339409, 8.419413, 22.91118, -11.012053, 8.2395935, 22.52716, -10.748026, 8.414791, 22.562681, -10.238874, 8.305614, 22.187637, -9.396812, 9.323492, 23.09922, -10.46002, 9.004967, 22.97543, -9.396812, 9.323492, 23.09922, -10.238874, 8.305614, 22.187637, -9.780174, 8.406629, 22.240479, -9.396812, 9.323492, 23.09922, -9.780174, 8.406629, 22.240479, -9.642679, 8.753303, 22.532566, -8.943731, 9.065945, 22.87788, -8.476179, 9.272415, 23.238873, -9.396812, 9.323492, 23.09922, -8.476179, 9.272415, 23.238873, -8.943731, 9.065945, 22.87788, -8.7198925, 8.690582, 22.52244, -8.476179, 9.272415, 23.238873, -8.7198925, 8.690582, 22.52244, -8.520136, 8.706246, 22.59189, -9.396812, 9.323492, 23.09922, -10.197205, 8.742559, 23.74126, -10.46002, 9.004967, 22.97543, -9.730011, 9.053403, 23.47832, -10.197205, 8.742559, 23.74126, -9.396812, 9.323492, 23.09922, -10.197205, 8.742559, 23.74126, -9.730011, 9.053403, 23.47832, -9.879805, 8.6168785, 24.008417, -10.197205, 8.742559, 23.74126, -9.879805, 8.6168785, 24.008417, -10.089674, 8.597132, 23.985575, -9.396812, 9.323492, 23.09922, -9.784039, 8.756422, 24.024965, -9.730011, 9.053403, 23.47832, -9.396812, 9.323492, 23.09922, -9.557905, 8.814163, 24.051355, -9.784039, 8.756422, 24.024965, -9.642679, 8.753303, 22.532566, -9.374041, 8.548035, 22.28962, -9.396812, 9.323492, 23.09922, -9.642679, 8.753303, 22.532566, -9.570545, 8.498457, 22.26675, -9.374041, 8.548035, 22.28962, -8.476179, 9.272415, 23.238873, -9.097107, 9.033176, 23.725506, -9.396812, 9.323492, 23.09922, -9.097107, 9.033176, 23.725506, -8.476179, 9.272415, 23.238873, -8.663195, 8.882344, 23.984976, -9.097107, 9.033176, 23.725506, -8.663195, 8.882344, 23.984976, -8.990536, 8.872201, 24.00973, -8.943731, 9.065945, 22.87788, -9.374041, 8.548035, 22.28962, -8.84972, 8.556498, 22.469631, -8.943731, 9.065945, 22.87788, -9.396812, 9.323492, 23.09922, -9.374041, 8.548035, 22.28962, -7.649511, 8.880784, 23.39443, -8.29641, 9.024534, 23.588913, -8.476179, 9.272415, 23.238873, -8.29641, 9.024534, 23.588913, -7.649511, 8.880784, 23.39443, -7.667808, 8.801199, 23.562954, -8.29641, 9.024534, 23.588913, -7.667808, 8.801199, 23.562954, -8.106319, 8.869322, 23.749159, -10.748026, 8.414791, 22.562681, -10.238874, 8.305614, 22.187637, -10.46002, 9.004967, 22.97543, -10.748026, 8.414791, 22.562681, -10.583004, 8.204302, 22.268263, -10.238874, 8.305614, 22.187637, -11.012053, 8.2395935, 22.52716, -10.796698, 8.076697, 22.316998, -10.748026, 8.414791, 22.562681, -11.012053, 8.2395935, 22.52716, -10.918375, 8.067935, 22.346127, -10.796698, 8.076697, 22.316998, -11.012053, 8.2395935, 22.52716, -11.339409, 8.419413, 22.91118, -11.160143, 8.010557, 22.403072, -11.6488285, 8.007675, 22.739779, -11.160143, 8.010557, 22.403072, -11.339409, 8.419413, 22.91118, -11.6488285, 8.007675, 22.739779, -11.5230255, 7.8992805, 22.537394, -11.160143, 8.010557, 22.403072, -8.041105, 9.099136, 22.774368, -7.6169944, 8.80678, 23.443369, -7.7392535, 8.897409, 23.481745, -8.041105, 9.099136, 22.774368, -7.5259223, 8.667595, 22.517582, -7.6169944, 8.80678, 23.443369, -8.604433, 8.708588, 23.005474, -8.175547, 8.90155, 23.08837, -8.352665, 8.7482605, 23.189362, -8.604433, 8.708588, 23.005474, -8.041105, 9.099136, 22.774368, -8.175547, 8.90155, 23.08837, -9.354351, 7.2679915, 20.05077, -9.012537, 7.976405, 20.566238, -9.399052, 7.74749, 20.912569, -9.354351, 7.2679915, 20.05077, -9.275749, 7.2862973, 19.989244, -9.012537, 7.976405, 20.566238, -9.012537, 7.976405, 20.566238, -9.383741, 7.8629084, 21.12708, -9.399052, 7.74749, 20.912569, -9.012537, 7.976405, 20.566238, -9.122709, 8.10707, 21.038918, -9.383741, 7.8629084, 21.12708, -9.122709, 8.10707, 21.038918, -9.012537, 7.976405, 20.566238, -8.725763, 8.597912, 21.202042, -9.122709, 8.10707, 21.038918, -8.725763, 8.597912, 21.202042, -9.341703, 8.161026, 21.703133, -9.122709, 8.10707, 21.038918, -9.341703, 8.161026, 21.703133, -9.369871, 7.9649425, 21.320047, -9.341703, 8.161026, 21.703133, -9.114939, 8.379078, 21.764282, -9.299545, 8.194818, 21.797089, -9.341703, 8.161026, 21.703133, -8.725763, 8.597912, 21.202042, -9.114939, 8.379078, 21.764282, -9.049728, 8.516345, 22.375086, -8.632957, 8.781511, 22.557295, -8.934408, 8.559859, 22.53762, -9.049728, 8.516345, 22.375086, -8.364826, 9.012768, 22.024788, -8.632957, 8.781511, 22.557295, -9.180586, 7.258568, 19.97211, -9.012537, 7.976405, 20.566238, -9.275749, 7.2862973, 19.989244, -9.012537, 7.976405, 20.566238, -9.180586, 7.258568, 19.97211, -8.98185, 7.591978, 20.29632, -8.98185, 7.591978, 20.29632, -9.180586, 7.258568, 19.97211, -8.903774, 7.424042, 20.197647, -9.012537, 7.976405, 20.566238, -8.491125, 8.092604, 20.86079, -8.725763, 8.597912, 21.202042, -9.012537, 7.976405, 20.566238, -8.566002, 7.9071417, 20.66232, -8.491125, 8.092604, 20.86079, -7.9790645, 8.107249, 21.086702, -8.364826, 9.012768, 22.024788, -8.725763, 8.597912, 21.202042, -8.364826, 9.012768, 22.024788, -7.9790645, 8.107249, 21.086702, -7.822355, 8.259277, 21.4402, -8.364826, 9.012768, 22.024788, -7.822355, 8.259277, 21.4402, -8.002892, 8.544012, 21.647385, -7.964889, 8.880724, 22.295406, -8.041105, 9.099136, 22.774368, -8.364826, 9.012768, 22.024788, -8.041105, 9.099136, 22.774368, -7.964889, 8.880724, 22.295406, -7.5640388, 8.629003, 22.34164, -8.041105, 9.099136, 22.774368, -7.5640388, 8.629003, 22.34164, -7.5259223, 8.667595, 22.517582, -8.364826, 9.012768, 22.024788, -9.114939, 8.379078, 21.764282, -8.725763, 8.597912, 21.202042, -8.749628, 8.718971, 21.97248, -9.114939, 8.379078, 21.764282, -8.364826, 9.012768, 22.024788, -9.114939, 8.379078, 21.764282, -8.749628, 8.718971, 21.97248, -9.156193, 8.312938, 22.116726, -9.114939, 8.379078, 21.764282, -9.156193, 8.312938, 22.116726, -9.228645, 8.264259, 21.957209, -8.364826, 9.012768, 22.024788, -9.126802, 8.436518, 22.200216, -8.749628, 8.718971, 21.97248, -8.364826, 9.012768, 22.024788, -9.049728, 8.516345, 22.375086, -9.126802, 8.436518, 22.200216, -8.002892, 8.544012, 21.647385, -7.684996, 8.432556, 21.75747, -8.364826, 9.012768, 22.024788, -8.002892, 8.544012, 21.647385, -7.7519794, 8.363534, 21.605556, -7.684996, 8.432556, 21.75747, -8.041105, 9.099136, 22.774368, -8.632957, 8.781511, 22.557295, -8.364826, 9.012768, 22.024788, -8.632957, 8.781511, 22.557295, -8.041105, 9.099136, 22.774368, -8.604433, 8.708588, 23.005474, -8.632957, 8.781511, 22.557295, -8.604433, 8.708588, 23.005474, -8.767645, 8.649889, 22.776098, -7.964889, 8.880724, 22.295406, -7.684996, 8.432556, 21.75747, -7.5840287, 8.511244, 22.2162, -7.964889, 8.880724, 22.295406, -8.364826, 9.012768, 22.024788, -7.684996, 8.432556, 21.75747, -7.7392535, 8.897409, 23.481745, -8.175547, 8.90155, 23.08837, -8.041105, 9.099136, 22.774368, -8.175547, 8.90155, 23.08837, -7.7392535, 8.897409, 23.481745, -7.856048, 8.8193245, 23.551592, -8.175547, 8.90155, 23.08837, -7.856048, 8.8193245, 23.551592, -8.1856785, 8.794295, 23.313171, -8.491125, 8.092604, 20.86079, -7.9790645, 8.107249, 21.086702, -8.725763, 8.597912, 21.202042, -8.491125, 8.092604, 20.86079, -8.181248, 7.964281, 20.873774, -7.9790645, 8.107249, 21.086702, -8.566002, 7.9071417, 20.66232, -8.305813, 7.8208942, 20.736774, -8.491125, 8.092604, 20.86079, -8.566002, 7.9071417, 20.66232, -8.377768, 7.7919035, 20.663399, -8.305813, 7.8208942, 20.736774, -8.566002, 7.9071417, 20.66232, -9.012537, 7.976405, 20.566238, -8.519827, 7.7004347, 20.51477, -8.98185, 7.591978, 20.29632, -8.519827, 7.7004347, 20.51477, -9.012537, 7.976405, 20.566238, -8.98185, 7.591978, 20.29632, -8.76738, 7.536939, 20.311623, -8.519827, 7.7004347, 20.51477, -7.2060475, 9.208552, 22.897741, -7.80905, 8.817884, 23.60042, -7.899529, 8.892069, 23.45427, -7.2060475, 9.208552, 22.897741, -6.7061973, 8.855337, 23.494766, -7.80905, 8.817884, 23.60042, -7.4934087, 8.594069, 22.34334, -7.539685, 8.893689, 22.84897, -7.65863, 8.6545105, 22.680805, -7.4934087, 8.594069, 22.34334, -7.2060475, 9.208552, 22.897741, -7.539685, 8.893689, 22.84897, -4.068832, 7.336172, 20.780975, -4.714312, 8.10803, 21.257643, -5.132072, 7.6965923, 20.921396, -4.068832, 7.336172, 20.780975, -3.9881473, 7.385569, 20.85462, -4.714312, 8.10803, 21.257643, -4.714312, 8.10803, 21.257643, -5.3909154, 7.790823, 20.987743, -5.132072, 7.6965923, 20.921396, -4.714312, 8.10803, 21.257643, -5.2879725, 8.145002, 21.248096, -5.3909154, 7.790823, 20.987743, -5.2879725, 8.145002, 21.248096, -4.714312, 8.10803, 21.257643, -5.489513, 8.767287, 21.711481, -5.2879725, 8.145002, 21.248096, -5.489513, 8.767287, 21.711481, -6.085754, 8.035226, 21.16589, -5.2879725, 8.145002, 21.248096, -6.085754, 8.035226, 21.16589, -5.623652, 7.8744917, 21.047535, -6.085754, 8.035226, 21.16589, -6.1478257, 8.328001, 21.433554, -6.1905837, 8.063794, 21.235548, -6.085754, 8.035226, 21.16589, -5.489513, 8.767287, 21.711481, -6.1478257, 8.328001, 21.433554, -6.841514, 8.391562, 21.667154, -7.0118194, 8.758585, 22.183632, -7.009152, 8.4349575, 21.841381, -6.841514, 8.391562, 21.667154, -6.416242, 9.177944, 22.323563, -7.0118194, 8.758585, 22.183632, -3.9419742, 7.374406, 20.966194, -4.714312, 8.10803, 21.257643, -3.9881473, 7.385569, 20.85462, -4.714312, 8.10803, 21.257643, -3.9419742, 7.374406, 20.966194, -4.32668, 7.7234807, 21.258965, -4.32668, 7.7234807, 21.258965, -3.9419742, 7.374406, 20.966194, -4.1686845, 7.564969, 21.337112, -4.714312, 8.10803, 21.257643, -4.9478, 8.286828, 21.951738, -5.489513, 8.767287, 21.711481, -4.714312, 8.10803, 21.257643, -4.704924, 8.094104, 21.826122, -4.9478, 8.286828, 21.951738, -5.0908885, 8.360533, 22.620289, -6.416242, 9.177944, 22.323563, -5.489513, 8.767287, 21.711481, -6.416242, 9.177944, 22.323563, -5.0908885, 8.360533, 22.620289, -5.486716, 8.50116, 22.883144, -6.416242, 9.177944, 22.323563, -5.486716, 8.50116, 22.883144, -5.8117423, 8.768128, 22.703707, -6.5980697, 9.059402, 22.89238, -7.2060475, 9.208552, 22.897741, -6.416242, 9.177944, 22.323563, -7.2060475, 9.208552, 22.897741, -6.5980697, 9.059402, 22.89238, -6.507363, 8.83523, 23.408497, -7.2060475, 9.208552, 22.897741, -6.507363, 8.83523, 23.408497, -6.7061973, 8.855337, 23.494766, -6.416242, 9.177944, 22.323563, -6.1478257, 8.328001, 21.433554, -5.489513, 8.767287, 21.711481, -6.3719687, 8.766026, 21.89067, -6.1478257, 8.328001, 21.433554, -6.416242, 9.177944, 22.323563, -6.1478257, 8.328001, 21.433554, -6.3719687, 8.766026, 21.89067, -6.5476384, 8.165707, 21.472878, -6.1478257, 8.328001, 21.433554, -6.5476384, 8.165707, 21.472878, -6.369827, 8.1280155, 21.354618, -6.416242, 9.177944, 22.323563, -6.645247, 8.314198, 21.536797, -6.3719687, 8.766026, 21.89067, -6.416242, 9.177944, 22.323563, -6.841514, 8.391562, 21.667154, -6.645247, 8.314198, 21.536797, -5.8117423, 8.768128, 22.703707, -5.8438954, 8.677977, 23.119993, -6.416242, 9.177944, 22.323563, -5.8117423, 8.768128, 22.703707, -5.6736007, 8.611357, 23.006952, -5.8438954, 8.677977, 23.119993, -7.2060475, 9.208552, 22.897741, -7.0118194, 8.758585, 22.183632, -6.416242, 9.177944, 22.323563, -7.0118194, 8.758585, 22.183632, -7.2060475, 9.208552, 22.897741, -7.4934087, 8.594069, 22.34334, -7.0118194, 8.758585, 22.183632, -7.4934087, 8.594069, 22.34334, -7.256623, 8.534951, 22.097307, -6.5980697, 9.059402, 22.89238, -5.8438954, 8.677977, 23.119993, -6.361113, 8.698025, 23.343914, -6.5980697, 9.059402, 22.89238, -6.416242, 9.177944, 22.323563, -5.8438954, 8.677977, 23.119993, -7.899529, 8.892069, 23.45427, -7.539685, 8.893689, 22.84897, -7.2060475, 9.208552, 22.897741, -7.539685, 8.893689, 22.84897, -7.899529, 8.892069, 23.45427, -7.983706, 8.764886, 23.345701, -7.539685, 8.893689, 22.84897, -7.983706, 8.764886, 23.345701, -7.7710595, 8.722573, 22.907578, -4.9478, 8.286828, 21.951738, -5.0908885, 8.360533, 22.620289, -5.489513, 8.767287, 21.711481, -4.9478, 8.286828, 21.951738, -4.867348, 8.1940365, 22.336489, -5.0908885, 8.360533, 22.620289, -4.704924, 8.094104, 21.826122, -4.7211885, 8.02508, 22.154125, -4.9478, 8.286828, 21.951738, -4.704924, 8.094104, 21.826122, -4.6452146, 7.9936295, 22.056368, -4.7211885, 8.02508, 22.154125, -4.704924, 8.094104, 21.826122, -4.714312, 8.10803, 21.257643, -4.489761, 7.8906374, 21.858315, -4.32668, 7.7234807, 21.258965, -4.489761, 7.8906374, 21.858315, -4.714312, 8.10803, 21.257643, -4.32668, 7.7234807, 21.258965, -4.284569, 7.6962314, 21.524137, -4.489761, 7.8906374, 21.858315, -7.4770913, 9.070688, 24.137522, -7.9792457, 8.86656, 23.280897, -7.798713, 8.910673, 23.258356, -7.4770913, 9.070688, 24.137522, -8.267395, 8.83601, 24.344084, -7.9792457, 8.86656, 23.280897, -6.969793, 8.361493, 23.999329, -7.3750834, 8.766386, 23.809656, -7.2131476, 8.500739, 23.731937, -6.969793, 8.361493, 23.999329, -7.4770913, 9.070688, 24.137522, -7.3750834, 8.766386, 23.809656, -6.8215938, 6.6485844, 27.584244, -6.934275, 7.533578, 26.905539, -6.54954, 7.0755653, 26.591766, -6.8215938, 6.6485844, 27.584244, -6.908404, 6.709503, 27.638208, -6.934275, 7.533578, 26.905539, -6.934275, 7.533578, 26.905539, -6.512714, 7.1929045, 26.339361, -6.54954, 7.0755653, 26.591766, -6.934275, 7.533578, 26.905539, -6.735351, 7.5916777, 26.38232, -6.512714, 7.1929045, 26.339361, -6.735351, 7.5916777, 26.38232, -6.934275, 7.533578, 26.905539, -7.004352, 8.309274, 26.101746, -6.735351, 7.5916777, 26.38232, -7.004352, 8.309274, 26.101746, -6.413199, 7.4994855, 25.660645, -6.735351, 7.5916777, 26.38232, -6.413199, 7.4994855, 25.660645, -6.4796042, 7.2974, 26.112244, -6.413199, 7.4994855, 25.660645, -6.6031613, 7.8468814, 25.541204, -6.4362464, 7.5467825, 25.541874, -6.413199, 7.4994855, 25.660645, -7.004352, 8.309274, 26.101746, -6.6031613, 7.8468814, 25.541204, -6.5875335, 7.9890084, 24.822481, -6.9586644, 8.467667, 24.52183, -6.6856866, 8.076338, 24.609621, -6.5875335, 7.9890084, 24.822481, -7.210817, 8.882824, 25.071764, -6.9586644, 8.467667, 24.52183, -7.030128, 6.721147, 27.639519, -6.934275, 7.533578, 26.905539, -6.908404, 6.709503, 27.638208, -6.934275, 7.533578, 26.905539, -7.030128, 6.721147, 27.639519, -7.122934, 7.142128, 27.21883, -7.122934, 7.142128, 27.21883, -7.030128, 6.721147, 27.639519, -7.2680945, 6.9975395, 27.31846, -6.934275, 7.533578, 26.905539, -7.48521, 7.8700495, 26.46049, -7.004352, 8.309274, 26.101746, -6.934275, 7.533578, 26.905539, -7.476253, 7.6441336, 26.707956, -7.48521, 7.8700495, 26.46049, -8.052195, 8.094164, 26.095999, -7.210817, 8.882824, 25.071764, -7.004352, 8.309274, 26.101746, -7.210817, 8.882824, 25.071764, -8.052195, 8.094164, 26.095999, -8.141077, 8.305193, 25.651417, -7.210817, 8.882824, 25.071764, -8.141077, 8.305193, 25.651417, -7.8344674, 8.5407715, 25.446497, -7.7096314, 8.899331, 24.684853, -7.4770913, 9.070688, 24.137522, -7.210817, 8.882824, 25.071764, -7.4770913, 9.070688, 24.137522, -7.7096314, 8.899331, 24.684853, -8.258791, 8.789434, 24.556768, -7.4770913, 9.070688, 24.137522, -8.258791, 8.789434, 24.556768, -8.267395, 8.83601, 24.344084, -7.210817, 8.882824, 25.071764, -6.6031613, 7.8468814, 25.541204, -7.004352, 8.309274, 26.101746, -6.8924146, 8.3843, 25.220247, -6.6031613, 7.8468814, 25.541204, -7.210817, 8.882824, 25.071764, -6.6031613, 7.8468814, 25.541204, -6.8924146, 8.3843, 25.220247, -6.5150523, 7.712496, 25.137945, -6.6031613, 7.8468814, 25.541204, -6.5150523, 7.712496, 25.137945, -6.4764667, 7.6428747, 25.340647, -7.210817, 8.882824, 25.071764, -6.543115, 7.8768907, 25.041906, -6.8924146, 8.3843, 25.220247, -7.210817, 8.882824, 25.071764, -6.5875335, 7.9890084, 24.822481, -6.543115, 7.8768907, 25.041906, -7.8344674, 8.5407715, 25.446497, -8.223946, 8.544853, 25.256237, -7.210817, 8.882824, 25.071764, -7.8344674, 8.5407715, 25.446497, -8.185364, 8.448042, 25.446867, -8.223946, 8.544853, 25.256237, -7.4770913, 9.070688, 24.137522, -6.9586644, 8.467667, 24.52183, -7.210817, 8.882824, 25.071764, -6.9586644, 8.467667, 24.52183, -7.4770913, 9.070688, 24.137522, -6.969793, 8.361493, 23.999329, -6.9586644, 8.467667, 24.52183, -6.969793, 8.361493, 23.999329, -6.8314404, 8.240372, 24.300121, -7.7096314, 8.899331, 24.684853, -8.223946, 8.544853, 25.256237, -8.244255, 8.633982, 24.69929, -7.7096314, 8.899331, 24.684853, -7.210817, 8.882824, 25.071764, -8.223946, 8.544853, 25.256237, -7.798713, 8.910673, 23.258356, -7.3750834, 8.766386, 23.809656, -7.4770913, 9.070688, 24.137522, -7.3750834, 8.766386, 23.809656, -7.798713, 8.910673, 23.258356, -7.6923747, 8.766327, 23.204716, -7.3750834, 8.766386, 23.809656, -7.6923747, 8.766327, 23.204716, -7.3779397, 8.62144, 23.55421, -7.48521, 7.8700495, 26.46049, -8.052195, 8.094164, 26.095999, -7.004352, 8.309274, 26.101746, -7.48521, 7.8700495, 26.46049, -7.886131, 7.860207, 26.385628, -8.052195, 8.094164, 26.095999, -7.476253, 7.6441336, 26.707956, -7.776336, 7.6484556, 26.566654, -7.48521, 7.8700495, 26.46049, -7.476253, 7.6441336, 26.707956, -7.720366, 7.5934772, 26.668566, -7.776336, 7.6484556, 26.566654, -7.476253, 7.6441336, 26.707956, -6.934275, 7.533578, 26.905539, -7.604976, 7.443248, 26.871801, -7.122934, 7.142128, 27.21883, -7.604976, 7.443248, 26.871801, -6.934275, 7.533578, 26.905539, -7.122934, 7.142128, 27.21883, -7.3896413, 7.171777, 27.159313, -7.604976, 7.443248, 26.871801, -6.0571437, 2.278272, 22.798777, -6.422816, 4.561778, 23.067327, -6.255482, 4.6069903, 22.866049, -6.0571437, 2.278272, 22.798777, -6.2640643, 2.2363775, 23.040073, -6.422816, 4.561778, 23.067327, -6.229696, -0.29725552, 22.954578, -6.2640643, 2.2363775, 23.040073, -6.0571437, 2.278272, 22.798777, -6.229696, -0.29725552, 22.954578, -6.4595838, -0.294353, 23.230642, -6.2640643, 2.2363775, 23.040073, -6.8122897, 2.1787193, 22.89551, -6.9267097, 4.515596, 22.675344, -6.886102, 4.499142, 22.937313, -6.8122897, 2.1787193, 22.89551, -6.8565516, 2.1943433, 22.589626, -6.9267097, 4.515596, 22.675344, -6.34096, 2.278272, 22.29763, -6.2974906, 4.6231685, 22.603962, -6.5006537, 4.606991, 22.43314, -6.34096, 2.278272, 22.29763, -6.0964046, 2.2940338, 22.49008, -6.2974906, 4.6231685, 22.603962, -6.886102, 4.499142, 22.937313, -7.330835, 6.6817102, 22.999025, -7.3017764, 6.6674695, 23.172724, -6.886102, 4.499142, 22.937313, -6.9267097, 4.515596, 22.675344, -7.330835, 6.6817102, 22.999025, -6.255482, 4.6069903, 22.866049, -6.9959083, 6.7194586, 23.261988, -6.883434, 6.7554083, 23.12764, -6.255482, 4.6069903, 22.866049, -6.422816, 4.561778, 23.067327, -6.9959083, 6.7194586, 23.261988, -6.883434, 6.7554083, 23.12764, -6.9959083, 6.7194586, 23.261988, -7.3726697, 7.8587894, 23.408417, -6.883434, 6.7554083, 23.12764, -7.3726697, 7.8587894, 23.408417, -7.2935386, 7.8998566, 23.310549, -6.2640643, 2.2363775, 23.040073, -6.6823287, 4.515596, 23.106857, -6.422816, 4.561778, 23.067327, -6.2640643, 2.2363775, 23.040073, -6.5727344, 2.1943433, 23.090773, -6.6823287, 4.515596, 23.106857, -6.422816, 4.561778, 23.067327, -6.6823287, 4.515596, 23.106857, -7.1677566, 6.6817102, 23.28698, -6.422816, 4.561778, 23.067327, -7.1677566, 6.6817102, 23.28698, -6.9959083, 6.7194586, 23.261988, -6.8565516, 2.1943433, 22.589626, -6.759332, 4.561778, 22.473125, -6.9267097, 4.515596, 22.675344, -6.8565516, 2.1943433, 22.589626, -6.6543117, 2.2363775, 22.350998, -6.759332, 4.561778, 22.473125, -6.9267097, 4.515596, 22.675344, -6.759332, 4.561778, 22.473125, -7.2210193, 6.7194586, 22.864502, -6.9267097, 4.515596, 22.675344, -7.2210193, 6.7194586, 22.864502, -7.330835, 6.6817102, 22.999025, -6.5006537, 4.606991, 22.43314, -6.9114146, 6.7684054, 22.951649, -7.0479617, 6.7554083, 22.837128, -6.5006537, 4.606991, 22.43314, -6.2974906, 4.6231685, 22.603962, -6.9114146, 6.7684054, 22.951649, -6.0964046, 2.2940338, 22.49008, -6.255482, 4.6069903, 22.866049, -6.2974906, 4.6231685, 22.603962, -6.0964046, 2.2940338, 22.49008, -6.0571437, 2.278272, 22.798777, -6.255482, 4.6069903, 22.866049, -6.2974906, 4.6231685, 22.603962, -6.883434, 6.7554083, 23.12764, -6.9114146, 6.7684054, 22.951649, -6.2974906, 4.6231685, 22.603962, -6.255482, 4.6069903, 22.866049, -6.883434, 6.7554083, 23.12764, -6.6543117, 2.2363775, 22.350998, -6.5006537, 4.606991, 22.43314, -6.759332, 4.561778, 22.473125, -6.6543117, 2.2363775, 22.350998, -6.34096, 2.278272, 22.29763, -6.5006537, 4.606991, 22.43314, -6.9183087, -0.294353, 22.420652, -6.34096, 2.278272, 22.29763, -6.6543117, 2.2363775, 22.350998, -6.9183087, -0.294353, 22.420652, -6.563316, -0.29725552, 22.365494, -6.34096, 2.278272, 22.29763, -6.759332, 4.561778, 22.473125, -7.0479617, 6.7554083, 22.837128, -7.2210193, 6.7194586, 22.864502, -6.759332, 4.561778, 22.473125, -6.5006537, 4.606991, 22.43314, -7.0479617, 6.7554083, 22.837128, -7.2210193, 6.7194586, 22.864502, -7.0479617, 6.7554083, 22.837128, -7.4157386, 7.8998566, 23.094776, -7.2210193, 6.7194586, 22.864502, -7.4157386, 7.8998566, 23.094776, -7.5403686, 7.8587894, 23.112305, -7.596041, 7.8030686, 23.339376, -7.8122087, 8.670566, 23.565685, -7.4973254, 7.8185544, 23.425323, -7.596041, 7.8030686, 23.339376, -7.884003, 8.657431, 23.502459, -7.8122087, 8.670566, 23.565685, -7.3726697, 7.8587894, 23.408417, -7.6698213, 8.747442, 23.485046, -7.2935386, 7.8998566, 23.310549, -7.3726697, 7.8587894, 23.408417, -7.724811, 8.710249, 23.555187, -7.6698213, 8.747442, 23.485046, -7.4973254, 7.8185544, 23.425323, -7.724811, 8.710249, 23.555187, -7.3726697, 7.8587894, 23.408417, -7.4973254, 7.8185544, 23.425323, -7.8122087, 8.670566, 23.565685, -7.724811, 8.710249, 23.555187, -7.3161235, 7.9149265, 23.18085, -7.7589135, 8.747442, 23.327732, -7.4157386, 7.8998566, 23.094776, -7.3161235, 7.9149265, 23.18085, -7.6881547, 8.763065, 23.391544, -7.7589135, 8.747442, 23.327732, -7.2935386, 7.8998566, 23.310549, -7.6881547, 8.763065, 23.391544, -7.3161235, 7.9149265, 23.18085, -7.2935386, 7.8998566, 23.310549, -7.6698213, 8.747442, 23.485046, -7.6881547, 8.763065, 23.391544, -7.5403686, 7.8587894, 23.112305, -7.901301, 8.670566, 23.408371, -7.61898, 7.8185544, 23.210514, -7.5403686, 7.8587894, 23.112305, -7.8473487, 8.710249, 23.338818, -7.901301, 8.670566, 23.408371, -7.4157386, 7.8998566, 23.094776, -7.8473487, 8.710249, 23.338818, -7.5403686, 7.8587894, 23.112305, -7.4157386, 7.8998566, 23.094776, -7.7589135, 8.747442, 23.327732, -7.8473487, 8.710249, 23.338818, -6.8164535, -0.2914486, 23.28688, -6.8122897, 2.1787193, 22.89551, -6.5727344, 2.1943433, 23.090773, -6.8164535, -0.2914486, 23.28688, -7.092834, -0.29034328, 23.054392, -6.8122897, 2.1787193, 22.89551, -7.61898, 7.8185544, 23.210514, -7.884003, 8.657431, 23.502459, -7.596041, 7.8030686, 23.339376, -7.61898, 7.8185544, 23.210514, -7.901301, 8.670566, 23.408371, -7.884003, 8.657431, 23.502459, -6.5727344, 2.1943433, 23.090773, -6.886102, 4.499142, 22.937313, -6.6823287, 4.515596, 23.106857, -6.5727344, 2.1943433, 23.090773, -6.8122897, 2.1787193, 22.89551, -6.886102, 4.499142, 22.937313, -7.092834, -0.29034328, 23.054392, -6.8565516, 2.1943433, 22.589626, -6.8122897, 2.1787193, 22.89551, -7.092834, -0.29034328, 23.054392, -7.1500726, -0.2914486, 22.697796, -6.8565516, 2.1943433, 22.589626, -6.563316, -0.29725552, 22.365494, -6.0964046, 2.2940338, 22.49008, -6.34096, 2.278272, 22.29763, -6.563316, -0.29725552, 22.365494, -6.288299, -0.2983632, 22.598755, -6.0964046, 2.2940338, 22.49008, -6.4595838, -0.294353, 23.230642, -6.5727344, 2.1943433, 23.090773, -6.2640643, 2.2363775, 23.040073, -6.4595838, -0.294353, 23.230642, -6.8164535, -0.2914486, 23.28688, -6.5727344, 2.1943433, 23.090773, -7.1500726, -0.2914486, 22.697796, -6.6543117, 2.2363775, 22.350998, -6.8565516, 2.1943433, 22.589626, -7.1500726, -0.2914486, 22.697796, -6.9183087, -0.294353, 22.420652, -6.6543117, 2.2363775, 22.350998, -6.288299, -0.2983632, 22.598755, -6.0571437, 2.278272, 22.798777, -6.0964046, 2.2940338, 22.49008, -6.288299, -0.2983632, 22.598755, -6.229696, -0.29725552, 22.954578, -6.0571437, 2.278272, 22.798777, -6.6823287, 4.515596, 23.106857, -7.3017764, 6.6674695, 23.172724, -7.1677566, 6.6817102, 23.28698, -6.6823287, 4.515596, 23.106857, -6.886102, 4.499142, 22.937313, -7.3017764, 6.6674695, 23.172724, -7.1677566, 6.6817102, 23.28698, -7.596041, 7.8030686, 23.339376, -7.4973254, 7.8185544, 23.425323, -7.1677566, 6.6817102, 23.28698, -7.3017764, 6.6674695, 23.172724, -7.596041, 7.8030686, 23.339376, -7.3017764, 6.6674695, 23.172724, -7.61898, 7.8185544, 23.210514, -7.596041, 7.8030686, 23.339376, -7.3017764, 6.6674695, 23.172724, -7.330835, 6.6817102, 22.999025, -7.61898, 7.8185544, 23.210514, -6.9959083, 6.7194586, 23.261988, -7.4973254, 7.8185544, 23.425323, -7.3726697, 7.8587894, 23.408417, -6.9959083, 6.7194586, 23.261988, -7.1677566, 6.6817102, 23.28698, -7.4973254, 7.8185544, 23.425323, -7.0479617, 6.7554083, 22.837128, -7.3161235, 7.9149265, 23.18085, -7.4157386, 7.8998566, 23.094776, -7.0479617, 6.7554083, 22.837128, -6.9114146, 6.7684054, 22.951649, -7.3161235, 7.9149265, 23.18085, -6.9114146, 6.7684054, 22.951649, -7.2935386, 7.8998566, 23.310549, -7.3161235, 7.9149265, 23.18085, -6.9114146, 6.7684054, 22.951649, -6.883434, 6.7554083, 23.12764, -7.2935386, 7.8998566, 23.310549, -7.330835, 6.6817102, 22.999025, -7.5403686, 7.8587894, 23.112305, -7.61898, 7.8185544, 23.210514, -7.330835, 6.6817102, 22.999025, -7.2210193, 6.7194586, 22.864502, -7.5403686, 7.8587894, 23.112305, -5.520864, 5.50994, 24.639458, -6.0238147, 5.3727474, 24.921116, -6.027886, 5.4678555, 24.868423, -5.520864, 5.50994, 24.639458, -5.4553275, 5.043026, 24.83633, -6.0238147, 5.3727474, 24.921116, -5.780359, 5.49264, 24.220936, -5.780785, 5.505308, 24.545547, -5.885944, 5.489472, 24.411573, -5.780359, 5.49264, 24.220936, -5.520864, 5.50994, 24.639458, -5.780785, 5.505308, 24.545547, -4.172077, 4.097315, 23.081501, -4.3400836, 4.5620866, 23.516861, -4.6323843, 4.612814, 23.259392, -4.172077, 4.097315, 23.081501, -4.1252418, 4.0723934, 23.12863, -4.3400836, 4.5620866, 23.516861, -4.3400836, 4.5620866, 23.516861, -4.7473187, 4.731488, 23.319653, -4.6323843, 4.612814, 23.259392, -4.3400836, 4.5620866, 23.516861, -4.6260543, 4.7886457, 23.530365, -4.7473187, 4.731488, 23.319653, -4.6260543, 4.7886457, 23.530365, -4.3400836, 4.5620866, 23.516861, -4.6092978, 5.019003, 23.91192, -4.6260543, 4.7886457, 23.530365, -4.6092978, 5.019003, 23.91192, -5.05752, 5.0416813, 23.479633, -4.6260543, 4.7886457, 23.530365, -5.05752, 5.0416813, 23.479633, -4.8509307, 4.836937, 23.373531, -5.05752, 5.0416813, 23.479633, -5.044219, 5.1299534, 23.679413, -5.109679, 5.073374, 23.52538, -5.05752, 5.0416813, 23.479633, -4.6092978, 5.019003, 23.91192, -5.044219, 5.1299534, 23.679413, -5.416506, 5.348407, 23.822437, -5.4675636, 5.4387703, 24.16735, -5.5109153, 5.382139, 23.924562, -5.416506, 5.348407, 23.822437, -5.0510015, 5.370203, 24.32837, -5.4675636, 5.4387703, 24.16735, -4.119844, 4.0247717, 23.180462, -4.3400836, 4.5620866, 23.516861, -4.1252418, 4.0723934, 23.12863, -4.3400836, 4.5620866, 23.516861, -4.119844, 4.0247717, 23.180462, -4.253202, 4.254257, 23.418776, -4.253202, 4.254257, 23.418776, -4.119844, 4.0247717, 23.180462, -4.22462, 4.107806, 23.420288, -4.3400836, 4.5620866, 23.516861, -4.504075, 4.5656476, 23.906963, -4.6092978, 5.019003, 23.91192, -4.3400836, 4.5620866, 23.516861, -4.4175224, 4.424284, 23.794865, -4.504075, 4.5656476, 23.906963, -4.644364, 4.490939, 24.260977, -5.0510015, 5.370203, 24.32837, -4.6092978, 5.019003, 23.91192, -5.0510015, 5.370203, 24.32837, -4.644364, 4.490939, 24.260977, -4.8376765, 4.627639, 24.436502, -5.0510015, 5.370203, 24.32837, -4.8376765, 4.627639, 24.436502, -4.9126344, 4.897375, 24.407942, -5.261839, 5.2418065, 24.581022, -5.520864, 5.50994, 24.639458, -5.0510015, 5.370203, 24.32837, -5.520864, 5.50994, 24.639458, -5.261839, 5.2418065, 24.581022, -5.3500004, 4.9882493, 24.782772, -5.520864, 5.50994, 24.639458, -5.3500004, 4.9882493, 24.782772, -5.4553275, 5.043026, 24.83633, -5.0510015, 5.370203, 24.32837, -5.044219, 5.1299534, 23.679413, -4.6092978, 5.019003, 23.91192, -5.09594, 5.2896347, 24.011734, -5.044219, 5.1299534, 23.679413, -5.0510015, 5.370203, 24.32837, -5.044219, 5.1299534, 23.679413, -5.09594, 5.2896347, 24.011734, -5.2869577, 5.1833105, 23.681408, -5.044219, 5.1299534, 23.679413, -5.2869577, 5.1833105, 23.681408, -5.197224, 5.1352034, 23.604895, -5.0510015, 5.370203, 24.32837, -5.321176, 5.2789783, 23.735258, -5.09594, 5.2896347, 24.011734, -5.0510015, 5.370203, 24.32837, -5.416506, 5.348407, 23.822437, -5.321176, 5.2789783, 23.735258, -4.9126344, 4.897375, 24.407942, -5.007563, 4.7688026, 24.597364, -5.0510015, 5.370203, 24.32837, -4.9126344, 4.897375, 24.407942, -4.924829, 4.7089424, 24.521822, -5.007563, 4.7688026, 24.597364, -5.520864, 5.50994, 24.639458, -5.4675636, 5.4387703, 24.16735, -5.0510015, 5.370203, 24.32837, -5.4675636, 5.4387703, 24.16735, -5.520864, 5.50994, 24.639458, -5.780359, 5.49264, 24.220936, -5.4675636, 5.4387703, 24.16735, -5.780359, 5.49264, 24.220936, -5.6462617, 5.4495068, 24.077614, -5.261839, 5.2418065, 24.581022, -5.007563, 4.7688026, 24.597364, -5.284591, 4.8992395, 24.734537, -5.261839, 5.2418065, 24.581022, -5.0510015, 5.370203, 24.32837, -5.007563, 4.7688026, 24.597364, -6.027886, 5.4678555, 24.868423, -5.780785, 5.505308, 24.545547, -5.520864, 5.50994, 24.639458, -5.780785, 5.505308, 24.545547, -6.027886, 5.4678555, 24.868423, -6.0964007, 5.467935, 24.783894, -5.780785, 5.505308, 24.545547, -6.0964007, 5.467935, 24.783894, -5.9559407, 5.492031, 24.540054, -4.504075, 4.5656476, 23.906963, -4.644364, 4.490939, 24.260977, -4.6092978, 5.019003, 23.91192, -4.504075, 4.5656476, 23.906963, -4.5401545, 4.407131, 24.07597, -4.644364, 4.490939, 24.260977, -4.4175224, 4.424284, 23.794865, -4.4788713, 4.3216662, 23.951574, -4.504075, 4.5656476, 23.906963, -4.4175224, 4.424284, 23.794865, -4.4406796, 4.305605, 23.890078, -4.4788713, 4.3216662, 23.951574, -4.4175224, 4.424284, 23.794865, -4.3400836, 4.5620866, 23.516861, -4.367128, 4.250943, 23.76148, -4.253202, 4.254257, 23.418776, -4.367128, 4.250943, 23.76148, -4.3400836, 4.5620866, 23.516861, -4.253202, 4.254257, 23.418776, -4.274289, 4.1678133, 23.544254, -4.367128, 4.250943, 23.76148, -6.100014, 5.3618755, 25.36038, -6.053468, 5.4692774, 24.699776, -5.951238, 5.4974275, 24.761002, -6.100014, 5.3618755, 25.36038, -6.5840898, 5.185566, 25.139408, -6.053468, 5.4692774, 24.699776, -5.702927, 4.9555445, 25.310673, -5.8909087, 5.2601395, 25.158152, -5.754765, 5.1142435, 25.118452, -5.702927, 4.9555445, 25.310673, -6.100014, 5.3618755, 25.36038, -5.8909087, 5.2601395, 25.158152, -6.868545, 2.974114, 26.87804, -6.7395024, 3.6925054, 26.67168, -6.369455, 3.4866214, 26.543037, -6.868545, 2.974114, 26.87804, -6.9426017, 2.9990473, 26.889389, -6.7395024, 3.6925054, 26.67168, -6.7395024, 3.6925054, 26.67168, -6.2608256, 3.6231003, 26.448338, -6.369455, 3.4866214, 26.543037, -6.7395024, 3.6925054, 26.67168, -6.429816, 3.8601167, 26.47786, -6.2608256, 3.6231003, 26.448338, -6.429816, 3.8601167, 26.47786, -6.7395024, 3.6925054, 26.67168, -6.5268407, 4.3764234, 26.390133, -6.429816, 3.8601167, 26.47786, -6.5268407, 4.3764234, 26.390133, -5.9696584, 3.9842505, 26.192686, -6.429816, 3.8601167, 26.47786, -5.9696584, 3.9842505, 26.192686, -6.1631923, 3.745168, 26.363018, -5.9696584, 3.9842505, 26.192686, -6.049317, 4.232349, 26.135853, -5.944036, 4.043989, 26.133595, -5.9696584, 3.9842505, 26.192686, -6.5268407, 4.3764234, 26.390133, -6.049317, 4.232349, 26.135853, -5.773127, 4.5062346, 25.794031, -5.898379, 4.8849564, 25.608694, -5.7558064, 4.6165876, 25.667988, -5.773127, 4.5062346, 25.794031, -6.2885137, 4.9992027, 25.899937, -5.898379, 4.8849564, 25.608694, -7.0106807, 3.0087154, 26.849985, -6.7395024, 3.6925054, 26.67168, -6.9426017, 2.9990473, 26.889389, -6.7395024, 3.6925054, 26.67168, -7.0106807, 3.0087154, 26.849985, -6.9334645, 3.3768253, 26.686901, -6.9334645, 3.3768253, 26.686901, -7.0106807, 3.0087154, 26.849985, -7.0455823, 3.2654228, 26.659414, -6.7395024, 3.6925054, 26.67168, -6.892614, 4.0271807, 26.316252, -6.5268407, 4.3764234, 26.390133, -6.7395024, 3.6925054, 26.67168, -6.9665236, 3.8246672, 26.401512, -6.892614, 4.0271807, 26.316252, -7.0833793, 4.2717676, 25.97569, -6.2885137, 4.9992027, 25.899937, -6.5268407, 4.3764234, 26.390133, -6.2885137, 4.9992027, 25.899937, -7.0833793, 4.2717676, 25.97569, -6.9840283, 4.5172915, 25.75856, -6.2885137, 4.9992027, 25.899937, -6.9840283, 4.5172915, 25.75856, -6.7441907, 4.708605, 25.804724, -6.4090276, 5.1228185, 25.524706, -6.100014, 5.3618755, 25.36038, -6.2885137, 4.9992027, 25.899937, -6.100014, 5.3618755, 25.36038, -6.4090276, 5.1228185, 25.524706, -6.6533313, 5.102206, 25.24277, -6.100014, 5.3618755, 25.36038, -6.6533313, 5.102206, 25.24277, -6.5840898, 5.185566, 25.139408, -6.2885137, 4.9992027, 25.899937, -6.049317, 4.232349, 26.135853, -6.5268407, 4.3764234, 26.390133, -6.123463, 4.6506386, 25.980106, -6.049317, 4.232349, 26.135853, -6.2885137, 4.9992027, 25.899937, -6.049317, 4.232349, 26.135853, -6.123463, 4.6506386, 25.980106, -5.8564105, 4.2503223, 25.933277, -6.049317, 4.232349, 26.135853, -5.8564105, 4.2503223, 25.933277, -5.898976, 4.1555033, 26.03533, -6.2885137, 4.9992027, 25.899937, -5.822971, 4.3798666, 25.900269, -6.123463, 4.6506386, 25.980106, -6.2885137, 4.9992027, 25.899937, -5.773127, 4.5062346, 25.794031, -5.822971, 4.3798666, 25.900269, -6.7441907, 4.708605, 25.804724, -6.8905606, 4.769124, 25.571657, -6.2885137, 4.9992027, 25.899937, -6.7441907, 4.708605, 25.804724, -6.933764, 4.659773, 25.663965, -6.8905606, 4.769124, 25.571657, -6.100014, 5.3618755, 25.36038, -5.898379, 4.8849564, 25.608694, -6.2885137, 4.9992027, 25.899937, -5.898379, 4.8849564, 25.608694, -6.100014, 5.3618755, 25.36038, -5.702927, 4.9555445, 25.310673, -5.898379, 4.8849564, 25.608694, -5.702927, 4.9555445, 25.310673, -5.727136, 4.8010197, 25.48896, -6.4090276, 5.1228185, 25.524706, -6.8905606, 4.769124, 25.571657, -6.712791, 4.966251, 25.297207, -6.4090276, 5.1228185, 25.524706, -6.2885137, 4.9992027, 25.899937, -6.8905606, 4.769124, 25.571657, -5.951238, 5.4974275, 24.761002, -5.8909087, 5.2601395, 25.158152, -6.100014, 5.3618755, 25.36038, -5.8909087, 5.2601395, 25.158152, -5.951238, 5.4974275, 24.761002, -5.857908, 5.4214015, 24.738348, -5.8909087, 5.2601395, 25.158152, -5.857908, 5.4214015, 24.738348, -5.786898, 5.237897, 24.993658, -6.892614, 4.0271807, 26.316252, -7.0833793, 4.2717676, 25.97569, -6.5268407, 4.3764234, 26.390133, -6.892614, 4.0271807, 26.316252, -7.084641, 4.049944, 26.135506, -7.0833793, 4.2717676, 25.97569, -6.9665236, 3.8246672, 26.401512, -7.0913386, 3.8691072, 26.228043, -6.892614, 4.0271807, 26.316252, -6.9665236, 3.8246672, 26.401512, -7.0892367, 3.8087032, 26.28731, -7.0913386, 3.8691072, 26.228043, -6.9665236, 3.8246672, 26.401512, -6.7395024, 3.6925054, 26.67168, -7.0888734, 3.6620417, 26.401033, -6.9334645, 3.3768253, 26.686901, -7.0888734, 3.6620417, 26.401033, -6.7395024, 3.6925054, 26.67168, -6.9334645, 3.3768253, 26.686901, -7.0596833, 3.4167135, 26.568975, -7.0888734, 3.6620417, 26.401033, -6.3319364, 5.7359695, 25.081383, -5.9698243, 5.384118, 24.680239, -5.904365, 5.4435186, 24.765022, -6.3319364, 5.7359695, 25.081383, -6.686463, 5.4666615, 24.73655, -5.9698243, 5.384118, 24.680239, -6.155786, 5.38664, 25.487875, -6.125067, 5.5240107, 25.150925, -6.053208, 5.3827543, 25.27274, -6.155786, 5.38664, 25.487875, -6.3319364, 5.7359695, 25.081383, -6.125067, 5.5240107, 25.150925, -8.413198, 4.890188, 26.407503, -7.970281, 5.3087463, 26.08081, -7.7057996, 5.0614824, 26.335453, -8.413198, 4.890188, 26.407503, -8.465763, 4.9176445, 26.35488, -7.970281, 5.3087463, 26.08081, -7.970281, 5.3087463, 26.08081, -7.5340934, 5.103933, 26.298006, -7.7057996, 5.0614824, 26.335453, -7.970281, 5.3087463, 26.08081, -7.592959, 5.3087974, 26.109215, -7.5340934, 5.103933, 26.298006, -7.592959, 5.3087974, 26.109215, -7.970281, 5.3087463, 26.08081, -7.4463277, 5.651639, 25.785309, -7.592959, 5.3087974, 26.109215, -7.4463277, 5.651639, 25.785309, -7.0733275, 5.2124443, 26.196064, -7.592959, 5.3087974, 26.109215, -7.0733275, 5.2124443, 26.196064, -7.379638, 5.141446, 26.26409, -7.0733275, 5.2124443, 26.196064, -7.0267406, 5.3698435, 26.018854, -7.005255, 5.218994, 26.151365, -7.0733275, 5.2124443, 26.196064, -7.4463277, 5.651639, 25.785309, -7.0267406, 5.3698435, 26.018854, -6.5752587, 5.356027, 25.89963, -6.46051, 5.5302234, 25.563005, -6.4677835, 5.358314, 25.792141, -6.5752587, 5.356027, 25.89963, -6.836881, 5.81112, 25.415369, -6.46051, 5.5302234, 25.563005, -8.498926, 4.900632, 26.286041, -7.970281, 5.3087463, 26.08081, -8.465763, 4.9176445, 26.35488, -7.970281, 5.3087463, 26.08081, -8.498926, 4.900632, 26.286041, -8.239595, 5.077715, 26.094341, -8.239595, 5.077715, 26.094341, -8.498926, 4.900632, 26.286041, -8.351461, 4.9742537, 26.046995, -7.970281, 5.3087463, 26.08081, -7.8256736, 5.34012, 25.656843, -7.4463277, 5.651639, 25.785309, -7.970281, 5.3087463, 26.08081, -7.9896913, 5.2398424, 25.73777, -7.8256736, 5.34012, 25.656843, -7.744165, 5.310528, 25.246101, -6.836881, 5.81112, 25.415369, -7.4463277, 5.651639, 25.785309, -6.836881, 5.81112, 25.415369, -7.744165, 5.310528, 25.246101, -7.485513, 5.35617, 25.082954, -6.836881, 5.81112, 25.415369, -7.485513, 5.35617, 25.082954, -7.2575336, 5.5329638, 25.196087, -6.7352633, 5.6665816, 25.089825, -6.3319364, 5.7359695, 25.081383, -6.836881, 5.81112, 25.415369, -6.3319364, 5.7359695, 25.081383, -6.7352633, 5.6665816, 25.089825, -6.815335, 5.4714, 24.787678, -6.3319364, 5.7359695, 25.081383, -6.815335, 5.4714, 24.787678, -6.686463, 5.4666615, 24.73655, -6.836881, 5.81112, 25.415369, -7.0267406, 5.3698435, 26.018854, -7.4463277, 5.651639, 25.785309, -6.8723903, 5.5936527, 25.718147, -7.0267406, 5.3698435, 26.018854, -6.836881, 5.81112, 25.415369, -7.0267406, 5.3698435, 26.018854, -6.8723903, 5.5936527, 25.718147, -6.7732606, 5.244132, 25.999937, -7.0267406, 5.3698435, 26.018854, -6.7732606, 5.244132, 25.999937, -6.888104, 5.2399597, 26.077604, -6.836881, 5.81112, 25.415369, -6.703796, 5.3285036, 25.979181, -6.8723903, 5.5936527, 25.718147, -6.836881, 5.81112, 25.415369, -6.5752587, 5.356027, 25.89963, -6.703796, 5.3285036, 25.979181, -7.2575336, 5.5329638, 25.196087, -7.249745, 5.4294252, 24.944311, -6.836881, 5.81112, 25.415369, -7.2575336, 5.5329638, 25.196087, -7.3613477, 5.405882, 25.013523, -7.249745, 5.4294252, 24.944311, -6.3319364, 5.7359695, 25.081383, -6.46051, 5.5302234, 25.563005, -6.836881, 5.81112, 25.415369, -6.46051, 5.5302234, 25.563005, -6.3319364, 5.7359695, 25.081383, -6.155786, 5.38664, 25.487875, -6.46051, 5.5302234, 25.563005, -6.155786, 5.38664, 25.487875, -6.3074155, 5.384823, 25.640076, -6.7352633, 5.6665816, 25.089825, -7.249745, 5.4294252, 24.944311, -6.916063, 5.3964624, 24.806072, -6.7352633, 5.6665816, 25.089825, -6.836881, 5.81112, 25.415369, -7.249745, 5.4294252, 24.944311, -5.904365, 5.4435186, 24.765022, -6.125067, 5.5240107, 25.150925, -6.3319364, 5.7359695, 25.081383, -6.125067, 5.5240107, 25.150925, -5.904365, 5.4435186, 24.765022, -5.8518662, 5.369458, 24.847279, -6.125067, 5.5240107, 25.150925, -5.8518662, 5.369458, 24.847279, -5.982039, 5.3976545, 25.132534, -7.8256736, 5.34012, 25.656843, -7.744165, 5.310528, 25.246101, -7.4463277, 5.651639, 25.785309, -7.8256736, 5.34012, 25.656843, -7.8908563, 5.243067, 25.42438, -7.744165, 5.310528, 25.246101, -7.9896913, 5.2398424, 25.73777, -7.989692, 5.1600666, 25.528631, -7.8256736, 5.34012, 25.656843, -7.9896913, 5.2398424, 25.73777, -8.038292, 5.1534004, 25.594398, -7.989692, 5.1600666, 25.528631, -7.9896913, 5.2398424, 25.73777, -7.970281, 5.3087463, 26.08081, -8.13969, 5.114773, 25.721031, -8.239595, 5.077715, 26.094341, -8.13969, 5.114773, 25.721031, -7.970281, 5.3087463, 26.08081, -8.239595, 5.077715, 26.094341, -8.274294, 5.0338635, 25.932472, -8.13969, 5.114773, 25.721031, -6.331285, 5.6798854, 24.653553, -5.876188, 5.388583, 24.768026, -5.910171, 5.4440794, 24.847443, -6.331285, 5.6798854, 24.653553, -6.2332807, 5.427716, 24.268461, -5.876188, 5.388583, 24.768026, -6.555012, 5.4120646, 24.973154, -6.2882495, 5.5129185, 24.845657, -6.350158, 5.405257, 24.957108, -6.555012, 5.4120646, 24.973154, -6.331285, 5.6798854, 24.653553, -6.2882495, 5.5129185, 24.845657, -8.316685, 4.926779, 23.737947, -7.8548923, 5.297988, 23.903517, -7.925697, 5.1127415, 24.21713, -8.316685, 4.926779, 23.737947, -8.301708, 4.9451656, 23.675941, -7.8548923, 5.297988, 23.903517, -7.8548923, 5.297988, 23.903517, -7.815824, 5.15586, 24.324652, -7.925697, 5.1127415, 24.21713, -7.8548923, 5.297988, 23.903517, -7.6972337, 5.3176384, 24.191387, -7.815824, 5.15586, 24.324652, -7.6972337, 5.3176384, 24.191387, -7.8548923, 5.297988, 23.903517, -7.37958, 5.598157, 24.143217, -7.6972337, 5.3176384, 24.191387, -7.37958, 5.598157, 24.143217, -7.52163, 5.2640796, 24.611933, -7.6972337, 5.3176384, 24.191387, -7.52163, 5.2640796, 24.611933, -7.717137, 5.1935024, 24.421171, -7.52163, 5.2640796, 24.611933, -7.3604617, 5.389035, 24.563318, -7.457949, 5.2713842, 24.642237, -7.52163, 5.2640796, 24.611933, -7.37958, 5.598157, 24.143217, -7.3604617, 5.389035, 24.563318, -7.058254, 5.3891068, 24.842972, -6.74797, 5.522382, 24.775219, -6.928924, 5.3911495, 24.875774, -7.058254, 5.3891068, 24.842972, -6.8127713, 5.7382603, 24.422806, -6.74797, 5.522382, 24.775219, -8.2651825, 4.9275074, 23.624132, -7.8548923, 5.297988, 23.903517, -8.301708, 4.9451656, 23.675941, -7.8548923, 5.297988, 23.903517, -8.2651825, 4.9275074, 23.624132, -7.9951816, 5.089792, 23.725204, -7.9951816, 5.089792, 23.725204, -8.2651825, 4.9275074, 23.624132, -8.016663, 4.994527, 23.62873, -7.8548923, 5.297988, 23.903517, -7.4628644, 5.3177576, 23.831608, -7.37958, 5.598157, 24.143217, -7.8548923, 5.297988, 23.903517, -7.6030936, 5.2291493, 23.751238, -7.4628644, 5.3177576, 23.831608, -7.1153607, 5.282588, 23.72141, -6.8127713, 5.7382603, 24.422806, -7.37958, 5.598157, 24.143217, -6.8127713, 5.7382603, 24.422806, -7.1153607, 5.282588, 23.72141, -6.87394, 5.326008, 23.83848, -6.8127713, 5.7382603, 24.422806, -6.87394, 5.326008, 23.83848, -6.8445373, 5.4831157, 24.042107, -6.518, 5.605947, 24.369406, -6.331285, 5.6798854, 24.653553, -6.8127713, 5.7382603, 24.422806, -6.331285, 5.6798854, 24.653553, -6.518, 5.605947, 24.369406, -6.3297257, 5.428781, 24.196327, -6.331285, 5.6798854, 24.653553, -6.3297257, 5.428781, 24.196327, -6.2332807, 5.427716, 24.268461, -6.8127713, 5.7382603, 24.422806, -7.3604617, 5.389035, 24.563318, -7.37958, 5.598157, 24.143217, -7.056313, 5.567637, 24.537035, -7.3604617, 5.389035, 24.563318, -6.8127713, 5.7382603, 24.422806, -7.3604617, 5.389035, 24.563318, -7.056313, 5.567637, 24.537035, -7.24099, 5.2982535, 24.745642, -7.3604617, 5.389035, 24.563318, -7.24099, 5.2982535, 24.745642, -7.3485713, 5.2914233, 24.69535, -6.8127713, 5.7382603, 24.422806, -7.1779733, 5.3646665, 24.78445, -7.056313, 5.567637, 24.537035, -6.8127713, 5.7382603, 24.422806, -7.058254, 5.3891068, 24.842972, -7.1779733, 5.3646665, 24.78445, -6.8445373, 5.4831157, 24.042107, -6.6547856, 5.388571, 23.94828, -6.8127713, 5.7382603, 24.422806, -6.8445373, 5.4831157, 24.042107, -6.7587676, 5.367548, 23.897482, -6.6547856, 5.388571, 23.94828, -6.331285, 5.6798854, 24.653553, -6.74797, 5.522382, 24.775219, -6.8127713, 5.7382603, 24.422806, -6.74797, 5.522382, 24.775219, -6.331285, 5.6798854, 24.653553, -6.555012, 5.4120646, 24.973154, -6.74797, 5.522382, 24.775219, -6.555012, 5.4120646, 24.973154, -6.737456, 5.410923, 24.927305, -6.518, 5.605947, 24.369406, -6.6547856, 5.388571, 23.94828, -6.404985, 5.3717523, 24.133698, -6.518, 5.605947, 24.369406, -6.8127713, 5.7382603, 24.422806, -6.6547856, 5.388571, 23.94828, -5.910171, 5.4440794, 24.847443, -6.2882495, 5.5129185, 24.845657, -6.331285, 5.6798854, 24.653553, -6.2882495, 5.5129185, 24.845657, -5.910171, 5.4440794, 24.847443, -5.947083, 5.38731, 24.924572, -6.2882495, 5.5129185, 24.845657, -5.947083, 5.38731, 24.924572, -6.211325, 5.4130735, 24.94913, -7.4628644, 5.3177576, 23.831608, -7.1153607, 5.282588, 23.72141, -7.37958, 5.598157, 24.143217, -7.4628644, 5.3177576, 23.831608, -7.319452, 5.226307, 23.692755, -7.1153607, 5.282588, 23.72141, -7.6030936, 5.2291493, 23.751238, -7.4537644, 5.1571455, 23.66732, -7.4628644, 5.3177576, 23.831608, -7.6030936, 5.2291493, 23.751238, -7.522962, 5.150751, 23.66025, -7.4537644, 5.1571455, 23.66732, -7.6030936, 5.2291493, 23.751238, -7.8548923, 5.297988, 23.903517, -7.6649528, 5.116803, 23.641397, -7.9951816, 5.089792, 23.725204, -7.6649528, 5.116803, 23.641397, -7.8548923, 5.297988, 23.903517, -7.9951816, 5.089792, 23.725204, -7.8895354, 5.0471697, 23.63519, -7.6649528, 5.116803, 23.641397, -5.857347, 5.7116084, 24.363003, -5.903773, 5.378351, 24.927797, -6.00098, 5.4485216, 24.902779, -5.857347, 5.7116084, 24.363003, -5.4018383, 5.389697, 24.404541, -5.903773, 5.378351, 24.927797, -6.2981954, 5.4044847, 24.163559, -6.076615, 5.5257673, 24.45499, -6.2318177, 5.399699, 24.403456, -6.2981954, 5.4044847, 24.163559, -5.857347, 5.7116084, 24.363003, -6.076615, 5.5257673, 24.45499, -5.4074144, 4.736121, 21.818316, -5.439855, 5.177844, 22.396505, -5.831164, 4.97289, 22.37704, -5.4074144, 4.736121, 21.818316, -5.3315473, 4.755287, 21.823586, -5.439855, 5.177844, 22.396505, -5.439855, 5.177844, 22.396505, -5.9201818, 5.030147, 22.5292, -5.831164, 4.97289, 22.37704, -5.439855, 5.177844, 22.396505, -5.7215886, 5.217056, 22.642551, -5.9201818, 5.030147, 22.5292, -5.7215886, 5.217056, 22.642551, -5.439855, 5.177844, 22.396505, -5.5580716, 5.554124, 23.009472, -5.7215886, 5.217056, 22.642551, -5.5580716, 5.554124, 23.009472, -6.160071, 5.178384, 22.936909, -5.7215886, 5.217056, 22.642551, -6.160071, 5.178384, 22.936909, -6.000282, 5.0809345, 22.665983, -6.160071, 5.178384, 22.936909, -6.046327, 5.3291693, 23.119432, -6.1793313, 5.1894274, 23.018368, -6.160071, 5.178384, 22.936909, -5.5580716, 5.554124, 23.009472, -6.046327, 5.3291693, 23.119432, -6.2826037, 5.3541317, 23.53723, -6.1099467, 5.52, 23.893438, -6.287435, 5.3614902, 23.69768, -6.2826037, 5.3541317, 23.53723, -5.7123218, 5.753208, 23.741987, -6.1099467, 5.52, 23.893438, -5.2635365, 4.7313147, 21.856642, -5.439855, 5.177844, 22.396505, -5.3315473, 4.755287, 21.823586, -5.439855, 5.177844, 22.396505, -5.2635365, 4.7313147, 21.856642, -5.291579, 4.922532, 22.195475, -5.291579, 4.922532, 22.195475, -5.2635365, 4.7313147, 21.856642, -5.19895, 4.8064184, 22.150917, -5.439855, 5.177844, 22.396505, -5.24503, 5.200137, 22.848965, -5.5580716, 5.554124, 23.009472, -5.439855, 5.177844, 22.396505, -5.1995373, 5.0887403, 22.666178, -5.24503, 5.200137, 22.848965, -5.0281267, 5.1566877, 23.240654, -5.7123218, 5.753208, 23.741987, -5.5580716, 5.554124, 23.009472, -5.7123218, 5.753208, 23.741987, -5.0281267, 5.1566877, 23.240654, -5.097282, 5.219322, 23.551094, -5.7123218, 5.753208, 23.741987, -5.097282, 5.219322, 23.551094, -5.3022, 5.4230766, 23.628416, -5.577439, 5.6020293, 24.084873, -5.857347, 5.7116084, 24.363003, -5.7123218, 5.753208, 23.741987, -5.857347, 5.7116084, 24.363003, -5.577439, 5.6020293, 24.084873, -5.342383, 5.384054, 24.275276, -5.857347, 5.7116084, 24.363003, -5.342383, 5.384054, 24.275276, -5.4018383, 5.389697, 24.404541, -5.7123218, 5.753208, 23.741987, -6.046327, 5.3291693, 23.119432, -5.5580716, 5.554124, 23.009472, -5.917007, 5.5500546, 23.476519, -6.046327, 5.3291693, 23.119432, -5.7123218, 5.753208, 23.741987, -6.046327, 5.3291693, 23.119432, -5.917007, 5.5500546, 23.476519, -6.244391, 5.2299757, 23.296265, -6.046327, 5.3291693, 23.119432, -6.244391, 5.2299757, 23.296265, -6.210569, 5.21813, 23.159012, -5.7123218, 5.753208, 23.741987, -6.2491727, 5.318202, 23.382605, -5.917007, 5.5500546, 23.476519, -5.7123218, 5.753208, 23.741987, -6.2826037, 5.3541317, 23.53723, -6.2491727, 5.318202, 23.382605, -5.3022, 5.4230766, 23.628416, -5.154279, 5.307758, 23.83554, -5.7123218, 5.753208, 23.741987, -5.3022, 5.4230766, 23.628416, -5.1251884, 5.2768984, 23.701454, -5.154279, 5.307758, 23.83554, -5.857347, 5.7116084, 24.363003, -6.1099467, 5.52, 23.893438, -5.7123218, 5.753208, 23.741987, -6.1099467, 5.52, 23.893438, -5.857347, 5.7116084, 24.363003, -6.2981954, 5.4044847, 24.163559, -6.1099467, 5.52, 23.893438, -6.2981954, 5.4044847, 24.163559, -6.291105, 5.3954716, 23.937151, -5.577439, 5.6020293, 24.084873, -5.154279, 5.307758, 23.83554, -5.3127255, 5.301915, 24.168657, -5.577439, 5.6020293, 24.084873, -5.7123218, 5.753208, 23.741987, -5.154279, 5.307758, 23.83554, -6.00098, 5.4485216, 24.902779, -6.076615, 5.5257673, 24.45499, -5.857347, 5.7116084, 24.363003, -6.076615, 5.5257673, 24.45499, -6.00098, 5.4485216, 24.902779, -6.1017814, 5.3846993, 24.875137, -6.076615, 5.5257673, 24.45499, -6.1017814, 5.3846993, 24.875137, -6.184852, 5.4140406, 24.567734, -5.24503, 5.200137, 22.848965, -5.0281267, 5.1566877, 23.240654, -5.5580716, 5.554124, 23.009472, -5.24503, 5.200137, 22.848965, -5.0552382, 5.0847697, 22.992046, -5.0281267, 5.1566877, 23.240654, -5.1995373, 5.0887403, 22.666178, -5.0783086, 4.9989347, 22.825645, -5.24503, 5.200137, 22.848965, -5.1995373, 5.0887403, 22.666178, -5.0852733, 4.9907217, 22.742779, -5.0783086, 4.9989347, 22.825645, -5.1995373, 5.0887403, 22.666178, -5.439855, 5.177844, 22.396505, -5.102995, 4.9489727, 22.570704, -5.291579, 4.922532, 22.195475, -5.102995, 4.9489727, 22.570704, -5.439855, 5.177844, 22.396505, -5.291579, 4.922532, 22.195475, -5.163247, 4.866782, 22.303133, -5.102995, 4.9489727, 22.570704, -5.488422, 5.4559135, 25.067163, -6.115338, 5.4586115, 24.851, -6.0311418, 5.4854546, 24.766218, -5.488422, 5.4559135, 25.067163, -5.8165927, 5.2966948, 25.486755, -6.115338, 5.4586115, 24.851, -5.37565, 5.0100093, 24.717901, -5.60895, 5.3052464, 24.837267, -5.5913396, 5.1425714, 24.705996, -5.37565, 5.0100093, 24.717901, -5.488422, 5.4559135, 25.067163, -5.60895, 5.3052464, 24.837267, -3.997026, 3.4084823, 26.23418, -4.2373033, 4.0717287, 26.033955, -4.2301164, 3.8139224, 25.675167, -3.997026, 3.4084823, 26.23418, -4.010387, 3.441938, 26.303183, -4.2373033, 4.0717287, 26.033955, -4.2373033, 4.0717287, 26.033955, -4.3043237, 3.923424, 25.547739, -4.2301164, 3.8139224, 25.675167, -4.2373033, 4.0717287, 26.033955, -4.351472, 4.177483, 25.700325, -4.3043237, 3.923424, 25.547739, -4.351472, 4.177483, 25.700325, -4.2373033, 4.0717287, 26.033955, -4.5209985, 4.6817417, 25.74504, -4.351472, 4.177483, 25.700325, -4.5209985, 4.6817417, 25.74504, -4.504621, 4.2121515, 25.204073, -4.351472, 4.177483, 25.700325, -4.504621, 4.2121515, 25.204073, -4.3712316, 4.021261, 25.432993, -4.504621, 4.2121515, 25.204073, -4.6092005, 4.455448, 25.259403, -4.5602574, 4.259628, 25.160809, -4.504621, 4.2121515, 25.204073, -4.5209985, 4.6817417, 25.74504, -4.6092005, 4.455448, 25.259403, -4.8856854, 4.646908, 24.917786, -5.1403203, 5.003999, 24.975368, -5.0130434, 4.735046, 24.86499, -4.8856854, 4.646908, 24.917786, -4.988566, 5.1979246, 25.387373, -5.1403203, 5.003999, 24.975368, -4.0683527, 3.4522138, 26.357347, -4.2373033, 4.0717287, 26.033955, -4.010387, 3.441938, 26.303183, -4.2373033, 4.0717287, 26.033955, -4.0683527, 3.4522138, 26.357347, -4.242745, 3.782452, 26.231682, -4.242745, 3.782452, 26.231682, -4.0683527, 3.4522138, 26.357347, -4.2885528, 3.6797438, 26.329477, -4.2373033, 4.0717287, 26.033955, -4.656346, 4.3628387, 26.08645, -4.5209985, 4.6817417, 25.74504, -4.2373033, 4.0717287, 26.033955, -4.57377, 4.1835814, 26.181997, -4.656346, 4.3628387, 26.08645, -5.062138, 4.5712886, 26.17474, -4.988566, 5.1979246, 25.387373, -4.5209985, 4.6817417, 25.74504, -4.988566, 5.1979246, 25.387373, -5.062138, 4.5712886, 26.17474, -5.2679048, 4.7707634, 26.016972, -4.988566, 5.1979246, 25.387373, -5.2679048, 4.7707634, 26.016972, -5.1766024, 4.9423556, 25.806808, -5.3932915, 5.275302, 25.41678, -5.488422, 5.4559135, 25.067163, -4.988566, 5.1979246, 25.387373, -5.488422, 5.4559135, 25.067163, -5.3932915, 5.275302, 25.41678, -5.7287784, 5.236825, 25.581738, -5.488422, 5.4559135, 25.067163, -5.7287784, 5.236825, 25.581738, -5.8165927, 5.2966948, 25.486755, -4.988566, 5.1979246, 25.387373, -4.6092005, 4.455448, 25.259403, -4.5209985, 4.6817417, 25.74504, -4.825552, 4.8508716, 25.277138, -4.6092005, 4.455448, 25.259403, -4.988566, 5.1979246, 25.387373, -4.6092005, 4.455448, 25.259403, -4.825552, 4.8508716, 25.277138, -4.749141, 4.4242043, 25.014292, -4.6092005, 4.455448, 25.259403, -4.749141, 4.4242043, 25.014292, -4.653323, 4.3502817, 25.089447, -4.988566, 5.1979246, 25.387373, -4.784744, 4.543359, 24.994701, -4.825552, 4.8508716, 25.277138, -4.988566, 5.1979246, 25.387373, -4.8856854, 4.646908, 24.917786, -4.784744, 4.543359, 24.994701, -5.1766024, 4.9423556, 25.806808, -5.4469156, 4.981415, 25.883078, -4.988566, 5.1979246, 25.387373, -5.1766024, 4.9423556, 25.806808, -5.3591576, 4.891834, 25.949957, -5.4469156, 4.981415, 25.883078, -5.488422, 5.4559135, 25.067163, -5.1403203, 5.003999, 24.975368, -4.988566, 5.1979246, 25.387373, -5.1403203, 5.003999, 24.975368, -5.488422, 5.4559135, 25.067163, -5.37565, 5.0100093, 24.717901, -5.1403203, 5.003999, 24.975368, -5.37565, 5.0100093, 24.717901, -5.1955976, 4.8869286, 24.792862, -5.3932915, 5.275302, 25.41678, -5.4469156, 4.981415, 25.883078, -5.6794295, 5.1171756, 25.630447, -5.3932915, 5.275302, 25.41678, -4.988566, 5.1979246, 25.387373, -5.4469156, 4.981415, 25.883078, -6.0311418, 5.4854546, 24.766218, -5.60895, 5.3052464, 24.837267, -5.488422, 5.4559135, 25.067163, -5.60895, 5.3052464, 24.837267, -6.0311418, 5.4854546, 24.766218, -6.01732, 5.3982096, 24.681461, -5.60895, 5.3052464, 24.837267, -6.01732, 5.3982096, 24.681461, -5.733069, 5.248727, 24.701923, -4.656346, 4.3628387, 26.08645, -5.062138, 4.5712886, 26.17474, -4.5209985, 4.6817417, 25.74504, -4.656346, 4.3628387, 26.08645, -4.8856683, 4.377059, 26.22222, -5.062138, 4.5712886, 26.17474, -4.57377, 4.1835814, 26.181997, -4.779701, 4.213683, 26.243425, -4.656346, 4.3628387, 26.08645, -4.57377, 4.1835814, 26.181997, -4.715779, 4.162856, 26.263649, -4.779701, 4.213683, 26.243425, -4.57377, 4.1835814, 26.181997, -4.2373033, 4.0717287, 26.033955, -4.590958, 4.0354905, 26.29871, -4.242745, 3.782452, 26.231682, -4.590958, 4.0354905, 26.29871, -4.2373033, 4.0717287, 26.033955, -4.242745, 3.782452, 26.231682, -4.395331, 3.8163853, 26.320553, -4.590958, 4.0354905, 26.29871, -6.1042504, 1.2728562, 23.197392, -5.918094, 2.7018955, 23.690025, -5.9148564, 2.7549148, 23.524578, -6.1042504, 1.2728562, 23.197392, -6.1098676, 1.2173094, 23.399563, -5.918094, 2.7018955, 23.690025, -6.4102006, -0.38891935, 23.115858, -6.1098676, 1.2173094, 23.399563, -6.1042504, 1.2728562, 23.197392, -6.4102006, -0.38891935, 23.115858, -6.407615, -0.42018914, 23.34877, -6.1098676, 1.2173094, 23.399563, -6.445969, 1.214475, 23.560837, -6.334398, 2.7400944, 23.713541, -6.206943, 2.6917243, 23.821266, -6.445969, 1.214475, 23.560837, -6.593639, 1.2685848, 23.43337, -6.334398, 2.7400944, 23.713541, -6.45285, 1.3504215, 23.077362, -6.04308, 2.8031602, 23.417374, -6.2159896, 2.8219187, 23.420893, -6.45285, 1.3504215, 23.077362, -6.2506223, 1.3273255, 23.06642, -6.04308, 2.8031602, 23.417374, -6.206943, 2.6917243, 23.821266, -6.1571717, 4.087394, 24.238876, -6.071984, 4.053143, 24.309086, -6.206943, 2.6917243, 23.821266, -6.334398, 2.7400944, 23.713541, -6.1571717, 4.087394, 24.238876, -5.9148564, 2.7549148, 23.524578, -5.878671, 4.066166, 24.22505, -5.875483, 4.1052194, 24.1148, -5.9148564, 2.7549148, 23.524578, -5.918094, 2.7018955, 23.690025, -5.878671, 4.066166, 24.22505, -5.875483, 4.1052194, 24.1148, -5.878671, 4.066166, 24.22505, -5.8760715, 4.7822523, 24.560642, -5.875483, 4.1052194, 24.1148, -5.8760715, 4.7822523, 24.560642, -5.8734727, 4.8202696, 24.482874, -6.1098676, 1.2173094, 23.399563, -6.0342355, 2.6733067, 23.816893, -5.918094, 2.7018955, 23.690025, -6.1098676, 1.2173094, 23.399563, -6.2450395, 1.1910194, 23.5534, -6.0342355, 2.6733067, 23.816893, -5.918094, 2.7018955, 23.690025, -6.0342355, 2.6733067, 23.816893, -5.9568696, 4.0428257, 24.307844, -5.918094, 2.7018955, 23.690025, -5.9568696, 4.0428257, 24.307844, -5.878671, 4.066166, 24.22505, -6.593639, 1.2685848, 23.43337, -6.3314233, 2.7938638, 23.547709, -6.334398, 2.7400944, 23.713541, -6.593639, 1.2685848, 23.43337, -6.589192, 1.3239617, 23.234522, -6.3314233, 2.7938638, 23.547709, -6.334398, 2.7400944, 23.713541, -6.3314233, 2.7938638, 23.547709, -6.1551642, 4.1276875, 24.129848, -6.334398, 2.7400944, 23.713541, -6.1551642, 4.1276875, 24.129848, -6.1571717, 4.087394, 24.238876, -6.2159896, 2.8219187, 23.420893, -5.9612355, 4.1389728, 24.042906, -6.0775657, 4.150184, 24.04522, -6.2159896, 2.8219187, 23.420893, -6.04308, 2.8031602, 23.417374, -5.9612355, 4.1389728, 24.042906, -6.2506223, 1.3273255, 23.06642, -5.9148564, 2.7549148, 23.524578, -6.04308, 2.8031602, 23.417374, -6.2506223, 1.3273255, 23.06642, -6.1042504, 1.2728562, 23.197392, -5.9148564, 2.7549148, 23.524578, -6.04308, 2.8031602, 23.417374, -5.875483, 4.1052194, 24.1148, -5.9612355, 4.1389728, 24.042906, -6.04308, 2.8031602, 23.417374, -5.9148564, 2.7549148, 23.524578, -5.875483, 4.1052194, 24.1148, -6.589192, 1.3239617, 23.234522, -6.2159896, 2.8219187, 23.420893, -6.3314233, 2.7938638, 23.547709, -6.589192, 1.3239617, 23.234522, -6.45285, 1.3504215, 23.077362, -6.2159896, 2.8219187, 23.420893, -6.971047, -0.29482222, 23.15477, -6.45285, 1.3504215, 23.077362, -6.589192, 1.3239617, 23.234522, -6.971047, -0.29482222, 23.15477, -6.8199706, -0.29774308, 22.974768, -6.45285, 1.3504215, 23.077362, -6.3314233, 2.7938638, 23.547709, -6.0775657, 4.150184, 24.04522, -6.1551642, 4.1276875, 24.129848, -6.3314233, 2.7938638, 23.547709, -6.2159896, 2.8219187, 23.420893, -6.0775657, 4.150184, 24.04522, -6.1551642, 4.1276875, 24.129848, -6.0775657, 4.150184, 24.04522, -6.023566, 4.8536663, 24.431192, -6.1551642, 4.1276875, 24.129848, -6.023566, 4.8536663, 24.431192, -6.082048, 4.828084, 24.48972, -6.020826, 4.7619505, 24.618555, -5.936191, 5.2892733, 24.899128, -5.934734, 4.757289, 24.618954, -6.020826, 4.7619505, 24.618555, -5.999312, 5.2915688, 24.898323, -5.936191, 5.2892733, 24.899128, -5.8760715, 4.7822523, 24.560642, -5.8913436, 5.3461533, 24.805635, -5.8734727, 4.8202696, 24.482874, -5.8760715, 4.7822523, 24.560642, -5.8932214, 5.3139095, 24.859932, -5.8913436, 5.3461533, 24.805635, -5.934734, 4.757289, 24.618954, -5.8932214, 5.3139095, 24.859932, -5.8760715, 4.7822523, 24.560642, -5.934734, 4.757289, 24.618954, -5.936191, 5.2892733, 24.899128, -5.8932214, 5.3139095, 24.859932, -5.9370246, 4.8486996, 24.431229, -6.000772, 5.3705015, 24.767956, -6.023566, 4.8536663, 24.431192, -5.9370246, 4.8486996, 24.431229, -5.937638, 5.3697376, 24.769716, -6.000772, 5.3705015, 24.767956, -5.8734727, 4.8202696, 24.482874, -5.937638, 5.3697376, 24.769716, -5.9370246, 4.8486996, 24.431229, -5.8734727, 4.8202696, 24.482874, -5.8913436, 5.3461533, 24.805635, -5.937638, 5.3697376, 24.769716, -6.082048, 4.828084, 24.48972, -6.0456195, 5.3136215, 24.86145, -6.0841565, 4.790536, 24.567505, -6.082048, 4.828084, 24.48972, -6.0437293, 5.3473988, 24.80811, -6.0456195, 5.3136215, 24.86145, -6.023566, 4.8536663, 24.431192, -6.0437293, 5.3473988, 24.80811, -6.082048, 4.828084, 24.48972, -6.023566, 4.8536663, 24.431192, -6.000772, 5.3705015, 24.767956, -6.0437293, 5.3473988, 24.80811, -6.5591636, -0.41737175, 23.530104, -6.445969, 1.214475, 23.560837, -6.2450395, 1.1910194, 23.5534, -6.5591636, -0.41737175, 23.530104, -6.791864, -0.37708354, 23.536917, -6.445969, 1.214475, 23.560837, -6.0841565, 4.790536, 24.567505, -5.999312, 5.2915688, 24.898323, -6.020826, 4.7619505, 24.618555, -6.0841565, 4.790536, 24.567505, -6.0456195, 5.3136215, 24.86145, -5.999312, 5.2915688, 24.898323, -6.2450395, 1.1910194, 23.5534, -6.206943, 2.6917243, 23.821266, -6.0342355, 2.6733067, 23.816893, -6.2450395, 1.1910194, 23.5534, -6.445969, 1.214475, 23.560837, -6.206943, 2.6917243, 23.821266, -6.791864, -0.37708354, 23.536917, -6.593639, 1.2685848, 23.43337, -6.445969, 1.214475, 23.560837, -6.791864, -0.37708354, 23.536917, -6.9689336, -0.32619548, 23.389013, -6.593639, 1.2685848, 23.43337, -6.8199706, -0.29774308, 22.974768, -6.2506223, 1.3273255, 23.06642, -6.45285, 1.3504215, 23.077362, -6.8199706, -0.29774308, 22.974768, -6.5876184, -0.33810782, 22.968916, -6.2506223, 1.3273255, 23.06642, -6.407615, -0.42018914, 23.34877, -6.2450395, 1.1910194, 23.5534, -6.1098676, 1.2173094, 23.399563, -6.407615, -0.42018914, 23.34877, -6.5591636, -0.41737175, 23.530104, -6.2450395, 1.1910194, 23.5534, -6.9689336, -0.32619548, 23.389013, -6.589192, 1.3239617, 23.234522, -6.593639, 1.2685848, 23.43337, -6.9689336, -0.32619548, 23.389013, -6.971047, -0.29482222, 23.15477, -6.589192, 1.3239617, 23.234522, -6.5876184, -0.33810782, 22.968916, -6.1042504, 1.2728562, 23.197392, -6.2506223, 1.3273255, 23.06642, -6.5876184, -0.33810782, 22.968916, -6.4102006, -0.38891935, 23.115858, -6.1042504, 1.2728562, 23.197392, -6.0342355, 2.6733067, 23.816893, -6.071984, 4.053143, 24.309086, -5.9568696, 4.0428257, 24.307844, -6.0342355, 2.6733067, 23.816893, -6.206943, 2.6917243, 23.821266, -6.071984, 4.053143, 24.309086, -5.9568696, 4.0428257, 24.307844, -6.020826, 4.7619505, 24.618555, -5.934734, 4.757289, 24.618954, -5.9568696, 4.0428257, 24.307844, -6.071984, 4.053143, 24.309086, -6.020826, 4.7619505, 24.618555, -6.071984, 4.053143, 24.309086, -6.0841565, 4.790536, 24.567505, -6.020826, 4.7619505, 24.618555, -6.071984, 4.053143, 24.309086, -6.1571717, 4.087394, 24.238876, -6.0841565, 4.790536, 24.567505, -5.878671, 4.066166, 24.22505, -5.934734, 4.757289, 24.618954, -5.8760715, 4.7822523, 24.560642, -5.878671, 4.066166, 24.22505, -5.9568696, 4.0428257, 24.307844, -5.934734, 4.757289, 24.618954, -6.0775657, 4.150184, 24.04522, -5.9370246, 4.8486996, 24.431229, -6.023566, 4.8536663, 24.431192, -6.0775657, 4.150184, 24.04522, -5.9612355, 4.1389728, 24.042906, -5.9370246, 4.8486996, 24.431229, -5.9612355, 4.1389728, 24.042906, -5.8734727, 4.8202696, 24.482874, -5.9370246, 4.8486996, 24.431229, -5.9612355, 4.1389728, 24.042906, -5.875483, 4.1052194, 24.1148, -5.8734727, 4.8202696, 24.482874, -6.1571717, 4.087394, 24.238876, -6.082048, 4.828084, 24.48972, -6.0841565, 4.790536, 24.567505, -6.1571717, 4.087394, 24.238876, -6.1551642, 4.1276875, 24.129848, -6.082048, 4.828084, 24.48972, -7.3456454, 4.1475077, 21.98113, -7.7547154, 4.0165606, 21.935587, -7.73344, 4.0729046, 21.883982, -7.3456454, 4.1475077, 21.98113, -7.39551, 3.8681917, 22.222395, -7.7547154, 4.0165606, 21.935587, -7.2885756, 4.023938, 21.648745, -7.4439178, 4.086134, 21.816298, -7.4376407, 4.0358357, 21.702383, -7.2885756, 4.023938, 21.648745, -7.3456454, 4.1475077, 21.98113, -7.4439178, 4.086134, 21.816298, -5.835222, 3.136388, 22.094479, -6.143434, 3.5016217, 22.14021, -6.182443, 3.446207, 21.86201, -5.835222, 3.136388, 22.094479, -5.831487, 3.134635, 22.146042, -6.143434, 3.5016217, 22.14021, -6.143434, 3.5016217, 22.14021, -6.276529, 3.5192416, 21.814625, -6.182443, 3.446207, 21.86201, -6.143434, 3.5016217, 22.14021, -6.311259, 3.6133804, 21.96652, -6.276529, 3.5192416, 21.814625, -6.311259, 3.6133804, 21.96652, -6.143434, 3.5016217, 22.14021, -6.488027, 3.838162, 22.121197, -6.311259, 3.6133804, 21.96652, -6.488027, 3.838162, 22.121197, -6.5290217, 3.7089415, 21.687683, -6.311259, 3.6133804, 21.96652, -6.5290217, 3.7089415, 21.687683, -6.3611083, 3.5839481, 21.772032, -6.5290217, 3.7089415, 21.687683, -6.61853, 3.8050375, 21.778206, -6.5801196, 3.7298079, 21.680998, -6.5290217, 3.7089415, 21.687683, -6.488027, 3.838162, 22.121197, -6.61853, 3.8050375, 21.778206, -6.895559, 3.9183722, 21.635805, -7.089803, 4.029269, 21.773304, -6.996865, 3.9430032, 21.63935, -6.895559, 3.9183722, 21.635805, -6.936025, 4.07687, 22.060637, -7.089803, 4.029269, 21.773304, -5.852432, 3.11117, 22.186768, -6.143434, 3.5016217, 22.14021, -5.831487, 3.134635, 22.146042, -6.143434, 3.5016217, 22.14021, -5.852432, 3.11117, 22.186768, -6.0435195, 3.2870784, 22.199028, -6.0435195, 3.2870784, 22.199028, -5.852432, 3.11117, 22.186768, -6.0259957, 3.1908565, 22.246626, -6.143434, 3.5016217, 22.14021, -6.4199605, 3.5414572, 22.270983, -6.488027, 3.838162, 22.121197, -6.143434, 3.5016217, 22.14021, -6.316384, 3.4395418, 22.283651, -6.4199605, 3.5414572, 22.270983, -6.6648736, 3.5251694, 22.411572, -6.936025, 4.07687, 22.060637, -6.488027, 3.838162, 22.121197, -6.936025, 4.07687, 22.060637, -6.6648736, 3.5251694, 22.411572, -6.8574367, 3.616784, 22.385473, -6.936025, 4.07687, 22.060637, -6.8574367, 3.616784, 22.385473, -6.8897033, 3.7860932, 22.274248, -7.1705127, 3.995197, 22.128761, -7.3456454, 4.1475077, 21.98113, -6.936025, 4.07687, 22.060637, -7.3456454, 4.1475077, 21.98113, -7.1705127, 3.995197, 22.128761, -7.311005, 3.8388238, 22.254168, -7.3456454, 4.1475077, 21.98113, -7.311005, 3.8388238, 22.254168, -7.39551, 3.8681917, 22.222395, -6.936025, 4.07687, 22.060637, -6.61853, 3.8050375, 21.778206, -6.488027, 3.838162, 22.121197, -6.808315, 3.9616337, 21.892696, -6.61853, 3.8050375, 21.778206, -6.936025, 4.07687, 22.060637, -6.61853, 3.8050375, 21.778206, -6.808315, 3.9616337, 21.892696, -6.754091, 3.8023548, 21.658047, -6.61853, 3.8050375, 21.778206, -6.754091, 3.8023548, 21.658047, -6.667336, 3.7712486, 21.66921, -6.936025, 4.07687, 22.060637, -6.8002677, 3.871698, 21.648817, -6.808315, 3.9616337, 21.892696, -6.936025, 4.07687, 22.060637, -6.895559, 3.9183722, 21.635805, -6.8002677, 3.871698, 21.648817, -6.8897033, 3.7860932, 22.274248, -7.0301766, 3.7129135, 22.36113, -6.936025, 4.07687, 22.060637, -6.8897033, 3.7860932, 22.274248, -6.94754, 3.6726983, 22.372395, -7.0301766, 3.7129135, 22.36113, -7.3456454, 4.1475077, 21.98113, -7.089803, 4.029269, 21.773304, -6.936025, 4.07687, 22.060637, -7.089803, 4.029269, 21.773304, -7.3456454, 4.1475077, 21.98113, -7.2885756, 4.023938, 21.648745, -7.089803, 4.029269, 21.773304, -7.2885756, 4.023938, 21.648745, -7.145592, 3.9925408, 21.643513, -7.1705127, 3.995197, 22.128761, -7.0301766, 3.7129135, 22.36113, -7.250442, 3.7801287, 22.278788, -7.1705127, 3.995197, 22.128761, -6.936025, 4.07687, 22.060637, -7.0301766, 3.7129135, 22.36113, -7.73344, 4.0729046, 21.883982, -7.4439178, 4.086134, 21.816298, -7.3456454, 4.1475077, 21.98113, -7.4439178, 4.086134, 21.816298, -7.73344, 4.0729046, 21.883982, -7.730845, 4.0477705, 21.8088, -7.4439178, 4.086134, 21.816298, -7.730845, 4.0477705, 21.8088, -7.537543, 4.047288, 21.737965, -6.4199605, 3.5414572, 22.270983, -6.6648736, 3.5251694, 22.411572, -6.488027, 3.838162, 22.121197, -6.4199605, 3.5414572, 22.270983, -6.5177665, 3.4538927, 22.380463, -6.6648736, 3.5251694, 22.411572, -6.316384, 3.4395418, 22.283651, -6.4231963, 3.3844132, 22.362364, -6.4199605, 3.5414572, 22.270983, -6.316384, 3.4395418, 22.283651, -6.372542, 3.369453, 22.350878, -6.4231963, 3.3844132, 22.362364, -6.316384, 3.4395418, 22.283651, -6.143434, 3.5016217, 22.14021, -6.269735, 3.3225741, 22.32892, -6.0435195, 3.2870784, 22.199028, -6.269735, 3.3225741, 22.32892, -6.143434, 3.5016217, 22.14021, -6.0435195, 3.2870784, 22.199028, -6.1135287, 3.244587, 22.275616, -6.269735, 3.3225741, 22.32892, -8.006098, 4.0689516, 22.13492, -7.6670833, 4.0418825, 21.783504, -7.640471, 4.0882125, 21.854998, -8.006098, 4.0689516, 22.13492, -8.1642685, 3.8312206, 21.842627, -7.6670833, 4.0418825, 21.783504, -7.756726, 3.8452916, 22.38111, -7.7926917, 3.9996805, 22.145943, -7.6962695, 3.9147148, 22.219965, -7.756726, 3.8452916, 22.38111, -8.006098, 4.0689516, 22.13492, -7.7926917, 3.9996805, 22.145943, -9.112757, 2.5434918, 23.142544, -8.955492, 3.0269203, 22.925161, -8.686771, 2.9242716, 23.071201, -9.112757, 2.5434918, 23.142544, -9.159386, 2.5504227, 23.109524, -8.955492, 3.0269203, 22.925161, -8.955492, 3.0269203, 22.925161, -8.584085, 3.0207791, 23.038433, -8.686771, 2.9242716, 23.071201, -8.955492, 3.0269203, 22.925161, -8.695301, 3.1616163, 22.923185, -8.584085, 3.0207791, 23.038433, -8.695301, 3.1616163, 22.923185, -8.955492, 3.0269203, 22.925161, -8.715704, 3.4878736, 22.713732, -8.695301, 3.1616163, 22.923185, -8.715704, 3.4878736, 22.713732, -8.307899, 3.2759342, 22.950727, -8.695301, 3.1616163, 22.923185, -8.307899, 3.2759342, 22.950727, -8.491686, 3.1070724, 23.009012, -8.307899, 3.2759342, 22.950727, -8.328896, 3.4248629, 22.82755, -8.266613, 3.3116674, 22.91728, -8.307899, 3.2759342, 22.950727, -8.715704, 3.4878736, 22.713732, -8.328896, 3.4248629, 22.82755, -8.018351, 3.603071, 22.708073, -8.005375, 3.8135586, 22.46657, -7.9505553, 3.661367, 22.623951, -8.018351, 3.603071, 22.708073, -8.361177, 3.876412, 22.41829, -8.005375, 3.8135586, 22.46657, -9.178247, 2.5394235, 23.05604, -8.955492, 3.0269203, 22.925161, -9.159386, 2.5504227, 23.109524, -8.955492, 3.0269203, 22.925161, -9.178247, 2.5394235, 23.05604, -9.064192, 2.7795045, 22.919472, -9.064192, 2.7795045, 22.919472, -9.178247, 2.5394235, 23.05604, -9.110924, 2.6796026, 22.880608, -8.955492, 3.0269203, 22.925161, -8.875946, 3.1743643, 22.591955, -8.715704, 3.4878736, 22.713732, -8.955492, 3.0269203, 22.925161, -8.9538765, 3.0363877, 22.650652, -8.875946, 3.1743643, 22.591955, -8.822695, 3.2558434, 22.270594, -8.361177, 3.876412, 22.41829, -8.715704, 3.4878736, 22.713732, -8.361177, 3.876412, 22.41829, -8.822695, 3.2558434, 22.270594, -8.668564, 3.40597, 22.14398, -8.361177, 3.876412, 22.41829, -8.668564, 3.40597, 22.14398, -8.56176, 3.58513, 22.231379, -8.250622, 3.8801494, 22.13827, -8.006098, 4.0689516, 22.13492, -8.361177, 3.876412, 22.41829, -8.006098, 4.0689516, 22.13492, -8.250622, 3.8801494, 22.13827, -8.250301, 3.7793045, 21.885279, -8.006098, 4.0689516, 22.13492, -8.250301, 3.7793045, 21.885279, -8.1642685, 3.8312206, 21.842627, -8.361177, 3.876412, 22.41829, -8.328896, 3.4248629, 22.82755, -8.715704, 3.4878736, 22.713732, -8.302567, 3.6759555, 22.615335, -8.328896, 3.4248629, 22.82755, -8.361177, 3.876412, 22.41829, -8.328896, 3.4248629, 22.82755, -8.302567, 3.6759555, 22.615335, -8.126291, 3.4355674, 22.803347, -8.328896, 3.4248629, 22.82755, -8.126291, 3.4355674, 22.803347, -8.196812, 3.379901, 22.85993, -8.361177, 3.876412, 22.41829, -8.094337, 3.5251052, 22.770916, -8.302567, 3.6759555, 22.615335, -8.361177, 3.876412, 22.41829, -8.018351, 3.603071, 22.708073, -8.094337, 3.5251052, 22.770916, -8.56176, 3.58513, 22.231379, -8.532204, 3.5644715, 22.029161, -8.361177, 3.876412, 22.41829, -8.56176, 3.58513, 22.231379, -8.598177, 3.497055, 22.083708, -8.532204, 3.5644715, 22.029161, -8.006098, 4.0689516, 22.13492, -8.005375, 3.8135586, 22.46657, -8.361177, 3.876412, 22.41829, -8.005375, 3.8135586, 22.46657, -8.006098, 4.0689516, 22.13492, -7.756726, 3.8452916, 22.38111, -8.005375, 3.8135586, 22.46657, -7.756726, 3.8452916, 22.38111, -7.852491, 3.7639642, 22.499866, -8.250622, 3.8801494, 22.13827, -8.532204, 3.5644715, 22.029161, -8.306704, 3.6845965, 21.918798, -8.250622, 3.8801494, 22.13827, -8.361177, 3.876412, 22.41829, -8.532204, 3.5644715, 22.029161, -7.640471, 4.0882125, 21.854998, -7.7926917, 3.9996805, 22.145943, -8.006098, 4.0689516, 22.13492, -7.7926917, 3.9996805, 22.145943, -7.640471, 4.0882125, 21.854998, -7.576958, 4.04729, 21.902573, -7.7926917, 3.9996805, 22.145943, -7.576958, 4.04729, 21.902573, -7.6565347, 3.9742813, 22.111242, -8.875946, 3.1743643, 22.591955, -8.822695, 3.2558434, 22.270594, -8.715704, 3.4878736, 22.713732, -8.875946, 3.1743643, 22.591955, -8.895857, 3.1287508, 22.405542, -8.822695, 3.2558434, 22.270594, -8.9538765, 3.0363877, 22.650652, -8.940609, 3.017987, 22.493166, -8.875946, 3.1743643, 22.591955, -8.9538765, 3.0363877, 22.650652, -8.9667015, 2.9863772, 22.539291, -8.940609, 3.017987, 22.493166, -8.9538765, 3.0363877, 22.650652, -8.955492, 3.0269203, 22.925161, -9.018259, 2.9038734, 22.63329, -9.064192, 2.7795045, 22.919472, -9.018259, 2.9038734, 22.63329, -8.955492, 3.0269203, 22.925161, -9.064192, 2.7795045, 22.919472, -9.078123, 2.7668862, 22.79162, -9.018259, 2.9038734, 22.63329, -8.007105, 4.2432814, 21.79742, -7.6102357, 3.9935734, 21.830496, -7.6156516, 4.0593348, 21.890636, -8.007105, 4.2432814, 21.79742, -8.03323, 3.9421675, 21.519663, -7.6102357, 3.9935734, 21.830496, -8.098074, 4.0979156, 22.171206, -7.9227233, 4.142306, 21.975924, -7.9388556, 4.0766134, 22.104975, -8.098074, 4.0979156, 22.171206, -8.007105, 4.2432814, 21.79742, -7.9227233, 4.142306, 21.975924, -9.771751, 3.535802, 21.75793, -9.379068, 3.8438268, 21.68758, -9.350674, 3.7584422, 21.997433, -9.771751, 3.535802, 21.75793, -9.776064, 3.537459, 21.700321, -9.379068, 3.8438268, 21.68758, -9.379068, 3.8438268, 21.68758, -9.238958, 3.8097954, 22.04487, -9.350674, 3.7584422, 21.997433, -9.379068, 3.8438268, 21.68758, -9.184797, 3.91052, 21.871664, -9.238958, 3.8097954, 22.04487, -9.184797, 3.91052, 21.871664, -9.379068, 3.8438268, 21.68758, -8.955339, 4.1180353, 21.68752, -9.184797, 3.91052, 21.871664, -8.955339, 4.1180353, 21.68752, -8.938385, 3.9436283, 22.172657, -9.184797, 3.91052, 21.871664, -8.938385, 3.9436283, 22.172657, -9.138344, 3.8554869, 22.087576, -8.938385, 3.9436283, 22.172657, -8.830858, 4.0307946, 22.063833, -8.879677, 3.9519649, 22.17815, -8.938385, 3.9436283, 22.172657, -8.955339, 4.1180353, 21.68752, -8.830858, 4.0307946, 22.063833, -8.525064, 4.0757513, 22.206758, -8.3042145, 4.1594357, 22.040773, -8.414618, 4.077264, 22.197893, -8.525064, 4.0757513, 22.206758, -8.445869, 4.2673163, 21.729216, -8.3042145, 4.1594357, 22.040773, -9.761192, 3.5089111, 21.653206, -9.379068, 3.8438268, 21.68758, -9.776064, 3.537459, 21.700321, -9.379068, 3.8438268, 21.68758, -9.761192, 3.5089111, 21.653206, -9.529911, 3.6422298, 21.627573, -9.529911, 3.6422298, 21.627573, -9.761192, 3.5089111, 21.653206, -9.567165, 3.544581, 21.576538, -9.379068, 3.8438268, 21.68758, -9.097656, 3.8193562, 21.5222, -8.955339, 4.1180353, 21.68752, -9.379068, 3.8438268, 21.68758, -9.2248955, 3.7364876, 21.514492, -9.097656, 3.8193562, 21.5222, -8.856268, 3.7445884, 21.349636, -8.445869, 4.2673163, 21.729216, -8.955339, 4.1180353, 21.68752, -8.445869, 4.2673163, 21.729216, -8.856268, 3.7445884, 21.349636, -8.63674, 3.7917051, 21.369219, -8.445869, 4.2673163, 21.729216, -8.63674, 3.7917051, 21.369219, -8.568174, 3.9699275, 21.489882, -8.232094, 4.130494, 21.637142, -8.007105, 4.2432814, 21.79742, -8.445869, 4.2673163, 21.729216, -8.007105, 4.2432814, 21.79742, -8.232094, 4.130494, 21.637142, -8.128689, 3.9327025, 21.48767, -8.007105, 4.2432814, 21.79742, -8.128689, 3.9327025, 21.48767, -8.03323, 3.9421675, 21.519663, -8.445869, 4.2673163, 21.729216, -8.830858, 4.0307946, 22.063833, -8.955339, 4.1180353, 21.68752, -8.606184, 4.161176, 21.922972, -8.830858, 4.0307946, 22.063833, -8.445869, 4.2673163, 21.729216, -8.830858, 4.0307946, 22.063833, -8.606184, 4.161176, 21.922972, -8.680071, 3.982471, 22.196703, -8.830858, 4.0307946, 22.063833, -8.680071, 3.982471, 22.196703, -8.7803135, 3.9735296, 22.187021, -8.445869, 4.2673163, 21.729216, -8.633348, 4.0487413, 22.19735, -8.606184, 4.161176, 21.922972, -8.445869, 4.2673163, 21.729216, -8.525064, 4.0757513, 22.206758, -8.633348, 4.0487413, 22.19735, -8.568174, 3.9699275, 21.489882, -8.441978, 3.8581774, 21.385015, -8.445869, 4.2673163, 21.729216, -8.568174, 3.9699275, 21.489882, -8.53607, 3.8349931, 21.376839, -8.441978, 3.8581774, 21.385015, -8.007105, 4.2432814, 21.79742, -8.3042145, 4.1594357, 22.040773, -8.445869, 4.2673163, 21.729216, -8.3042145, 4.1594357, 22.040773, -8.007105, 4.2432814, 21.79742, -8.098074, 4.0979156, 22.171206, -8.3042145, 4.1594357, 22.040773, -8.098074, 4.0979156, 22.171206, -8.254183, 4.096847, 22.18368, -8.232094, 4.130494, 21.637142, -8.441978, 3.8581774, 21.385015, -8.191664, 3.867425, 21.469576, -8.232094, 4.130494, 21.637142, -8.445869, 4.2673163, 21.729216, -8.441978, 3.8581774, 21.385015, -7.6156516, 4.0593348, 21.890636, -7.9227233, 4.142306, 21.975924, -8.007105, 4.2432814, 21.79742, -7.9227233, 4.142306, 21.975924, -7.6156516, 4.0593348, 21.890636, -7.62472, 4.0303593, 21.974613, -7.9227233, 4.142306, 21.975924, -7.62472, 4.0303593, 21.974613, -7.833011, 4.0754824, 22.059702, -9.097656, 3.8193562, 21.5222, -8.856268, 3.7445884, 21.349636, -8.955339, 4.1180353, 21.68752, -9.097656, 3.8193562, 21.5222, -9.020979, 3.7032843, 21.39327, -8.856268, 3.7445884, 21.349636, -9.2248955, 3.7364876, 21.514492, -9.123741, 3.6469016, 21.423038, -9.097656, 3.8193562, 21.5222, -9.2248955, 3.7364876, 21.514492, -9.181778, 3.6451564, 21.437378, -9.123741, 3.6469016, 21.423038, -9.2248955, 3.7364876, 21.514492, -9.379068, 3.8438268, 21.68758, -9.29739, 3.622695, 21.467472, -9.529911, 3.6422298, 21.627573, -9.29739, 3.622695, 21.467472, -9.379068, 3.8438268, 21.68758, -9.529911, 3.6422298, 21.627573, -9.471037, 3.5795534, 21.537083, -9.29739, 3.622695, 21.467472, -7.801645, 4.134115, 21.58577, -7.6006274, 4.02651, 21.917599, -7.6581864, 4.072356, 21.931242, -7.801645, 4.134115, 21.58577, -7.559606, 3.912532, 21.485746, -7.6006274, 4.02651, 21.917599, -8.072794, 3.9650502, 21.715675, -7.866752, 4.057515, 21.744825, -7.952191, 3.99135, 21.80066, -8.072794, 3.9650502, 21.715675, -7.801645, 4.134115, 21.58577, -7.866752, 4.057515, 21.744825, -8.445419, 3.1368337, 20.389019, -8.276415, 3.4964035, 20.596956, -8.461891, 3.4085104, 20.773108, -8.445419, 3.1368337, 20.389019, -8.407898, 3.1417727, 20.358862, -8.276415, 3.4964035, 20.596956, -8.276415, 3.4964035, 20.596956, -8.453417, 3.4740689, 20.868896, -8.461891, 3.4085104, 20.773108, -8.276415, 3.4964035, 20.596956, -8.327274, 3.583331, 20.814444, -8.453417, 3.4740689, 20.868896, -8.327274, 3.583331, 20.814444, -8.276415, 3.4964035, 20.596956, -8.134125, 3.821342, 20.866423, -8.327274, 3.583331, 20.814444, -8.134125, 3.821342, 20.866423, -8.4303055, 3.644507, 21.12673, -8.327274, 3.583331, 20.814444, -8.4303055, 3.644507, 21.12673, -8.445759, 3.5321927, 20.955154, -8.4303055, 3.644507, 21.12673, -8.320477, 3.7492816, 21.144423, -8.409815, 3.665008, 21.169518, -8.4303055, 3.644507, 21.12673, -8.134125, 3.821342, 20.866423, -8.320477, 3.7492816, 21.144423, -8.287462, 3.8450718, 21.426939, -8.086512, 3.9769306, 21.499489, -8.231916, 3.8731332, 21.501694, -8.287462, 3.8450718, 21.426939, -7.9577355, 4.0573144, 21.235031, -8.086512, 3.9769306, 21.499489, -8.362757, 3.127033, 20.352093, -8.276415, 3.4964035, 20.596956, -8.407898, 3.1417727, 20.358862, -8.276415, 3.4964035, 20.596956, -8.362757, 3.127033, 20.352093, -8.265045, 3.3001463, 20.48862, -8.265045, 3.3001463, 20.48862, -8.362757, 3.127033, 20.352093, -8.229216, 3.2149067, 20.450375, -8.276415, 3.4964035, 20.596956, -8.026518, 3.5625966, 20.730312, -8.134125, 3.821342, 20.866423, -8.276415, 3.4964035, 20.596956, -8.06389, 3.4651048, 20.64575, -8.026518, 3.5625966, 20.730312, -7.781936, 3.5772128, 20.836325, -7.9577355, 4.0573144, 21.235031, -8.134125, 3.821342, 20.866423, -7.9577355, 4.0573144, 21.235031, -7.781936, 3.5772128, 20.836325, -7.705567, 3.6661596, 20.99604, -7.9577355, 4.0573144, 21.235031, -7.705567, 3.6661596, 20.99604, -7.7892184, 3.8130858, 21.079828, -7.7676377, 4.0055275, 21.369812, -7.801645, 4.134115, 21.58577, -7.9577355, 4.0573144, 21.235031, -7.801645, 4.134115, 21.58577, -7.7676377, 4.0055275, 21.369812, -7.5783267, 3.8855352, 21.404331, -7.801645, 4.134115, 21.58577, -7.5783267, 3.8855352, 21.404331, -7.559606, 3.912532, 21.485746, -7.9577355, 4.0573144, 21.235031, -8.320477, 3.7492816, 21.144423, -8.134125, 3.821342, 20.866423, -8.143465, 3.9182801, 21.225508, -8.320477, 3.7492816, 21.144423, -7.9577355, 4.0573144, 21.235031, -8.320477, 3.7492816, 21.144423, -8.143465, 3.9182801, 21.225508, -8.340118, 3.7362518, 21.314922, -8.320477, 3.7492816, 21.144423, -8.340118, 3.7362518, 21.314922, -8.375259, 3.7055726, 21.241833, -7.9577355, 4.0573144, 21.235031, -8.325062, 3.7988772, 21.348167, -8.143465, 3.9182801, 21.225508, -7.9577355, 4.0573144, 21.235031, -8.287462, 3.8450718, 21.426939, -8.325062, 3.7988772, 21.348167, -7.7892184, 3.8130858, 21.079828, -7.6383195, 3.7634745, 21.137505, -7.9577355, 4.0573144, 21.235031, -7.7892184, 3.8130858, 21.079828, -7.6709933, 3.7234988, 21.069057, -7.6383195, 3.7634745, 21.137505, -7.801645, 4.134115, 21.58577, -8.086512, 3.9769306, 21.499489, -7.9577355, 4.0573144, 21.235031, -8.086512, 3.9769306, 21.499489, -7.801645, 4.134115, 21.58577, -8.072794, 3.9650502, 21.715675, -8.086512, 3.9769306, 21.499489, -8.072794, 3.9650502, 21.715675, -8.151386, 3.9267366, 21.61004, -7.7676377, 4.0055275, 21.369812, -7.6383195, 3.7634745, 21.137505, -7.588919, 3.823454, 21.350895, -7.7676377, 4.0055275, 21.369812, -7.9577355, 4.0573144, 21.235031, -7.6383195, 3.7634745, 21.137505, -7.6581864, 4.072356, 21.931242, -7.866752, 4.057515, 21.744825, -7.801645, 4.134115, 21.58577, -7.866752, 4.057515, 21.744825, -7.6581864, 4.072356, 21.931242, -7.71435, 4.039794, 21.968426, -7.866752, 4.057515, 21.744825, -7.71435, 4.039794, 21.968426, -7.87205, 4.018237, 21.856892, -8.026518, 3.5625966, 20.730312, -7.781936, 3.5772128, 20.836325, -8.134125, 3.821342, 20.866423, -8.026518, 3.5625966, 20.730312, -7.8797107, 3.5000882, 20.74282, -7.781936, 3.5772128, 20.836325, -8.06389, 3.4651048, 20.64575, -7.94037, 3.4260588, 20.685284, -8.026518, 3.5625966, 20.730312, -8.06389, 3.4651048, 20.64575, -7.975001, 3.40911, 20.652027, -7.94037, 3.4260588, 20.685284, -8.06389, 3.4651048, 20.64575, -8.276415, 3.4964035, 20.596956, -8.0436325, 3.3592296, 20.586334, -8.265045, 3.3001463, 20.48862, -8.0436325, 3.3592296, 20.586334, -8.276415, 3.4964035, 20.596956, -8.265045, 3.3001463, 20.48862, -8.163176, 3.2732167, 20.498547, -8.0436325, 3.3592296, 20.586334, -7.402509, 4.1859736, 21.63811, -7.6918817, 4.041251, 21.991617, -7.7346835, 4.0696325, 21.918596, -7.402509, 4.1859736, 21.63811, -7.1659408, 4.0452685, 21.938877, -7.6918817, 4.041251, 21.991617, -7.544909, 3.8685284, 21.406876, -7.563997, 4.036748, 21.631306, -7.6227355, 3.9156742, 21.563887, -7.544909, 3.8685284, 21.406876, -7.402509, 4.1859736, 21.63811, -7.563997, 4.036748, 21.631306, -5.9238067, 3.1665137, 20.728191, -6.2251096, 3.5616388, 20.915148, -6.4278474, 3.3525786, 20.777048, -5.9238067, 3.1665137, 20.728191, -5.8848615, 3.193083, 20.760527, -6.2251096, 3.5616388, 20.915148, -6.2251096, 3.5616388, 20.915148, -6.5504594, 3.402594, 20.803858, -6.4278474, 3.3525786, 20.777048, -6.2251096, 3.5616388, 20.915148, -6.4983582, 3.5830135, 20.909115, -6.5504594, 3.402594, 20.803858, -6.4983582, 3.5830135, 20.909115, -6.2251096, 3.5616388, 20.915148, -6.5891337, 3.903202, 21.097126, -6.4983582, 3.5830135, 20.909115, -6.5891337, 3.903202, 21.097126, -6.879663, 3.532806, 20.876286, -6.4983582, 3.5830135, 20.909115, -6.879663, 3.532806, 20.876286, -6.6607127, 3.447071, 20.828083, -6.879663, 3.532806, 20.876286, -6.9066834, 3.6857386, 20.988258, -6.9293284, 3.5507002, 20.90792, -6.879663, 3.532806, 20.876286, -6.5891337, 3.903202, 21.097126, -6.9066834, 3.6857386, 20.988258, -7.2366023, 3.7330508, 21.09622, -7.3143077, 3.9347124, 21.32243, -7.315949, 3.763792, 21.176712, -7.2366023, 3.7330508, 21.09622, -7.02702, 4.1361513, 21.366934, -7.3143077, 3.9347124, 21.32243, -5.862766, 3.1931438, 20.813963, -6.2251096, 3.5616388, 20.915148, -5.8848615, 3.193083, 20.760527, -6.2251096, 3.5616388, 20.915148, -5.862766, 3.1931438, 20.813963, -6.043155, 3.376487, 20.935175, -6.043155, 3.376487, 20.935175, -5.862766, 3.1931438, 20.813963, -5.9688907, 3.3041463, 20.98022, -6.2251096, 3.5616388, 20.915148, -6.3340783, 3.6836305, 21.23523, -6.5891337, 3.903202, 21.097126, -6.2251096, 3.5616388, 20.915148, -6.2198997, 3.5840178, 21.185366, -6.3340783, 3.6836305, 21.23523, -6.400766, 3.7538157, 21.548512, -7.02702, 4.1361513, 21.366934, -6.5891337, 3.903202, 21.097126, -7.02702, 4.1361513, 21.366934, -6.400766, 3.7538157, 21.548512, -6.588054, 3.8369021, 21.666204, -7.02702, 4.1361513, 21.366934, -6.588054, 3.8369021, 21.666204, -6.7412972, 3.9567418, 21.567703, -7.1137557, 4.110402, 21.642786, -7.402509, 4.1859736, 21.63811, -7.02702, 4.1361513, 21.366934, -7.402509, 4.1859736, 21.63811, -7.1137557, 4.110402, 21.642786, -7.0714173, 4.029825, 21.898872, -7.402509, 4.1859736, 21.63811, -7.0714173, 4.029825, 21.898872, -7.1659408, 4.0452685, 21.938877, -7.02702, 4.1361513, 21.366934, -6.9066834, 3.6857386, 20.988258, -6.5891337, 3.903202, 21.097126, -7.0096154, 3.9184318, 21.182732, -6.9066834, 3.6857386, 20.988258, -7.02702, 4.1361513, 21.366934, -6.9066834, 3.6857386, 20.988258, -7.0096154, 3.9184318, 21.182732, -7.098456, 3.6138377, 21.015465, -6.9066834, 3.6857386, 20.988258, -7.098456, 3.6138377, 21.015465, -7.014134, 3.5885832, 20.961206, -7.02702, 4.1361513, 21.366934, -7.1437945, 3.6882315, 21.038242, -7.0096154, 3.9184318, 21.182732, -7.02702, 4.1361513, 21.366934, -7.2366023, 3.7330508, 21.09622, -7.1437945, 3.6882315, 21.038242, -6.7412972, 3.9567418, 21.567703, -6.756685, 3.9355235, 21.769691, -7.02702, 4.1361513, 21.366934, -6.7412972, 3.9567418, 21.567703, -6.6761546, 3.8968792, 21.71939, -6.756685, 3.9355235, 21.769691, -7.402509, 4.1859736, 21.63811, -7.3143077, 3.9347124, 21.32243, -7.02702, 4.1361513, 21.366934, -7.3143077, 3.9347124, 21.32243, -7.402509, 4.1859736, 21.63811, -7.544909, 3.8685284, 21.406876, -7.3143077, 3.9347124, 21.32243, -7.544909, 3.8685284, 21.406876, -7.432817, 3.8261418, 21.293098, -7.1137557, 4.110402, 21.642786, -6.756685, 3.9355235, 21.769691, -7.002805, 3.9603796, 21.87517, -7.1137557, 4.110402, 21.642786, -7.02702, 4.1361513, 21.366934, -6.756685, 3.9355235, 21.769691, -7.7346835, 4.0696325, 21.918596, -7.563997, 4.036748, 21.631306, -7.402509, 4.1859736, 21.63811, -7.563997, 4.036748, 21.631306, -7.7346835, 4.0696325, 21.918596, -7.775924, 4.0044317, 21.873682, -7.563997, 4.036748, 21.631306, -7.775924, 4.0044317, 21.873682, -7.6754994, 3.960377, 21.667994, -6.3340783, 3.6836305, 21.23523, -6.400766, 3.7538157, 21.548512, -6.5891337, 3.903202, 21.097126, -6.3340783, 3.6836305, 21.23523, -6.2958436, 3.6586976, 21.422325, -6.400766, 3.7538157, 21.548512, -6.2198997, 3.5840178, 21.185366, -6.2276845, 3.568183, 21.344408, -6.3340783, 3.6836305, 21.23523, -6.2198997, 3.5840178, 21.185366, -6.1918397, 3.5477042, 21.29962, -6.2276845, 3.568183, 21.344408, -6.2198997, 3.5840178, 21.185366, -6.2251096, 3.5616388, 20.915148, -6.1187816, 3.4875863, 21.210886, -6.043155, 3.376487, 20.935175, -6.1187816, 3.4875863, 21.210886, -6.2251096, 3.5616388, 20.915148, -6.043155, 3.376487, 20.935175, -6.0228896, 3.3768027, 21.062256, -6.1187816, 3.4875863, 21.210886, -7.5309367, 4.1860266, 22.233109, -7.7731357, 4.049283, 21.837759, -7.686767, 4.0676775, 21.824697, -7.5309367, 4.1860266, 22.233109, -7.90918, 4.0913115, 22.343561, -7.7731357, 4.049283, 21.837759, -7.2945504, 3.8389473, 22.203506, -7.4850507, 4.0242515, 22.093157, -7.4099345, 3.8931296, 22.069782, -7.2945504, 3.8389473, 22.203506, -7.5309367, 4.1860266, 22.233109, -7.4850507, 4.0242515, 22.093157, -7.231361, 3.2090497, 23.990551, -7.2794967, 3.594735, 23.623615, -7.099934, 3.3586783, 23.498016, -7.231361, 3.2090497, 23.990551, -7.2722163, 3.2413416, 24.013079, -7.2794967, 3.594735, 23.623615, -7.2794967, 3.594735, 23.623615, -7.0818753, 3.401124, 23.37233, -7.099934, 3.3586783, 23.498016, -7.2794967, 3.594735, 23.623615, -7.1849923, 3.5940344, 23.37246, -7.0818753, 3.401124, 23.37233, -7.1849923, 3.5940344, 23.37246, -7.2794967, 3.594735, 23.623615, -7.3083177, 3.9218946, 23.202938, -7.1849923, 3.5940344, 23.37246, -7.3083177, 3.9218946, 23.202938, -7.0331464, 3.5110173, 23.034817, -7.1849923, 3.5940344, 23.37246, -7.0331464, 3.5110173, 23.034817, -7.0656457, 3.4388018, 23.25929, -7.0331464, 3.5110173, 23.034817, -7.121306, 3.6710267, 22.96056, -7.043958, 3.527542, 22.976107, -7.0331464, 3.5110173, 23.034817, -7.3083177, 3.9218946, 23.202938, -7.121306, 3.6710267, 22.96056, -7.113857, 3.7015529, 22.612543, -7.287684, 3.915894, 22.445786, -7.160318, 3.7328143, 22.507229, -7.113857, 3.7015529, 22.612543, -7.4040046, 4.1427026, 22.68545, -7.287684, 3.915894, 22.445786, -7.3301635, 3.2478483, 24.013186, -7.2794967, 3.594735, 23.623615, -7.2722163, 3.2413416, 24.013079, -7.2794967, 3.594735, 23.623615, -7.3301635, 3.2478483, 24.013186, -7.371898, 3.4266107, 23.792282, -7.371898, 3.4266107, 23.792282, -7.3301635, 3.2478483, 24.013186, -7.442037, 3.3642597, 23.847004, -7.2794967, 3.594735, 23.623615, -7.540323, 3.735655, 23.39579, -7.3083177, 3.9218946, 23.202938, -7.2794967, 3.594735, 23.623615, -7.5373693, 3.641142, 23.524654, -7.540323, 3.735655, 23.39579, -7.8095174, 3.8275523, 23.211908, -7.4040046, 4.1427026, 22.68545, -7.3083177, 3.9218946, 23.202938, -7.4040046, 4.1427026, 22.68545, -7.8095174, 3.8275523, 23.211908, -7.8509803, 3.9055398, 22.990408, -7.4040046, 4.1427026, 22.68545, -7.8509803, 3.9055398, 22.990408, -7.703343, 4.004422, 22.88101, -7.6422763, 4.1345253, 22.501507, -7.5309367, 4.1860266, 22.233109, -7.4040046, 4.1427026, 22.68545, -7.5309367, 4.1860266, 22.233109, -7.6422763, 4.1345253, 22.501507, -7.905112, 4.080037, 22.446768, -7.5309367, 4.1860266, 22.233109, -7.905112, 4.080037, 22.446768, -7.90918, 4.0913115, 22.343561, -7.4040046, 4.1427026, 22.68545, -7.121306, 3.6710267, 22.96056, -7.3083177, 3.9218946, 23.202938, -7.255689, 3.9115663, 22.781116, -7.121306, 3.6710267, 22.96056, -7.4040046, 4.1427026, 22.68545, -7.121306, 3.6710267, 22.96056, -7.255689, 3.9115663, 22.781116, -7.080893, 3.5860498, 22.77619, -7.121306, 3.6710267, 22.96056, -7.080893, 3.5860498, 22.77619, -7.062715, 3.563114, 22.87582, -7.4040046, 4.1427026, 22.68545, -7.0931907, 3.6592827, 22.722273, -7.255689, 3.9115663, 22.781116, -7.4040046, 4.1427026, 22.68545, -7.113857, 3.7015529, 22.612543, -7.0931907, 3.6592827, 22.722273, -7.703343, 4.004422, 22.88101, -7.8892875, 3.9995782, 22.79086, -7.4040046, 4.1427026, 22.68545, -7.703343, 4.004422, 22.88101, -7.8713326, 3.9631367, 22.88616, -7.8892875, 3.9995782, 22.79086, -7.5309367, 4.1860266, 22.233109, -7.287684, 3.915894, 22.445786, -7.4040046, 4.1427026, 22.68545, -7.287684, 3.915894, 22.445786, -7.5309367, 4.1860266, 22.233109, -7.2945504, 3.8389473, 22.203506, -7.287684, 3.915894, 22.445786, -7.2945504, 3.8389473, 22.203506, -7.2290454, 3.7958574, 22.35221, -7.6422763, 4.1345253, 22.501507, -7.8892875, 3.9995782, 22.79086, -7.8991284, 4.0135202, 22.522274, -7.6422763, 4.1345253, 22.501507, -7.4040046, 4.1427026, 22.68545, -7.8892875, 3.9995782, 22.79086, -7.686767, 4.0676775, 21.824697, -7.4850507, 4.0242515, 22.093157, -7.5309367, 4.1860266, 22.233109, -7.4850507, 4.0242515, 22.093157, -7.686767, 4.0676775, 21.824697, -7.63722, 3.9957073, 21.806574, -7.4850507, 4.0242515, 22.093157, -7.63722, 3.9957073, 21.806574, -7.487868, 3.9425092, 21.979464, -7.540323, 3.735655, 23.39579, -7.8095174, 3.8275523, 23.211908, -7.3083177, 3.9218946, 23.202938, -7.540323, 3.735655, 23.39579, -7.7316575, 3.7301946, 23.361069, -7.8095174, 3.8275523, 23.211908, -7.5373693, 3.641142, 23.524654, -7.680619, 3.6382384, 23.457644, -7.540323, 3.735655, 23.39579, -7.5373693, 3.641142, 23.524654, -7.6541924, 3.6169612, 23.508732, -7.680619, 3.6382384, 23.457644, -7.5373693, 3.641142, 23.524654, -7.2794967, 3.594735, 23.623615, -7.599995, 3.5552611, 23.612686, -7.371898, 3.4266107, 23.792282, -7.599995, 3.5552611, 23.612686, -7.2794967, 3.594735, 23.623615, -7.371898, 3.4266107, 23.792282, -7.4989257, 3.4396398, 23.762728, -7.599995, 3.5552611, 23.612686, -6.9064727, 0.8870442, 21.944698, -7.0634227, 1.9859911, 21.955528, -6.983601, 1.9958726, 21.857681, -6.9064727, 0.8870442, 21.944698, -7.0050845, 0.88108003, 22.061377, -7.0634227, 1.9859911, 21.955528, -7.007685, -0.32457638, 22.15034, -7.0050845, 0.88108003, 22.061377, -6.9064727, 0.8870442, 21.944698, -7.007685, -0.32457638, 22.15034, -7.116863, -0.30735517, 22.281223, -7.0050845, 0.88108003, 22.061377, -7.2671094, 0.85050464, 21.996153, -7.3045907, 1.9478768, 21.77239, -7.284966, 1.9531578, 21.8974, -7.2671094, 0.85050464, 21.996153, -7.288546, 0.84261394, 21.850367, -7.3045907, 1.9478768, 21.77239, -7.0425286, 0.86357975, 21.707298, -7.003896, 1.9904655, 21.73263, -7.101132, 1.9756031, 21.652607, -7.0425286, 0.86357975, 21.707298, -6.925528, 0.87903726, 21.79757, -7.003896, 1.9904655, 21.73263, -7.284966, 1.9531578, 21.8974, -7.4806666, 2.9942842, 21.815382, -7.466662, 2.9961905, 21.89844, -7.284966, 1.9531578, 21.8974, -7.3045907, 1.9478768, 21.77239, -7.4806666, 2.9942842, 21.815382, -6.983601, 1.9958726, 21.857681, -7.320312, 3.0230846, 21.937897, -7.2666173, 3.0324101, 21.872292, -6.983601, 1.9958726, 21.857681, -7.0634227, 1.9859911, 21.955528, -7.320312, 3.0230846, 21.937897, -7.2666173, 3.0324101, 21.872292, -7.320312, 3.0230846, 21.937897, -7.4912467, 3.573497, 21.949327, -7.2666173, 3.0324101, 21.872292, -7.4912467, 3.573497, 21.949327, -7.453357, 3.5873647, 21.900778, -7.0050845, 0.88108003, 22.061377, -7.1874385, 1.968081, 21.976803, -7.0634227, 1.9859911, 21.955528, -7.0050845, 0.88108003, 22.061377, -7.15249, 0.8660785, 22.087767, -7.1874385, 1.968081, 21.976803, -7.0634227, 1.9859911, 21.955528, -7.1874385, 1.968081, 21.976803, -7.40249, 3.0077667, 21.95179, -7.0634227, 1.9859911, 21.955528, -7.40249, 3.0077667, 21.95179, -7.320312, 3.0230846, 21.937897, -7.288546, 0.84261394, 21.850367, -7.2247424, 1.9581697, 21.674047, -7.3045907, 1.9478768, 21.77239, -7.288546, 0.84261394, 21.850367, -7.192162, 0.8488163, 21.734951, -7.2247424, 1.9581697, 21.674047, -7.3045907, 1.9478768, 21.77239, -7.2247424, 1.9581697, 21.674047, -7.428226, 3.0044737, 21.749603, -7.3045907, 1.9478768, 21.77239, -7.428226, 3.0044737, 21.749603, -7.4806666, 2.9942842, 21.815382, -7.101132, 1.9756031, 21.652607, -7.2801204, 3.029789, 21.78821, -7.345489, 3.018808, 21.73467, -7.101132, 1.9756031, 21.652607, -7.003896, 1.9904655, 21.73263, -7.2801204, 3.029789, 21.78821, -6.925528, 0.87903726, 21.79757, -6.983601, 1.9958726, 21.857681, -7.003896, 1.9904655, 21.73263, -6.925528, 0.87903726, 21.79757, -6.9064727, 0.8870442, 21.944698, -6.983601, 1.9958726, 21.857681, -7.003896, 1.9904655, 21.73263, -7.2666173, 3.0324101, 21.872292, -7.2801204, 3.029789, 21.78821, -7.003896, 1.9904655, 21.73263, -6.983601, 1.9958726, 21.857681, -7.2666173, 3.0324101, 21.872292, -7.192162, 0.8488163, 21.734951, -7.101132, 1.9756031, 21.652607, -7.2247424, 1.9581697, 21.674047, -7.192162, 0.8488163, 21.734951, -7.0425286, 0.86357975, 21.707298, -7.101132, 1.9756031, 21.652607, -7.3367667, -0.3452804, 21.89752, -7.0425286, 0.86357975, 21.707298, -7.192162, 0.8488163, 21.734951, -7.3367667, -0.3452804, 21.89752, -7.1676164, -0.3521583, 21.871283, -7.0425286, 0.86357975, 21.707298, -7.2247424, 1.9581697, 21.674047, -7.345489, 3.018808, 21.73467, -7.428226, 3.0044737, 21.749603, -7.2247424, 1.9581697, 21.674047, -7.101132, 1.9756031, 21.652607, -7.345489, 3.018808, 21.73467, -7.428226, 3.0044737, 21.749603, -7.345489, 3.018808, 21.73467, -7.5119376, 3.577262, 21.798561, -7.428226, 3.0044737, 21.749603, -7.5119376, 3.577262, 21.798561, -7.5716386, 3.5596325, 21.809055, -7.5982623, 3.5452437, 21.919594, -7.6945357, 3.969666, 21.982683, -7.5509553, 3.5562305, 21.959482, -7.5982623, 3.5452437, 21.919594, -7.7289567, 3.960751, 21.953426, -7.6945357, 3.969666, 21.982683, -7.4912467, 3.573497, 21.949327, -7.6261945, 4.0009108, 21.940428, -7.453357, 3.5873647, 21.900778, -7.4912467, 3.573497, 21.949327, -7.6525865, 3.9872801, 21.975618, -7.6261945, 4.0009108, 21.940428, -7.5509553, 3.5562305, 21.959482, -7.6525865, 3.9872801, 21.975618, -7.4912467, 3.573497, 21.949327, -7.5509553, 3.5562305, 21.959482, -7.6945357, 3.969666, 21.982683, -7.6525865, 3.9872801, 21.975618, -7.4642043, 3.588051, 21.838531, -7.668904, 3.9935448, 21.865906, -7.5119376, 3.577262, 21.798561, -7.4642043, 3.588051, 21.838531, -7.634957, 4.0036774, 21.895311, -7.668904, 3.9935448, 21.865906, -7.453357, 3.5873647, 21.900778, -7.634957, 4.0036774, 21.895311, -7.4642043, 3.588051, 21.838531, -7.453357, 3.5873647, 21.900778, -7.6261945, 4.0009108, 21.940428, -7.634957, 4.0036774, 21.895311, -7.5716386, 3.5596325, 21.809055, -7.737245, 3.9623003, 21.908161, -7.6092744, 3.5461726, 21.857725, -7.5716386, 3.5596325, 21.809055, -7.711328, 3.9771495, 21.873121, -7.737245, 3.9623003, 21.908161, -7.5119376, 3.577262, 21.798561, -7.711328, 3.9771495, 21.873121, -7.5716386, 3.5596325, 21.809055, -7.5119376, 3.577262, 21.798561, -7.668904, 3.9935448, 21.865906, -7.711328, 3.9771495, 21.873121, -7.2869067, -0.3004067, 22.307976, -7.2671094, 0.85050464, 21.996153, -7.15249, 0.8660785, 22.087767, -7.2869067, -0.3004067, 22.307976, -7.4190135, -0.30967426, 22.197882, -7.2671094, 0.85050464, 21.996153, -7.6092744, 3.5461726, 21.857725, -7.7289567, 3.960751, 21.953426, -7.5982623, 3.5452437, 21.919594, -7.6092744, 3.5461726, 21.857725, -7.737245, 3.9623003, 21.908161, -7.7289567, 3.960751, 21.953426, -7.15249, 0.8660785, 22.087767, -7.284966, 1.9531578, 21.8974, -7.1874385, 1.968081, 21.976803, -7.15249, 0.8660785, 22.087767, -7.2671094, 0.85050464, 21.996153, -7.284966, 1.9531578, 21.8974, -7.4190135, -0.30967426, 22.197882, -7.288546, 0.84261394, 21.850367, -7.2671094, 0.85050464, 21.996153, -7.4190135, -0.30967426, 22.197882, -7.446838, -0.32798862, 22.028917, -7.288546, 0.84261394, 21.850367, -7.1676164, -0.3521583, 21.871283, -6.925528, 0.87903726, 21.79757, -7.0425286, 0.86357975, 21.707298, -7.1676164, -0.3521583, 21.871283, -7.036158, -0.3428421, 21.981743, -6.925528, 0.87903726, 21.79757, -7.116863, -0.30735517, 22.281223, -7.15249, 0.8660785, 22.087767, -7.0050845, 0.88108003, 22.061377, -7.116863, -0.30735517, 22.281223, -7.2869067, -0.3004067, 22.307976, -7.15249, 0.8660785, 22.087767, -7.446838, -0.32798862, 22.028917, -7.192162, 0.8488163, 21.734951, -7.288546, 0.84261394, 21.850367, -7.446838, -0.32798862, 22.028917, -7.3367667, -0.3452804, 21.89752, -7.192162, 0.8488163, 21.734951, -7.036158, -0.3428421, 21.981743, -6.9064727, 0.8870442, 21.944698, -6.925528, 0.87903726, 21.79757, -7.036158, -0.3428421, 21.981743, -7.007685, -0.32457638, 22.15034, -6.9064727, 0.8870442, 21.944698, -7.1874385, 1.968081, 21.976803, -7.466662, 2.9961905, 21.89844, -7.40249, 3.0077667, 21.95179, -7.1874385, 1.968081, 21.976803, -7.284966, 1.9531578, 21.8974, -7.466662, 2.9961905, 21.89844, -7.40249, 3.0077667, 21.95179, -7.5982623, 3.5452437, 21.919594, -7.5509553, 3.5562305, 21.959482, -7.40249, 3.0077667, 21.95179, -7.466662, 2.9961905, 21.89844, -7.5982623, 3.5452437, 21.919594, -7.466662, 2.9961905, 21.89844, -7.6092744, 3.5461726, 21.857725, -7.5982623, 3.5452437, 21.919594, -7.466662, 2.9961905, 21.89844, -7.4806666, 2.9942842, 21.815382, -7.6092744, 3.5461726, 21.857725, -7.320312, 3.0230846, 21.937897, -7.5509553, 3.5562305, 21.959482, -7.4912467, 3.573497, 21.949327, -7.320312, 3.0230846, 21.937897, -7.40249, 3.0077667, 21.95179, -7.5509553, 3.5562305, 21.959482, -7.345489, 3.018808, 21.73467, -7.4642043, 3.588051, 21.838531, -7.5119376, 3.577262, 21.798561, -7.345489, 3.018808, 21.73467, -7.2801204, 3.029789, 21.78821, -7.4642043, 3.588051, 21.838531, -7.2801204, 3.029789, 21.78821, -7.453357, 3.5873647, 21.900778, -7.4642043, 3.588051, 21.838531, -7.2801204, 3.029789, 21.78821, -7.2666173, 3.0324101, 21.872292, -7.453357, 3.5873647, 21.900778, -7.4806666, 2.9942842, 21.815382, -7.5716386, 3.5596325, 21.809055, -7.6092744, 3.5461726, 21.857725, -7.4806666, 2.9942842, 21.815382, -7.428226, 3.0044737, 21.749603, -7.5716386, 3.5596325, 21.809055, -25.120148, 13.783545, 12.764626, -26.397978, 13.369994, 12.574373, -26.33375, 13.5643215, 12.432411, -25.120148, 13.783545, 12.764626, -25.263885, 12.827821, 13.423475, -26.397978, 13.369994, 12.574373, -24.933325, 13.512459, 11.685724, -25.424961, 13.642609, 12.2283325, -25.402243, 13.524235, 11.855768, -24.933325, 13.512459, 11.685724, -25.120148, 13.783545, 12.764626, -25.424961, 13.642609, 12.2283325, -20.327318, 10.662638, 12.792898, -21.313543, 11.772419, 13.055622, -21.432056, 11.691162, 12.167927, -20.327318, 10.662638, 12.792898, -20.315731, 10.639986, 12.953346, -21.313543, 11.772419, 13.055622, -21.313543, 11.772419, 13.055622, -21.73093, 11.930523, 12.043638, -21.432056, 11.691162, 12.167927, -21.313543, 11.772419, 13.055622, -21.845343, 12.171588, 12.549276, -21.73093, 11.930523, 12.043638, -21.845343, 12.171588, 12.549276, -21.313543, 11.772419, 13.055622, -22.412296, 12.812631, 13.106188, -21.845343, 12.171588, 12.549276, -22.412296, 12.812631, 13.106188, -22.532686, 12.553142, 11.708666, -21.845343, 12.171588, 12.549276, -22.532686, 12.553142, 11.708666, -21.999557, 12.142736, 11.931597, -22.532686, 12.553142, 11.708666, -22.81894, 12.8182955, 12.022795, -22.69415, 12.618034, 11.694317, -22.532686, 12.553142, 11.708666, -22.412296, 12.812631, 13.106188, -22.81894, 12.8182955, 12.022795, -23.693872, 13.206497, 11.613611, -24.30996, 13.49691, 12.078384, -24.013233, 13.277237, 11.631985, -23.693872, 13.206497, 11.613611, -23.830704, 13.556321, 12.993084, -24.30996, 13.49691, 12.078384, -20.380512, 10.551989, 13.072411, -21.313543, 11.772419, 13.055622, -20.315731, 10.639986, 12.953346, -21.313543, 11.772419, 13.055622, -20.380512, 10.551989, 13.072411, -20.98937, 11.087692, 13.168047, -20.98937, 11.087692, 13.168047, -20.380512, 10.551989, 13.072411, -20.92976, 10.772113, 13.284454, -21.313543, 11.772419, 13.055622, -22.184439, 11.839201, 13.474684, -22.412296, 12.812631, 13.106188, -21.313543, 11.772419, 13.055622, -21.854174, 11.521826, 13.480944, -22.184439, 11.839201, 13.474684, -22.953316, 11.729006, 13.905809, -23.830704, 13.556321, 12.993084, -22.412296, 12.812631, 13.106188, -23.830704, 13.556321, 12.993084, -22.953316, 11.729006, 13.905809, -23.56244, 12.014381, 13.853283, -23.830704, 13.556321, 12.993084, -23.56244, 12.014381, 13.853283, -23.671707, 12.578844, 13.562656, -24.563269, 13.266899, 13.176035, -25.120148, 13.783545, 12.764626, -23.830704, 13.556321, 12.993084, -25.120148, 13.783545, 12.764626, -24.563269, 13.266899, 13.176035, -24.997204, 12.729578, 13.513615, -25.120148, 13.783545, 12.764626, -24.997204, 12.729578, 13.513615, -25.263885, 12.827821, 13.423475, -23.830704, 13.556321, 12.993084, -22.81894, 12.8182955, 12.022795, -22.412296, 12.812631, 13.106188, -23.423197, 13.259348, 12.431152, -22.81894, 12.8182955, 12.022795, -23.830704, 13.556321, 12.993084, -22.81894, 12.8182955, 12.022795, -23.423197, 13.259348, 12.431152, -23.243961, 12.84373, 11.64538, -22.81894, 12.8182955, 12.022795, -23.243961, 12.84373, 11.64538, -22.970037, 12.747108, 11.67061, -23.830704, 13.556321, 12.993084, -23.392378, 13.061067, 11.639456, -23.423197, 13.259348, 12.431152, -23.830704, 13.556321, 12.993084, -23.693872, 13.206497, 11.613611, -23.392378, 13.061067, 11.639456, -23.671707, 12.578844, 13.562656, -24.109556, 12.314231, 13.807953, -23.830704, 13.556321, 12.993084, -23.671707, 12.578844, 13.562656, -23.848108, 12.188936, 13.830387, -24.109556, 12.314231, 13.807953, -25.120148, 13.783545, 12.764626, -24.30996, 13.49691, 12.078384, -23.830704, 13.556321, 12.993084, -24.30996, 13.49691, 12.078384, -25.120148, 13.783545, 12.764626, -24.933325, 13.512459, 11.685724, -24.30996, 13.49691, 12.078384, -24.933325, 13.512459, 11.685724, -24.482748, 13.423203, 11.660212, -24.563269, 13.266899, 13.176035, -24.109556, 12.314231, 13.807953, -24.80421, 12.541008, 13.571323, -24.563269, 13.266899, 13.176035, -23.830704, 13.556321, 12.993084, -24.109556, 12.314231, 13.807953, -26.33375, 13.5643215, 12.432411, -25.424961, 13.642609, 12.2283325, -25.120148, 13.783545, 12.764626, -25.424961, 13.642609, 12.2283325, -26.33375, 13.5643215, 12.432411, -26.324017, 13.5112915, 12.189222, -25.424961, 13.642609, 12.2283325, -26.324017, 13.5112915, 12.189222, -25.71668, 13.54311, 11.969755, -22.184439, 11.839201, 13.474684, -22.953316, 11.729006, 13.905809, -22.412296, 12.812631, 13.106188, -22.184439, 11.839201, 13.474684, -22.487688, 11.524162, 13.78611, -22.953316, 11.729006, 13.905809, -21.854174, 11.521826, 13.480944, -22.187183, 11.317971, 13.70713, -22.184439, 11.839201, 13.474684, -21.854174, 11.521826, 13.480944, -22.027325, 11.277615, 13.666721, -22.187183, 11.317971, 13.70713, -21.854174, 11.521826, 13.480944, -21.313543, 11.772419, 13.055622, -21.702065, 11.143689, 13.583382, -20.98937, 11.087692, 13.168047, -21.702065, 11.143689, 13.583382, -21.313543, 11.772419, 13.055622, -20.98937, 11.087692, 13.168047, -21.207434, 10.925812, 13.392204, -21.702065, 11.143689, 13.583382, -27.190845, 13.454124, 13.211977, -26.123392, 13.504549, 12.108877, -26.042444, 13.6264305, 12.347961, -27.190845, 13.454124, 13.211977, -27.674408, 12.802565, 12.217752, -26.123392, 13.504549, 12.108877, -26.397722, 12.685178, 13.907841, -26.517313, 13.244606, 13.225155, -26.210627, 12.959142, 13.428625, -26.397722, 12.685178, 13.907841, -27.190845, 13.454124, 13.211977, -26.517313, 13.244606, 13.225155, -30.59442, 8.298002, 15.834078, -30.123693, 9.8882065, 15.319544, -29.275402, 9.5317335, 15.74361, -30.59442, 8.298002, 15.834078, -30.741045, 8.328472, 15.732853, -30.123693, 9.8882065, 15.319544, -30.123693, 9.8882065, 15.319544, -28.95761, 9.849106, 15.67477, -29.275402, 9.5317335, 15.74361, -30.123693, 9.8882065, 15.319544, -29.313347, 10.32216, 15.361237, -28.95761, 9.849106, 15.67477, -29.313347, 10.32216, 15.361237, -30.123693, 9.8882065, 15.319544, -29.39274, 11.410282, 14.816835, -29.313347, 10.32216, 15.361237, -29.39274, 11.410282, 14.816835, -28.10266, 10.688791, 15.489471, -29.313347, 10.32216, 15.361237, -28.10266, 10.688791, 15.489471, -28.671627, 10.132956, 15.612846, -28.10266, 10.688791, 15.489471, -28.17546, 11.1942005, 15.154776, -27.974642, 10.813639, 15.397457, -28.10266, 10.688791, 15.489471, -29.39274, 11.410282, 14.816835, -28.17546, 11.1942005, 15.154776, -27.208647, 11.806034, 14.844706, -27.177315, 12.54514, 14.161594, -26.99829, 12.019685, 14.60232, -27.208647, 11.806034, 14.844706, -28.297514, 12.740184, 14.028536, -27.177315, 12.54514, 14.161594, -30.79949, 8.3112135, 15.561966, -30.123693, 9.8882065, 15.319544, -30.741045, 8.328472, 15.732853, -30.123693, 9.8882065, 15.319544, -30.79949, 8.3112135, 15.561966, -30.452688, 9.11234, 15.217451, -30.452688, 9.11234, 15.217451, -30.79949, 8.3112135, 15.561966, -30.594294, 8.811231, 15.062023, -30.123693, 9.8882065, 15.319544, -29.879786, 10.464624, 14.329517, -29.39274, 11.410282, 14.816835, -30.123693, 9.8882065, 15.319544, -30.117891, 10.010266, 14.465597, -29.879786, 10.464624, 14.329517, -29.715208, 10.829817, 13.354015, -28.297514, 12.740184, 14.028536, -29.39274, 11.410282, 14.816835, -28.297514, 12.740184, 14.028536, -29.715208, 10.829817, 13.354015, -29.23813, 11.34871, 13.010698, -28.297514, 12.740184, 14.028536, -29.23813, 11.34871, 13.010698, -28.912056, 11.883873, 13.344931, -27.949308, 12.851552, 13.156515, -27.190845, 13.454124, 13.211977, -28.297514, 12.740184, 14.028536, -27.190845, 13.454124, 13.211977, -27.949308, 12.851552, 13.156515, -27.942167, 12.621899, 12.332624, -27.190845, 13.454124, 13.211977, -27.942167, 12.621899, 12.332624, -27.674408, 12.802565, 12.217752, -28.297514, 12.740184, 14.028536, -28.17546, 11.1942005, 15.154776, -29.39274, 11.410282, 14.816835, -28.104351, 12.050874, 14.57691, -28.17546, 11.1942005, 15.154776, -28.297514, 12.740184, 14.028536, -28.17546, 11.1942005, 15.154776, -28.104351, 12.050874, 14.57691, -27.539679, 11.245704, 15.084768, -28.17546, 11.1942005, 15.154776, -27.539679, 11.245704, 15.084768, -27.758598, 11.0494, 15.242031, -28.297514, 12.740184, 14.028536, -27.44365, 11.537734, 15.013952, -28.104351, 12.050874, 14.57691, -28.297514, 12.740184, 14.028536, -27.208647, 11.806034, 14.844706, -27.44365, 11.537734, 15.013952, -28.912056, 11.883873, 13.344931, -28.817327, 11.888908, 12.70685, -28.297514, 12.740184, 14.028536, -28.912056, 11.883873, 13.344931, -29.021376, 11.656829, 12.85384, -28.817327, 11.888908, 12.70685, -27.190845, 13.454124, 13.211977, -27.177315, 12.54514, 14.161594, -28.297514, 12.740184, 14.028536, -27.177315, 12.54514, 14.161594, -27.190845, 13.454124, 13.211977, -26.397722, 12.685178, 13.907841, -27.177315, 12.54514, 14.161594, -26.397722, 12.685178, 13.907841, -26.694912, 12.386589, 14.250351, -27.949308, 12.851552, 13.156515, -28.817327, 11.888908, 12.70685, -28.11471, 12.312162, 12.404867, -27.949308, 12.851552, 13.156515, -28.297514, 12.740184, 14.028536, -28.817327, 11.888908, 12.70685, -26.042444, 13.6264305, 12.347961, -26.517313, 13.244606, 13.225155, -27.190845, 13.454124, 13.211977, -26.517313, 13.244606, 13.225155, -26.042444, 13.6264305, 12.347961, -25.841175, 13.485764, 12.483356, -26.517313, 13.244606, 13.225155, -25.841175, 13.485764, 12.483356, -26.088339, 13.183667, 13.109575, -29.879786, 10.464624, 14.329517, -29.715208, 10.829817, 13.354015, -29.39274, 11.410282, 14.816835, -29.879786, 10.464624, 14.329517, -29.939215, 10.384001, 13.731905, -29.715208, 10.829817, 13.354015, -30.117891, 10.010266, 14.465597, -30.074621, 10.006492, 13.9677925, -29.879786, 10.464624, 14.329517, -30.117891, 10.010266, 14.465597, -30.15518, 9.890993, 14.100925, -30.074621, 10.006492, 13.9677925, -30.117891, 10.010266, 14.465597, -30.123693, 9.8882065, 15.319544, -30.313383, 9.599236, 14.366161, -30.452688, 9.11234, 15.217451, -30.313383, 9.599236, 14.366161, -30.123693, 9.8882065, 15.319544, -30.452688, 9.11234, 15.217451, -30.495245, 9.115306, 14.813856, -30.313383, 9.599236, 14.366161, -27.201202, 14.111977, 12.216812, -25.942682, 13.340691, 12.239897, -25.963226, 13.52549, 12.449766, -27.201202, 14.111977, 12.216812, -27.26703, 13.264112, 11.247925, -25.942682, 13.340691, 12.239897, -27.481268, 13.527826, 13.334137, -26.931988, 13.740763, 12.7410145, -26.979942, 13.491425, 13.121708, -27.481268, 13.527826, 13.334137, -27.201202, 14.111977, 12.216812, -26.931988, 13.740763, 12.7410145, -32.707096, 11.829945, 11.838261, -31.489027, 12.834562, 11.726076, -31.396978, 12.465054, 12.665121, -32.707096, 11.829945, 11.838261, -32.720463, 11.854302, 11.658894, -31.489027, 12.834562, 11.726076, -31.489027, 12.834562, 11.726076, -31.048937, 12.614889, 12.831612, -31.396978, 12.465054, 12.665121, -31.489027, 12.834562, 11.726076, -30.883116, 12.990331, 12.325209, -31.048937, 12.614889, 12.831612, -30.883116, 12.990331, 12.325209, -31.489027, 12.834562, 11.726076, -30.172102, 13.71146, 11.822292, -30.883116, 12.990331, 12.325209, -30.172102, 13.71146, 11.822292, -30.112314, 13.00444, 13.27858, -30.883116, 12.990331, 12.325209, -30.112314, 13.00444, 13.27858, -30.735453, 12.748096, 12.981314, -30.112314, 13.00444, 13.27858, -29.77852, 13.318491, 12.969162, -29.928392, 13.031496, 13.299112, -30.112314, 13.00444, 13.27858, -30.172102, 13.71146, 11.822292, -29.77852, 13.318491, 12.969162, -28.82114, 13.425723, 13.433544, -28.131073, 13.753705, 12.945568, -28.474356, 13.438845, 13.407442, -28.82114, 13.425723, 13.433544, -28.579897, 14.188468, 12.007677, -28.131073, 13.753705, 12.945568, -32.672134, 11.781767, 11.502314, -31.489027, 12.834562, 11.726076, -32.720463, 11.854302, 11.658894, -31.489027, 12.834562, 11.726076, -32.672134, 11.781767, 11.502314, -31.952402, 12.217968, 11.469395, -31.952402, 12.217968, 11.469395, -32.672134, 11.781767, 11.502314, -32.0643, 11.928456, 11.276808, -31.489027, 12.834562, 11.726076, -30.603403, 12.827642, 11.204182, -30.172102, 13.71146, 11.822292, -31.489027, 12.834562, 11.726076, -30.9988, 12.565276, 11.150988, -30.603403, 12.827642, 11.204182, -29.840925, 12.6641445, 10.642536, -28.579897, 14.188468, 12.007677, -30.172102, 13.71146, 11.822292, -28.579897, 14.188468, 12.007677, -29.840925, 12.6641445, 10.642536, -29.154, 12.815418, 10.721668, -28.579897, 14.188468, 12.007677, -29.154, 12.815418, 10.721668, -28.948093, 13.334579, 11.159084, -27.901386, 13.802778, 11.676207, -27.201202, 14.111977, 12.216812, -28.579897, 14.188468, 12.007677, -27.201202, 14.111977, 12.216812, -27.901386, 13.802778, 11.676207, -27.566175, 13.240652, 11.143919, -27.201202, 14.111977, 12.216812, -27.566175, 13.240652, 11.143919, -27.26703, 13.264112, 11.247925, -28.579897, 14.188468, 12.007677, -29.77852, 13.318491, 12.969162, -30.172102, 13.71146, 11.822292, -29.078878, 13.783996, 12.575394, -29.77852, 13.318491, 12.969162, -28.579897, 14.188468, 12.007677, -29.77852, 13.318491, 12.969162, -29.078878, 13.783996, 12.575394, -29.3032, 13.130279, 13.369244, -29.77852, 13.318491, 12.969162, -29.3032, 13.130279, 13.369244, -29.61749, 13.100707, 13.335033, -28.579897, 14.188468, 12.007677, -29.159786, 13.339252, 13.394022, -29.078878, 13.783996, 12.575394, -28.579897, 14.188468, 12.007677, -28.82114, 13.425723, 13.433544, -29.159786, 13.339252, 13.394022, -28.948093, 13.334579, 11.159084, -28.545794, 13.027181, 10.795259, -28.579897, 14.188468, 12.007677, -28.948093, 13.334579, 11.159084, -28.840069, 12.952937, 10.761011, -28.545794, 13.027181, 10.795259, -27.201202, 14.111977, 12.216812, -28.131073, 13.753705, 12.945568, -28.579897, 14.188468, 12.007677, -28.131073, 13.753705, 12.945568, -27.201202, 14.111977, 12.216812, -27.481268, 13.527826, 13.334137, -28.131073, 13.753705, 12.945568, -27.481268, 13.527826, 13.334137, -27.971476, 13.512636, 13.371208, -27.901386, 13.802778, 11.676207, -28.545794, 13.027181, 10.795259, -27.7606, 13.039855, 11.0648365, -27.901386, 13.802778, 11.676207, -28.579897, 14.188468, 12.007677, -28.545794, 13.027181, 10.795259, -25.963226, 13.52549, 12.449766, -26.931988, 13.740763, 12.7410145, -27.201202, 14.111977, 12.216812, -26.931988, 13.740763, 12.7410145, -25.963226, 13.52549, 12.449766, -25.99063, 13.406307, 12.702139, -26.931988, 13.740763, 12.7410145, -25.99063, 13.406307, 12.702139, -26.647318, 13.508326, 12.980991, -30.603403, 12.827642, 11.204182, -29.840925, 12.6641445, 10.642536, -30.172102, 13.71146, 11.822292, -30.603403, 12.827642, 11.204182, -30.356276, 12.512424, 10.763277, -29.840925, 12.6641445, 10.642536, -30.9988, 12.565276, 11.150988, -30.676294, 12.321331, 10.836258, -30.603403, 12.827642, 11.204182, -30.9988, 12.565276, 11.150988, -30.858511, 12.308209, 10.879881, -30.676294, 12.321331, 10.836258, -30.9988, 12.565276, 11.150988, -31.489027, 12.834562, 11.726076, -31.220568, 12.222282, 10.96516, -31.952402, 12.217968, 11.469395, -31.220568, 12.222282, 10.96516, -31.489027, 12.834562, 11.726076, -31.952402, 12.217968, 11.469395, -31.764006, 12.055641, 11.166313, -31.220568, 12.222282, 10.96516, -26.549658, 13.852484, 11.521194, -25.914532, 13.414665, 12.523054, -26.09762, 13.550388, 12.580524, -26.549658, 13.852484, 11.521194, -25.778147, 13.206228, 11.136642, -25.914532, 13.414665, 12.523054, -27.393269, 13.267618, 11.867287, -26.750992, 13.556589, 11.991425, -27.016235, 13.327029, 12.142666, -27.393269, 13.267618, 11.867287, -26.549658, 13.852484, 11.521194, -26.750992, 13.556589, 11.991425, -28.51631, 11.110254, 7.4424706, -28.004425, 12.171139, 8.214411, -28.58325, 11.828327, 8.733058, -28.51631, 11.110254, 7.4424706, -28.3986, 11.137669, 7.350334, -28.004425, 12.171139, 8.214411, -28.004425, 12.171139, 8.214411, -28.560322, 12.001171, 9.0543, -28.58325, 11.828327, 8.733058, -28.004425, 12.171139, 8.214411, -28.169415, 12.366816, 8.922272, -28.560322, 12.001171, 9.0543, -28.169415, 12.366816, 8.922272, -28.004425, 12.171139, 8.214411, -27.574968, 13.101874, 9.166556, -28.169415, 12.366816, 8.922272, -27.574968, 13.101874, 9.166556, -28.497368, 12.447617, 9.9169655, -28.169415, 12.366816, 8.922272, -28.497368, 12.447617, 9.9169655, -28.539549, 12.153973, 9.343277, -28.497368, 12.447617, 9.9169655, -28.157778, 12.77416, 10.008541, -28.434235, 12.498222, 10.057671, -28.497368, 12.447617, 9.9169655, -27.574968, 13.101874, 9.166556, -28.157778, 12.77416, 10.008541, -28.060122, 12.979725, 10.923248, -27.435986, 13.376825, 11.196117, -27.887424, 13.044889, 11.166651, -28.060122, 12.979725, 10.923248, -27.034447, 13.723143, 10.398659, -27.435986, 13.376825, 11.196117, -28.256086, 11.096142, 7.324676, -28.004425, 12.171139, 8.214411, -28.3986, 11.137669, 7.350334, -28.004425, 12.171139, 8.214411, -28.256086, 11.096142, 7.324676, -27.958471, 11.59544, 7.810196, -27.958471, 11.59544, 7.810196, -28.256086, 11.096142, 7.324676, -27.841549, 11.343948, 7.662429, -28.004425, 12.171139, 8.214411, -27.223587, 12.345152, 8.655518, -27.574968, 13.101874, 9.166556, -28.004425, 12.171139, 8.214411, -27.335718, 12.067413, 8.358295, -27.223587, 12.345152, 8.655518, -26.456749, 12.367084, 8.993832, -27.034447, 13.723143, 10.398659, -27.574968, 13.101874, 9.166556, -27.034447, 13.723143, 10.398659, -26.456749, 12.367084, 8.993832, -26.222069, 12.594754, 9.523212, -27.034447, 13.723143, 10.398659, -26.222069, 12.594754, 9.523212, -26.492432, 13.021158, 9.833479, -26.435522, 13.525401, 10.803925, -26.549658, 13.852484, 11.521194, -27.034447, 13.723143, 10.398659, -26.549658, 13.852484, 11.521194, -26.435522, 13.525401, 10.803925, -25.835228, 13.148436, 10.873161, -26.549658, 13.852484, 11.521194, -25.835228, 13.148436, 10.873161, -25.778147, 13.206228, 11.136642, -27.034447, 13.723143, 10.398659, -28.157778, 12.77416, 10.008541, -27.574968, 13.101874, 9.166556, -27.610704, 13.283167, 10.320328, -28.157778, 12.77416, 10.008541, -27.034447, 13.723143, 10.398659, -28.157778, 12.77416, 10.008541, -27.610704, 13.283167, 10.320328, -28.219557, 12.675113, 10.536343, -28.157778, 12.77416, 10.008541, -28.219557, 12.675113, 10.536343, -28.328056, 12.602215, 10.297457, -27.034447, 13.723143, 10.398659, -28.175543, 12.86018, 10.661373, -27.610704, 13.283167, 10.320328, -27.034447, 13.723143, 10.398659, -28.060122, 12.979725, 10.923248, -28.175543, 12.86018, 10.661373, -26.492432, 13.021158, 9.833479, -26.016369, 12.854247, 9.998339, -27.034447, 13.723143, 10.398659, -26.492432, 13.021158, 9.833479, -26.116678, 12.750883, 9.77084, -26.016369, 12.854247, 9.998339, -26.549658, 13.852484, 11.521194, -27.435986, 13.376825, 11.196117, -27.034447, 13.723143, 10.398659, -27.435986, 13.376825, 11.196117, -26.549658, 13.852484, 11.521194, -27.393269, 13.267618, 11.867287, -27.435986, 13.376825, 11.196117, -27.393269, 13.267618, 11.867287, -27.637686, 13.179714, 11.523783, -26.435522, 13.525401, 10.803925, -26.016369, 12.854247, 9.998339, -25.865164, 12.972085, 10.685308, -26.435522, 13.525401, 10.803925, -27.034447, 13.723143, 10.398659, -26.016369, 12.854247, 9.998339, -26.09762, 13.550388, 12.580524, -26.750992, 13.556589, 11.991425, -26.549658, 13.852484, 11.521194, -26.750992, 13.556589, 11.991425, -26.09762, 13.550388, 12.580524, -26.272526, 13.433453, 12.685123, -26.750992, 13.556589, 11.991425, -26.272526, 13.433453, 12.685123, -26.766165, 13.395969, 12.328079, -27.223587, 12.345152, 8.655518, -26.456749, 12.367084, 8.993832, -27.574968, 13.101874, 9.166556, -27.223587, 12.345152, 8.655518, -26.759527, 12.152982, 8.67496, -26.456749, 12.367084, 8.993832, -27.335718, 12.067413, 8.358295, -26.946072, 11.938253, 8.469797, -27.223587, 12.345152, 8.655518, -27.335718, 12.067413, 8.358295, -27.053827, 11.894838, 8.359913, -26.946072, 11.938253, 8.469797, -27.335718, 12.067413, 8.358295, -28.004425, 12.171139, 8.214411, -27.266567, 11.75786, 8.137337, -27.958471, 11.59544, 7.810196, -27.266567, 11.75786, 8.137337, -28.004425, 12.171139, 8.214411, -27.958471, 11.59544, 7.810196, -27.63729, 11.513017, 7.833111, -27.266567, 11.75786, 8.137337, -25.299118, 14.016339, 11.705952, -26.202145, 13.431294, 12.758247, -26.33764, 13.542391, 12.539379, -25.299118, 14.016339, 11.705952, -24.55057, 13.487381, 12.600025, -26.202145, 13.431294, 12.758247, -25.729456, 13.096121, 10.875708, -25.798758, 13.544817, 11.6329155, -25.976883, 13.186634, 11.381079, -25.729456, 13.096121, 10.875708, -25.299118, 14.016339, 11.705952, -25.798758, 13.544817, 11.6329155, -20.600985, 11.212358, 8.53599, -21.567623, 12.368253, 9.249824, -22.19324, 11.752106, 8.746279, -20.600985, 11.212358, 8.53599, -20.480156, 11.286333, 8.646276, -21.567623, 12.368253, 9.249824, -21.567623, 12.368253, 9.249824, -22.58087, 11.89322, 8.845634, -22.19324, 11.752106, 8.746279, -21.567623, 12.368253, 9.249824, -22.426706, 12.42362, 9.235527, -22.58087, 11.89322, 8.845634, -22.426706, 12.42362, 9.235527, -21.567623, 12.368253, 9.249824, -22.728525, 13.355524, 9.92947, -22.426706, 12.42362, 9.235527, -22.728525, 13.355524, 9.92947, -23.621424, 12.259226, 9.112419, -22.426706, 12.42362, 9.235527, -23.621424, 12.259226, 9.112419, -22.929403, 12.018518, 8.935175, -23.621424, 12.259226, 9.112419, -23.71438, 12.69767, 9.51326, -23.778412, 12.302008, 9.216736, -23.621424, 12.259226, 9.112419, -22.728525, 13.355524, 9.92947, -23.71438, 12.69767, 9.51326, -24.753212, 12.792858, 9.863087, -25.008253, 13.342491, 10.636538, -25.004257, 12.857843, 10.124001, -24.753212, 12.792858, 9.863087, -24.116346, 13.970501, 10.84609, -25.008253, 13.342491, 10.636538, -20.411009, 11.269615, 8.813364, -21.567623, 12.368253, 9.249824, -20.480156, 11.286333, 8.646276, -21.567623, 12.368253, 9.249824, -20.411009, 11.269615, 8.813364, -20.987125, 11.792372, 9.251802, -20.987125, 11.792372, 9.251802, -20.411009, 11.269615, 8.813364, -20.750517, 11.554993, 9.368832, -21.567623, 12.368253, 9.249824, -21.917282, 12.636011, 10.289265, -22.728525, 13.355524, 9.92947, -21.567623, 12.368253, 9.249824, -21.553564, 12.347399, 10.101149, -21.917282, 12.636011, 10.289265, -22.131565, 12.746388, 11.290453, -24.116346, 13.970501, 10.84609, -22.728525, 13.355524, 9.92947, -24.116346, 13.970501, 10.84609, -22.131565, 12.746388, 11.290453, -22.724337, 12.956984, 11.684092, -24.116346, 13.970501, 10.84609, -22.724337, 12.956984, 11.684092, -23.211079, 13.356783, 11.415373, -24.388641, 13.79298, 11.697922, -25.299118, 14.016339, 11.705952, -24.116346, 13.970501, 10.84609, -25.299118, 14.016339, 11.705952, -24.388641, 13.79298, 11.697922, -24.252804, 13.45727, 12.470831, -25.299118, 14.016339, 11.705952, -24.252804, 13.45727, 12.470831, -24.55057, 13.487381, 12.600025, -24.116346, 13.970501, 10.84609, -23.71438, 12.69767, 9.51326, -22.728525, 13.355524, 9.92947, -24.050045, 13.353634, 10.197813, -23.71438, 12.69767, 9.51326, -24.116346, 13.970501, 10.84609, -23.71438, 12.69767, 9.51326, -24.050045, 13.353634, 10.197813, -24.313118, 12.454627, 9.572146, -23.71438, 12.69767, 9.51326, -24.313118, 12.454627, 9.572146, -24.046837, 12.398183, 9.39505, -24.116346, 13.970501, 10.84609, -24.459291, 12.676998, 9.6678705, -24.050045, 13.353634, 10.197813, -24.116346, 13.970501, 10.84609, -24.753212, 12.792858, 9.863087, -24.459291, 12.676998, 9.6678705, -23.211079, 13.356783, 11.415373, -23.25923, 13.221777, 12.038786, -24.116346, 13.970501, 10.84609, -23.211079, 13.356783, 11.415373, -23.004206, 13.122009, 11.869499, -23.25923, 13.221777, 12.038786, -25.299118, 14.016339, 11.705952, -25.008253, 13.342491, 10.636538, -24.116346, 13.970501, 10.84609, -25.008253, 13.342491, 10.636538, -25.299118, 14.016339, 11.705952, -25.729456, 13.096121, 10.875708, -25.008253, 13.342491, 10.636538, -25.729456, 13.096121, 10.875708, -25.374859, 13.007589, 10.507263, -24.388641, 13.79298, 11.697922, -23.25923, 13.221777, 12.038786, -24.033789, 13.251799, 12.374115, -24.388641, 13.79298, 11.697922, -24.116346, 13.970501, 10.84609, -23.25923, 13.221777, 12.038786, -26.33764, 13.542391, 12.539379, -25.798758, 13.544817, 11.6329155, -25.299118, 14.016339, 11.705952, -25.798758, 13.544817, 11.6329155, -26.33764, 13.542391, 12.539379, -26.4637, 13.351927, 12.376793, -25.798758, 13.544817, 11.6329155, -26.4637, 13.351927, 12.376793, -26.145252, 13.288563, 11.72068, -21.917282, 12.636011, 10.289265, -22.131565, 12.746388, 11.290453, -22.728525, 13.355524, 9.92947, -21.917282, 12.636011, 10.289265, -21.7968, 12.497051, 10.865448, -22.131565, 12.746388, 11.290453, -21.553564, 12.347399, 10.101149, -21.57792, 12.244032, 10.592348, -21.917282, 12.636011, 10.289265, -21.553564, 12.347399, 10.101149, -21.464146, 12.196934, 10.445953, -21.57792, 12.244032, 10.592348, -21.553564, 12.347399, 10.101149, -21.567623, 12.368253, 9.249824, -21.231346, 12.042698, 10.149359, -20.987125, 11.792372, 9.251802, -21.231346, 12.042698, 10.149359, -21.567623, 12.368253, 9.249824, -20.987125, 11.792372, 9.251802, -20.92406, 11.751565, 9.648912, -21.231346, 12.042698, 10.149359, -25.705019, 13.809881, 13.562584, -26.45702, 13.504189, 12.279745, -26.186665, 13.570251, 12.245989, -25.705019, 13.809881, 13.562584, -26.888538, 13.45844, 13.87192, -26.45702, 13.504189, 12.279745, -24.945316, 12.747828, 13.355633, -25.55226, 13.354175, 13.0715885, -25.309752, 12.956354, 12.9552, -24.945316, 12.747828, 13.355633, -25.705019, 13.809881, 13.562584, -25.55226, 13.354175, 13.0715885, -24.723381, 10.182662, 18.724218, -24.892126, 11.507984, 17.707823, -24.315966, 10.822086, 17.237934, -24.723381, 10.182662, 18.724218, -24.853382, 10.273891, 18.805033, -24.892126, 11.507984, 17.707823, -24.892126, 11.507984, 17.707823, -24.260817, 10.9978075, 16.859945, -24.315966, 10.822086, 17.237934, -24.892126, 11.507984, 17.707823, -24.594227, 11.594991, 16.92428, -24.260817, 10.9978075, 16.859945, -24.594227, 11.594991, 16.92428, -24.892126, 11.507984, 17.707823, -24.99707, 12.669626, 16.504105, -24.594227, 11.594991, 16.92428, -24.99707, 12.669626, 16.504105, -24.11179, 11.456928, 15.843534, -24.594227, 11.594991, 16.92428, -24.11179, 11.456928, 15.843534, -24.211235, 11.154295, 16.519825, -24.11179, 11.456928, 15.843534, -24.396267, 11.977171, 15.664667, -24.146303, 11.527758, 15.66567, -24.11179, 11.456928, 15.843534, -24.99707, 12.669626, 16.504105, -24.396267, 11.977171, 15.664667, -24.372864, 12.190012, 14.588343, -24.92865, 12.906826, 14.1381035, -24.519852, 12.320793, 14.269573, -24.372864, 12.190012, 14.588343, -25.306261, 13.528545, 14.961657, -24.92865, 12.906826, 14.1381035, -25.035671, 10.291328, 18.806995, -24.892126, 11.507984, 17.707823, -24.853382, 10.273891, 18.805033, -24.892126, 11.507984, 17.707823, -25.035671, 10.291328, 18.806995, -25.174652, 10.921768, 18.176992, -25.174652, 10.921768, 18.176992, -25.035671, 10.291328, 18.806995, -25.392036, 10.70524, 18.326195, -24.892126, 11.507984, 17.707823, -25.717178, 12.011866, 17.041342, -24.99707, 12.669626, 16.504105, -24.892126, 11.507984, 17.707823, -25.703766, 11.673546, 17.411934, -25.717178, 12.011866, 17.041342, -26.566265, 12.347487, 16.495499, -25.306261, 13.528545, 14.961657, -24.99707, 12.669626, 16.504105, -25.306261, 13.528545, 14.961657, -26.566265, 12.347487, 16.495499, -26.699371, 12.663515, 15.829714, -25.306261, 13.528545, 14.961657, -26.699371, 12.663515, 15.829714, -26.24021, 13.016304, 15.522837, -26.053259, 13.553265, 14.382238, -25.705019, 13.809881, 13.562584, -25.306261, 13.528545, 14.961657, -25.705019, 13.809881, 13.562584, -26.053259, 13.553265, 14.382238, -26.875654, 13.388691, 14.190426, -25.705019, 13.809881, 13.562584, -26.875654, 13.388691, 14.190426, -26.888538, 13.45844, 13.87192, -25.306261, 13.528545, 14.961657, -24.396267, 11.977171, 15.664667, -24.99707, 12.669626, 16.504105, -24.82944, 12.781982, 15.184017, -24.396267, 11.977171, 15.664667, -25.306261, 13.528545, 14.961657, -24.396267, 11.977171, 15.664667, -24.82944, 12.781982, 15.184017, -24.26432, 11.775922, 15.060766, -24.396267, 11.977171, 15.664667, -24.26432, 11.775922, 15.060766, -24.206535, 11.67166, 15.364321, -25.306261, 13.528545, 14.961657, -24.306345, 12.02211, 14.9169445, -24.82944, 12.781982, 15.184017, -25.306261, 13.528545, 14.961657, -24.372864, 12.190012, 14.588343, -24.306345, 12.02211, 14.9169445, -26.24021, 13.016304, 15.522837, -26.82347, 13.022417, 15.237913, -25.306261, 13.528545, 14.961657, -26.24021, 13.016304, 15.522837, -26.765692, 12.877438, 15.523392, -26.82347, 13.022417, 15.237913, -25.705019, 13.809881, 13.562584, -24.92865, 12.906826, 14.1381035, -25.306261, 13.528545, 14.961657, -24.92865, 12.906826, 14.1381035, -25.705019, 13.809881, 13.562584, -24.945316, 12.747828, 13.355633, -24.92865, 12.906826, 14.1381035, -24.945316, 12.747828, 13.355633, -24.738127, 12.5664425, 13.806084, -26.053259, 13.553265, 14.382238, -26.82347, 13.022417, 15.237913, -26.853886, 13.155892, 14.403858, -26.053259, 13.553265, 14.382238, -25.306261, 13.528545, 14.961657, -26.82347, 13.022417, 15.237913, -26.186665, 13.570251, 12.245989, -25.55226, 13.354175, 13.0715885, -25.705019, 13.809881, 13.562584, -25.55226, 13.354175, 13.0715885, -26.186665, 13.570251, 12.245989, -26.027418, 13.354084, 12.165661, -25.55226, 13.354175, 13.0715885, -26.027418, 13.354084, 12.165661, -25.556536, 13.13711, 12.689047, -25.717178, 12.011866, 17.041342, -26.566265, 12.347487, 16.495499, -24.99707, 12.669626, 16.504105, -25.717178, 12.011866, 17.041342, -26.317577, 11.997127, 16.929232, -26.566265, 12.347487, 16.495499, -25.703766, 11.673546, 17.411934, -26.153154, 11.680018, 17.200329, -25.717178, 12.011866, 17.041342, -25.703766, 11.673546, 17.411934, -26.069336, 11.597685, 17.352945, -26.153154, 11.680018, 17.200329, -25.703766, 11.673546, 17.411934, -24.892126, 11.507984, 17.707823, -25.896534, 11.372709, 17.657299, -25.174652, 10.921768, 18.176992, -25.896534, 11.372709, 17.657299, -24.892126, 11.507984, 17.707823, -25.174652, 10.921768, 18.176992, -25.57406, 10.966168, 18.087864, -25.896534, 11.372709, 17.657299, -23.578579, 3.6379063, 11.557745, -24.12619, 7.057568, 11.959915, -23.8756, 7.125276, 11.658489, -23.578579, 3.6379063, 11.557745, -23.888453, 3.5751674, 11.919102, -24.12619, 7.057568, 11.959915, -23.836985, -0.21907139, 11.791069, -23.888453, 3.5751674, 11.919102, -23.578579, 3.6379063, 11.557745, -23.836985, -0.21907139, 11.791069, -24.181253, -0.21472502, 12.204487, -23.888453, 3.5751674, 11.919102, -24.709448, 3.4888213, 11.70261, -24.880796, 6.9884076, 11.3729, -24.819984, 6.963768, 11.765211, -24.709448, 3.4888213, 11.70261, -24.77573, 3.5122192, 11.244535, -24.880796, 6.9884076, 11.3729, -24.003609, 3.6379066, 10.807255, -23.93851, 7.1495028, 11.266003, -24.242756, 7.125276, 11.010187, -24.003609, 3.6379066, 10.807255, -23.637373, 3.6615102, 11.095457, -23.93851, 7.1495028, 11.266003, -24.819984, 6.963768, 11.765211, -25.485994, 10.232269, 11.85763, -25.442476, 10.210943, 12.117752, -24.819984, 6.963768, 11.765211, -24.880796, 6.9884076, 11.3729, -25.485994, 10.232269, 11.85763, -23.8756, 7.125276, 11.658489, -24.984426, 10.2888, 12.251429, -24.815989, 10.342636, 12.050236, -23.8756, 7.125276, 11.658489, -24.12619, 7.057568, 11.959915, -24.984426, 10.2888, 12.251429, -24.815989, 10.342636, 12.050236, -24.984426, 10.2888, 12.251429, -25.548643, 11.995004, 12.470713, -24.815989, 10.342636, 12.050236, -25.548643, 11.995004, 12.470713, -25.430141, 12.056503, 12.32415, -23.888453, 3.5751674, 11.919102, -24.514824, 6.9884076, 12.019112, -24.12619, 7.057568, 11.959915, -23.888453, 3.5751674, 11.919102, -24.350702, 3.512219, 11.995026, -24.514824, 6.9884076, 12.019112, -24.12619, 7.057568, 11.959915, -24.514824, 6.9884076, 12.019112, -25.241776, 10.232269, 12.288855, -24.12619, 7.057568, 11.959915, -25.241776, 10.232269, 12.288855, -24.984426, 10.2888, 12.251429, -24.77573, 3.5122192, 11.244535, -24.63014, 7.057568, 11.070068, -24.880796, 6.9884076, 11.3729, -24.77573, 3.5122192, 11.244535, -24.472868, 3.5751677, 10.8871765, -24.63014, 7.057568, 11.070068, -24.880796, 6.9884076, 11.3729, -24.63014, 7.057568, 11.070068, -25.321539, 10.2888, 11.656172, -24.880796, 6.9884076, 11.3729, -25.321539, 10.2888, 11.656172, -25.485994, 10.232269, 11.85763, -24.242756, 7.125276, 11.010187, -24.857891, 10.362101, 11.78668, -25.062378, 10.342636, 11.615177, -24.242756, 7.125276, 11.010187, -23.93851, 7.1495028, 11.266003, -24.857891, 10.362101, 11.78668, -23.637373, 3.6615102, 11.095457, -23.8756, 7.125276, 11.658489, -23.93851, 7.1495028, 11.266003, -23.637373, 3.6615102, 11.095457, -23.578579, 3.6379063, 11.557745, -23.8756, 7.125276, 11.658489, -23.93851, 7.1495028, 11.266003, -24.815989, 10.342636, 12.050236, -24.857891, 10.362101, 11.78668, -23.93851, 7.1495028, 11.266003, -23.8756, 7.125276, 11.658489, -24.815989, 10.342636, 12.050236, -24.472868, 3.5751677, 10.8871765, -24.242756, 7.125276, 11.010187, -24.63014, 7.057568, 11.070068, -24.472868, 3.5751677, 10.8871765, -24.003609, 3.6379066, 10.807255, -24.242756, 7.125276, 11.010187, -24.868216, -0.21472502, 10.9914875, -24.003609, 3.6379066, 10.807255, -24.472868, 3.5751677, 10.8871765, -24.868216, -0.21472502, 10.9914875, -24.336596, -0.21907139, 10.908884, -24.003609, 3.6379066, 10.807255, -24.63014, 7.057568, 11.070068, -25.062378, 10.342636, 11.615177, -25.321539, 10.2888, 11.656172, -24.63014, 7.057568, 11.070068, -24.242756, 7.125276, 11.010187, -25.062378, 10.342636, 11.615177, -25.321539, 10.2888, 11.656172, -25.062378, 10.342636, 11.615177, -25.613142, 12.056503, 12.001019, -25.321539, 10.2888, 11.656172, -25.613142, 12.056503, 12.001019, -25.79978, 11.995004, 12.027269, -25.883152, 11.911559, 12.367321, -26.206875, 13.210678, 12.70623, -25.735323, 11.93475, 12.496033, -25.883152, 11.911559, 12.367321, -26.31439, 13.191008, 12.611546, -26.206875, 13.210678, 12.70623, -25.548643, 11.995004, 12.470713, -25.993643, 13.325804, 12.585468, -25.430141, 12.056503, 12.32415, -25.548643, 11.995004, 12.470713, -26.075993, 13.270106, 12.690508, -25.993643, 13.325804, 12.585468, -25.735323, 11.93475, 12.496033, -26.075993, 13.270106, 12.690508, -25.548643, 11.995004, 12.470713, -25.735323, 11.93475, 12.496033, -26.206875, 13.210678, 12.70623, -26.075993, 13.270106, 12.690508, -25.463963, 12.079072, 12.129919, -26.127062, 13.325804, 12.349884, -25.613142, 12.056503, 12.001019, -25.463963, 12.079072, 12.129919, -26.021097, 13.349201, 12.445444, -26.127062, 13.325804, 12.349884, -25.430141, 12.056503, 12.32415, -26.021097, 13.349201, 12.445444, -25.463963, 12.079072, 12.129919, -25.430141, 12.056503, 12.32415, -25.993643, 13.325804, 12.585468, -26.021097, 13.349201, 12.445444, -25.79978, 11.995004, 12.027269, -26.340294, 13.210678, 12.470644, -25.917505, 11.93475, 12.174345, -25.79978, 11.995004, 12.027269, -26.259499, 13.270106, 12.366484, -26.340294, 13.210678, 12.470644, -25.613142, 12.056503, 12.001019, -26.259499, 13.270106, 12.366484, -25.79978, 11.995004, 12.027269, -25.613142, 12.056503, 12.001019, -26.127062, 13.325804, 12.349884, -26.259499, 13.270106, 12.366484, -24.715683, -0.21037531, 12.288706, -24.709448, 3.4888213, 11.70261, -24.350702, 3.512219, 11.995026, -24.715683, -0.21037531, 12.288706, -25.129576, -0.2087202, 11.940542, -24.709448, 3.4888213, 11.70261, -25.917505, 11.93475, 12.174345, -26.31439, 13.191008, 12.611546, -25.883152, 11.911559, 12.367321, -25.917505, 11.93475, 12.174345, -26.340294, 13.210678, 12.470644, -26.31439, 13.191008, 12.611546, -24.350702, 3.512219, 11.995026, -24.819984, 6.963768, 11.765211, -24.514824, 6.9884076, 12.019112, -24.350702, 3.512219, 11.995026, -24.709448, 3.4888213, 11.70261, -24.819984, 6.963768, 11.765211, -25.129576, -0.2087202, 11.940542, -24.77573, 3.5122192, 11.244535, -24.709448, 3.4888213, 11.70261, -25.129576, -0.2087202, 11.940542, -25.215294, -0.21037531, 11.406523, -24.77573, 3.5122192, 11.244535, -24.336596, -0.21907139, 10.908884, -23.637373, 3.6615102, 11.095457, -24.003609, 3.6379066, 10.807255, -24.336596, -0.21907139, 10.908884, -23.924746, -0.2207303, 11.258205, -23.637373, 3.6615102, 11.095457, -24.181253, -0.21472502, 12.204487, -24.350702, 3.512219, 11.995026, -23.888453, 3.5751674, 11.919102, -24.181253, -0.21472502, 12.204487, -24.715683, -0.21037531, 12.288706, -24.350702, 3.512219, 11.995026, -25.215294, -0.21037531, 11.406523, -24.472868, 3.5751677, 10.8871765, -24.77573, 3.5122192, 11.244535, -25.215294, -0.21037531, 11.406523, -24.868216, -0.21472502, 10.9914875, -24.472868, 3.5751677, 10.8871765, -23.924746, -0.2207303, 11.258205, -23.578579, 3.6379063, 11.557745, -23.637373, 3.6615102, 11.095457, -23.924746, -0.2207303, 11.258205, -23.836985, -0.21907139, 11.791069, -23.578579, 3.6379063, 11.557745, -24.514824, 6.9884076, 12.019112, -25.442476, 10.210943, 12.117752, -25.241776, 10.232269, 12.288855, -24.514824, 6.9884076, 12.019112, -24.819984, 6.963768, 11.765211, -25.442476, 10.210943, 12.117752, -25.241776, 10.232269, 12.288855, -25.883152, 11.911559, 12.367321, -25.735323, 11.93475, 12.496033, -25.241776, 10.232269, 12.288855, -25.442476, 10.210943, 12.117752, -25.883152, 11.911559, 12.367321, -25.442476, 10.210943, 12.117752, -25.917505, 11.93475, 12.174345, -25.883152, 11.911559, 12.367321, -25.442476, 10.210943, 12.117752, -25.485994, 10.232269, 11.85763, -25.917505, 11.93475, 12.174345, -24.984426, 10.2888, 12.251429, -25.735323, 11.93475, 12.496033, -25.548643, 11.995004, 12.470713, -24.984426, 10.2888, 12.251429, -25.241776, 10.232269, 12.288855, -25.735323, 11.93475, 12.496033, -25.062378, 10.342636, 11.615177, -25.463963, 12.079072, 12.129919, -25.613142, 12.056503, 12.001019, -25.062378, 10.342636, 11.615177, -24.857891, 10.362101, 11.78668, -25.463963, 12.079072, 12.129919, -24.857891, 10.362101, 11.78668, -25.430141, 12.056503, 12.32415, -25.463963, 12.079072, 12.129919, -24.857891, 10.362101, 11.78668, -24.815989, 10.342636, 12.050236, -25.430141, 12.056503, 12.32415, -25.485994, 10.232269, 11.85763, -25.79978, 11.995004, 12.027269, -25.917505, 11.93475, 12.174345, -25.485994, 10.232269, 11.85763, -25.321539, 10.2888, 11.656172, -25.79978, 11.995004, 12.027269, 14.960796, 7.3177958, 10.284792, 14.197514, 7.0888658, 10.165964, 14.230383, 7.219601, 10.105017, 14.960796, 7.3177958, 10.284792, 14.899889, 6.6811314, 10.556076, 14.197514, 7.0888658, 10.165964, 15.035656, 7.2923393, 9.619492, 14.762205, 7.297971, 9.966335, 14.763325, 7.2744055, 9.734959, 15.035656, 7.2923393, 9.619492, 14.960796, 7.3177958, 10.284792, 14.762205, 7.297971, 9.966335, 17.810453, 5.5428567, 9.76613, 17.232166, 6.147483, 10.085447, 17.132032, 6.2065306, 9.564142, 17.810453, 5.5428567, 9.76613, 17.822802, 5.5103865, 9.856148, 17.232166, 6.147483, 10.085447, 17.232166, 6.147483, 10.085447, 16.95009, 6.3580484, 9.530783, 17.132032, 6.2065306, 9.564142, 17.232166, 6.147483, 10.085447, 16.89884, 6.4360313, 9.857285, 16.95009, 6.3580484, 9.530783, 16.89884, 6.4360313, 9.857285, 17.232166, 6.147483, 10.085447, 16.58012, 6.736306, 10.276844, 16.89884, 6.4360313, 9.857285, 16.58012, 6.736306, 10.276844, 16.462008, 6.7533646, 9.4380245, 16.89884, 6.4360313, 9.857285, 16.462008, 6.7533646, 9.4380245, 16.786558, 6.492572, 9.500252, 16.462008, 6.7533646, 9.4380245, 16.302326, 6.86697, 9.66189, 16.365639, 6.7913795, 9.442841, 16.462008, 6.7533646, 9.4380245, 16.58012, 6.736306, 10.276844, 16.302326, 6.86697, 9.66189, 15.768839, 7.134306, 9.500018, 15.418615, 7.241414, 9.825178, 15.579935, 7.1703434, 9.529699, 15.768839, 7.134306, 9.500018, 15.7336235, 7.169642, 10.347719, 15.418615, 7.241414, 9.825178, 17.788603, 5.4442396, 9.916714, 17.232166, 6.147483, 10.085447, 17.822802, 5.5103865, 9.856148, 17.232166, 6.147483, 10.085447, 17.788603, 5.4442396, 9.916714, 17.429657, 5.7386765, 10.057089, 17.429657, 5.7386765, 10.057089, 17.788603, 5.4442396, 9.916714, 17.469614, 5.541645, 10.084452, 17.232166, 6.147483, 10.085447, 16.729776, 6.127803, 10.365282, 16.58012, 6.736306, 10.276844, 17.232166, 6.147483, 10.085447, 16.926506, 5.9454803, 10.319639, 16.729776, 6.127803, 10.365282, 16.288668, 6.0046787, 10.627327, 15.7336235, 7.169642, 10.347719, 16.58012, 6.736306, 10.276844, 15.7336235, 7.169642, 10.347719, 16.288668, 6.0046787, 10.627327, 15.925083, 6.171483, 10.651344, 15.7336235, 7.169642, 10.347719, 15.925083, 6.171483, 10.651344, 15.8492565, 6.533848, 10.554647, 15.306034, 6.9726505, 10.442774, 14.960796, 7.3177958, 10.284792, 15.7336235, 7.169642, 10.347719, 14.960796, 7.3177958, 10.284792, 15.306034, 6.9726505, 10.442774, 15.061283, 6.615505, 10.587776, 14.960796, 7.3177958, 10.284792, 15.061283, 6.615505, 10.587776, 14.899889, 6.6811314, 10.556076, 15.7336235, 7.169642, 10.347719, 16.302326, 6.86697, 9.66189, 16.58012, 6.736306, 10.276844, 15.95689, 7.068595, 9.972179, 16.302326, 6.86697, 9.66189, 15.7336235, 7.169642, 10.347719, 16.302326, 6.86697, 9.66189, 15.95689, 7.068595, 9.972179, 16.037476, 6.9235826, 9.459768, 16.302326, 6.86697, 9.66189, 16.037476, 6.9235826, 9.459768, 16.200966, 6.8668146, 9.453747, 15.7336235, 7.169642, 10.347719, 15.948803, 7.0493326, 9.487534, 15.95689, 7.068595, 9.972179, 15.7336235, 7.169642, 10.347719, 15.768839, 7.134306, 9.500018, 15.948803, 7.0493326, 9.487534, 15.8492565, 6.533848, 10.554647, 15.598484, 6.34639, 10.67927, 15.7336235, 7.169642, 10.347719, 15.8492565, 6.533848, 10.554647, 15.754543, 6.2731757, 10.668556, 15.598484, 6.34639, 10.67927, 14.960796, 7.3177958, 10.284792, 15.418615, 7.241414, 9.825178, 15.7336235, 7.169642, 10.347719, 15.418615, 7.241414, 9.825178, 14.960796, 7.3177958, 10.284792, 15.035656, 7.2923393, 9.619492, 15.418615, 7.241414, 9.825178, 15.035656, 7.2923393, 9.619492, 15.302166, 7.2475796, 9.57913, 15.306034, 6.9726505, 10.442774, 15.598484, 6.34639, 10.67927, 15.17808, 6.500619, 10.592104, 15.306034, 6.9726505, 10.442774, 15.7336235, 7.169642, 10.347719, 15.598484, 6.34639, 10.67927, 14.230383, 7.219601, 10.105017, 14.762205, 7.297971, 9.966335, 14.960796, 7.3177958, 10.284792, 14.762205, 7.297971, 9.966335, 14.230383, 7.219601, 10.105017, 14.228041, 7.218266, 9.957092, 14.762205, 7.297971, 9.966335, 14.228041, 7.218266, 9.957092, 14.580687, 7.2687683, 9.813696, 16.729776, 6.127803, 10.365282, 16.288668, 6.0046787, 10.627327, 16.58012, 6.736306, 10.276844, 16.729776, 6.127803, 10.365282, 16.561163, 5.9042377, 10.517782, 16.288668, 6.0046787, 10.627327, 16.926506, 5.9454803, 10.319639, 16.737123, 5.796604, 10.437109, 16.729776, 6.127803, 10.365282, 16.926506, 5.9454803, 10.319639, 16.830635, 5.779464, 10.403534, 16.737123, 5.796604, 10.437109, 16.926506, 5.9454803, 10.319639, 17.232166, 6.147483, 10.085447, 17.020973, 5.7145977, 10.328281, 17.429657, 5.7386765, 10.057089, 17.020973, 5.7145977, 10.328281, 17.232166, 6.147483, 10.085447, 17.429657, 5.7386765, 10.057089, 17.308277, 5.615484, 10.174715, 17.020973, 5.7145977, 10.328281, 13.748751, 7.053542, 10.572222, 14.344303, 7.2258663, 9.903076, 14.400131, 7.268545, 10.054003, 13.748751, 7.053542, 10.572222, 13.42976, 6.790598, 9.931854, 14.344303, 7.2258663, 9.903076, 14.244236, 6.5294213, 10.857023, 14.149033, 6.936182, 10.532454, 14.338385, 6.748352, 10.605932, 14.244236, 6.5294213, 10.857023, 13.748751, 7.053542, 10.572222, 14.149033, 6.936182, 10.532454, 11.830196, 3.7078786, 11.580435, 12.08853, 4.698938, 11.459175, 12.606657, 4.4480114, 11.634403, 11.830196, 3.7078786, 11.580435, 11.739759, 3.7365186, 11.530159, 12.08853, 4.698938, 11.459175, 12.08853, 4.698938, 11.459175, 12.792095, 4.643725, 11.622538, 12.606657, 4.4480114, 11.634403, 12.08853, 4.698938, 11.459175, 12.569534, 4.9534965, 11.509512, 12.792095, 4.643725, 11.622538, 12.569534, 4.9534965, 11.509512, 12.08853, 4.698938, 11.459175, 12.50172, 5.6512947, 11.328012, 12.569534, 4.9534965, 11.509512, 12.50172, 5.6512947, 11.328012, 13.291006, 5.162043, 11.5888405, 12.569534, 4.9534965, 11.509512, 13.291006, 5.162043, 11.5888405, 12.958977, 4.8188252, 11.61166, 13.291006, 5.162043, 11.5888405, 13.2354355, 5.49573, 11.458206, 13.363539, 5.246913, 11.546379, 13.291006, 5.162043, 11.5888405, 12.50172, 5.6512947, 11.328012, 13.2354355, 5.49573, 11.458206, 13.796944, 5.897662, 11.320793, 13.790836, 6.4103017, 11.012831, 13.913025, 6.053091, 11.199114, 13.796944, 5.897662, 11.320793, 13.121664, 6.529729, 10.9958515, 13.790836, 6.4103017, 11.012831, 11.699358, 3.746626, 11.430775, 12.08853, 4.698938, 11.459175, 11.739759, 3.7365186, 11.530159, 12.08853, 4.698938, 11.459175, 11.699358, 3.746626, 11.430775, 11.891642, 4.2572117, 11.316582, 11.891642, 4.2572117, 11.316582, 11.699358, 3.746626, 11.430775, 11.803057, 4.0996614, 11.194469, 12.08853, 4.698938, 11.459175, 12.198441, 5.1560144, 10.946365, 12.50172, 5.6512947, 11.328012, 12.08853, 4.698938, 11.459175, 12.062822, 4.8732367, 10.978052, 12.198441, 5.1560144, 10.946365, 12.262251, 5.4878073, 10.418972, 13.121664, 6.529729, 10.9958515, 12.50172, 5.6512947, 11.328012, 13.121664, 6.529729, 10.9958515, 12.262251, 5.4878073, 10.418972, 12.532429, 5.8353195, 10.266994, 13.121664, 6.529729, 10.9958515, 12.532429, 5.8353195, 10.266994, 12.735952, 6.1089916, 10.515257, 13.298405, 6.7030754, 10.491749, 13.748751, 7.053542, 10.572222, 13.121664, 6.529729, 10.9958515, 13.748751, 7.053542, 10.572222, 13.298405, 6.7030754, 10.491749, 13.275257, 6.6692533, 9.985384, 13.748751, 7.053542, 10.572222, 13.275257, 6.6692533, 9.985384, 13.42976, 6.790598, 9.931854, 13.121664, 6.529729, 10.9958515, 13.2354355, 5.49573, 11.458206, 12.50172, 5.6512947, 11.328012, 13.256226, 6.064356, 11.224297, 13.2354355, 5.49573, 11.458206, 13.121664, 6.529729, 10.9958515, 13.2354355, 5.49573, 11.458206, 13.256226, 6.064356, 11.224297, 13.609962, 5.5398574, 11.403032, 13.2354355, 5.49573, 11.458206, 13.609962, 5.5398574, 11.403032, 13.485888, 5.4047327, 11.477678, 13.121664, 6.529729, 10.9958515, 13.663889, 5.719082, 11.394192, 13.256226, 6.064356, 11.224297, 13.121664, 6.529729, 10.9958515, 13.796944, 5.897662, 11.320793, 13.663889, 5.719082, 11.394192, 12.735952, 6.1089916, 10.515257, 12.770527, 6.18994, 10.142353, 13.121664, 6.529729, 10.9958515, 12.735952, 6.1089916, 10.515257, 12.654997, 6.03539, 10.206213, 12.770527, 6.18994, 10.142353, 13.748751, 7.053542, 10.572222, 13.790836, 6.4103017, 11.012831, 13.121664, 6.529729, 10.9958515, 13.790836, 6.4103017, 11.012831, 13.748751, 7.053542, 10.572222, 14.244236, 6.5294213, 10.857023, 13.790836, 6.4103017, 11.012831, 14.244236, 6.5294213, 10.857023, 14.080233, 6.311716, 11.029364, 13.298405, 6.7030754, 10.491749, 12.770527, 6.18994, 10.142353, 13.176046, 6.4788733, 9.995405, 13.298405, 6.7030754, 10.491749, 13.121664, 6.529729, 10.9958515, 12.770527, 6.18994, 10.142353, 14.400131, 7.268545, 10.054003, 14.149033, 6.936182, 10.532454, 13.748751, 7.053542, 10.572222, 14.149033, 6.936182, 10.532454, 14.400131, 7.268545, 10.054003, 14.524361, 7.1721907, 10.108967, 14.149033, 6.936182, 10.532454, 14.524361, 7.1721907, 10.108967, 14.399626, 6.918585, 10.443947, 12.198441, 5.1560144, 10.946365, 12.262251, 5.4878073, 10.418972, 12.50172, 5.6512947, 11.328012, 12.198441, 5.1560144, 10.946365, 12.143154, 5.1808786, 10.591608, 12.262251, 5.4878073, 10.418972, 12.062822, 4.8732367, 10.978052, 12.071648, 4.931636, 10.687195, 12.198441, 5.1560144, 10.946365, 12.062822, 4.8732367, 10.978052, 12.028627, 4.8476563, 10.753109, 12.071648, 4.931636, 10.687195, 12.062822, 4.8732367, 10.978052, 12.08853, 4.698938, 11.459175, 11.94441, 4.6445174, 10.876879, 11.891642, 4.2572117, 11.316582, 11.94441, 4.6445174, 10.876879, 12.08853, 4.698938, 11.459175, 11.891642, 4.2572117, 11.316582, 11.852747, 4.307365, 11.084038, 11.94441, 4.6445174, 10.876879, 13.7075405, 7.556304, 10.0746155, 14.456242, 7.1163726, 9.9533615, 14.450766, 7.1982546, 10.098274, 13.7075405, 7.556304, 10.0746155, 13.637535, 7.179911, 9.4115, 14.456242, 7.1163726, 9.9533615, 13.580511, 7.079011, 10.661566, 13.885704, 7.279489, 10.325115, 13.870678, 7.088043, 10.517448, 13.580511, 7.079011, 10.661566, 13.7075405, 7.556304, 10.0746155, 13.885704, 7.279489, 10.325115, 10.434713, 6.2257376, 9.758102, 11.151068, 6.834375, 9.774982, 11.238207, 6.5067883, 10.272303, 10.434713, 6.2257376, 9.758102, 10.420666, 6.2614727, 9.657367, 11.151068, 6.834375, 9.774982, 11.151068, 6.834375, 9.774982, 11.449902, 6.5769014, 10.375746, 11.238207, 6.5067883, 10.272303, 11.151068, 6.834375, 9.774982, 11.530296, 6.857931, 10.121874, 11.449902, 6.5769014, 10.375746, 11.530296, 6.857931, 10.121874, 11.151068, 6.834375, 9.774982, 11.933365, 7.3444357, 9.894124, 11.530296, 6.857931, 10.121874, 11.933365, 7.3444357, 9.894124, 12.01959, 6.757763, 10.651842, 11.530296, 6.857931, 10.121874, 12.01959, 6.757763, 10.651842, 11.640572, 6.6390653, 10.468557, 12.01959, 6.757763, 10.651842, 12.206385, 6.9807854, 10.499392, 12.129288, 6.772664, 10.661032, 12.01959, 6.757763, 10.651842, 11.933365, 7.3444357, 9.894124, 12.206385, 6.9807854, 10.499392, 12.789576, 6.995577, 10.750685, 13.181564, 7.251497, 10.484621, 12.994299, 7.0094805, 10.72579, 12.789576, 6.995577, 10.750685, 12.882765, 7.613677, 10.007544, 13.181564, 7.251497, 10.484621, 10.444181, 6.238669, 9.556094, 11.151068, 6.834375, 9.774982, 10.420666, 6.2614727, 9.657367, 11.151068, 6.834375, 9.774982, 10.444181, 6.238669, 9.556094, 10.868927, 6.502743, 9.566353, 10.868927, 6.502743, 9.566353, 10.444181, 6.238669, 9.556094, 10.796675, 6.356696, 9.423105, 11.151068, 6.834375, 9.774982, 11.658583, 6.90143, 9.442226, 11.933365, 7.3444357, 9.894124, 11.151068, 6.834375, 9.774982, 11.422882, 6.7517605, 9.392457, 11.658583, 6.90143, 9.442226, 12.092066, 6.8811474, 9.071436, 12.882765, 7.613677, 10.007544, 11.933365, 7.3444357, 9.894124, 12.882765, 7.613677, 10.007544, 12.092066, 6.8811474, 9.071436, 12.501753, 6.965708, 9.113264, 12.882765, 7.613677, 10.007544, 12.501753, 6.965708, 9.113264, 12.637543, 7.2165165, 9.423414, 13.274715, 7.4356117, 9.746176, 13.7075405, 7.556304, 10.0746155, 12.882765, 7.613677, 10.007544, 13.7075405, 7.556304, 10.0746155, 13.274715, 7.4356117, 9.746176, 13.45668, 7.17616, 9.358056, 13.7075405, 7.556304, 10.0746155, 13.45668, 7.17616, 9.358056, 13.637535, 7.179911, 9.4115, 12.882765, 7.613677, 10.007544, 12.206385, 6.9807854, 10.499392, 11.933365, 7.3444357, 9.894124, 12.606947, 7.305357, 10.3043785, 12.206385, 6.9807854, 10.499392, 12.882765, 7.613677, 10.007544, 12.206385, 6.9807854, 10.499392, 12.606947, 7.305357, 10.3043785, 12.502178, 6.827238, 10.693289, 12.206385, 6.9807854, 10.499392, 12.502178, 6.827238, 10.693289, 12.314716, 6.8113565, 10.680117, 12.882765, 7.613677, 10.007544, 12.587612, 6.947036, 10.728325, 12.606947, 7.305357, 10.3043785, 12.882765, 7.613677, 10.007544, 12.789576, 6.995577, 10.750685, 12.587612, 6.947036, 10.728325, 12.637543, 7.2165165, 9.423414, 12.864445, 7.0854006, 9.161785, 12.882765, 7.613677, 10.007544, 12.637543, 7.2165165, 9.423414, 12.688946, 7.04373, 9.142518, 12.864445, 7.0854006, 9.161785, 13.7075405, 7.556304, 10.0746155, 13.181564, 7.251497, 10.484621, 12.882765, 7.613677, 10.007544, 13.181564, 7.251497, 10.484621, 13.7075405, 7.556304, 10.0746155, 13.580511, 7.079011, 10.661566, 13.181564, 7.251497, 10.484621, 13.580511, 7.079011, 10.661566, 13.291115, 7.061289, 10.697266, 13.274715, 7.4356117, 9.746176, 12.864445, 7.0854006, 9.161785, 13.339142, 7.0672264, 9.294153, 13.274715, 7.4356117, 9.746176, 12.882765, 7.613677, 10.007544, 12.864445, 7.0854006, 9.161785, 14.450766, 7.1982546, 10.098274, 13.885704, 7.279489, 10.325115, 13.7075405, 7.556304, 10.0746155, 13.885704, 7.279489, 10.325115, 14.450766, 7.1982546, 10.098274, 14.443308, 7.0981884, 10.231224, 13.885704, 7.279489, 10.325115, 14.443308, 7.0981884, 10.231224, 14.063122, 7.117878, 10.426937, 11.658583, 6.90143, 9.442226, 12.092066, 6.8811474, 9.071436, 11.933365, 7.3444357, 9.894124, 11.658583, 6.90143, 9.442226, 11.79088, 6.773697, 9.139975, 12.092066, 6.8811474, 9.071436, 11.422882, 6.7517605, 9.392457, 11.603992, 6.6508856, 9.16963, 11.658583, 6.90143, 9.442226, 11.422882, 6.7517605, 9.392457, 11.497446, 6.636342, 9.199318, 11.603992, 6.6508856, 9.16963, 11.422882, 6.7517605, 9.392457, 11.151068, 6.834375, 9.774982, 11.28582, 6.5728126, 9.250239, 10.868927, 6.502743, 9.566353, 11.28582, 6.5728126, 9.250239, 11.151068, 6.834375, 9.774982, 10.868927, 6.502743, 9.566353, 10.970734, 6.4467087, 9.364567, 11.28582, 6.5728126, 9.250239, 14.070913, 7.495411, 9.618036, 14.482357, 7.125392, 10.12579, 14.3754425, 7.195709, 10.181591, 14.070913, 7.495411, 9.618036, 14.516771, 7.173098, 9.291218, 14.482357, 7.125392, 10.12579, 13.583624, 7.1059093, 9.775603, 13.968065, 7.2646875, 9.861715, 13.816387, 7.1105375, 9.930347, 13.583624, 7.1059093, 9.775603, 14.070913, 7.495411, 9.618036, 13.968065, 7.2646875, 9.861715, 12.772589, 6.376586, 6.9820247, 13.099979, 6.904664, 7.5420585, 12.775024, 6.637413, 7.820493, 12.772589, 6.376586, 6.9820247, 12.839212, 6.4047265, 6.9280143, 13.099979, 6.904664, 7.5420585, 13.099979, 6.904664, 7.5420585, 12.79912, 6.699265, 8.027189, 12.775024, 6.637413, 7.820493, 13.099979, 6.904664, 7.5420585, 13.025673, 6.931345, 7.982105, 12.79912, 6.699265, 8.027189, 13.025673, 6.931345, 7.982105, 13.099979, 6.904664, 7.5420585, 13.384867, 7.3345337, 8.193618, 13.025673, 6.931345, 7.982105, 13.384867, 7.3345337, 8.193618, 12.864683, 6.8550787, 8.580061, 13.025673, 6.931345, 7.982105, 12.864683, 6.8550787, 8.580061, 12.820887, 6.753347, 8.212795, 12.864683, 6.8550787, 8.580061, 13.068455, 7.0369205, 8.661718, 12.906773, 6.8680534, 8.665814, 12.864683, 6.8550787, 8.580061, 13.384867, 7.3345337, 8.193618, 13.068455, 7.0369205, 8.661718, 13.15687, 7.046695, 9.214457, 13.535355, 7.2501936, 9.400614, 13.26737, 7.0566998, 9.358015, 13.15687, 7.046695, 9.214457, 13.745743, 7.551609, 8.966543, 13.535355, 7.2501936, 9.400614, 12.922939, 6.384966, 6.9034257, 13.099979, 6.904664, 7.5420585, 12.839212, 6.4047265, 6.9280143, 13.099979, 6.904664, 7.5420585, 12.922939, 6.384966, 6.9034257, 13.11478, 6.619231, 7.236095, 13.11478, 6.619231, 7.236095, 12.922939, 6.384966, 6.9034257, 13.179651, 6.491927, 7.1159973, 13.099979, 6.904664, 7.5420585, 13.577553, 6.959513, 7.7937083, 13.384867, 7.3345337, 8.193618, 13.099979, 6.904664, 7.5420585, 13.501598, 6.832964, 7.5911536, 13.577553, 6.959513, 7.7937083, 14.043669, 6.938242, 7.9677033, 13.745743, 7.551609, 8.966543, 13.384867, 7.3345337, 8.193618, 13.745743, 7.551609, 8.966543, 14.043669, 6.938242, 7.9677033, 14.200254, 7.008704, 8.294954, 13.745743, 7.551609, 8.966543, 14.200254, 7.008704, 8.294954, 14.049522, 7.2166758, 8.535611, 14.115025, 7.3930063, 9.158257, 14.070913, 7.495411, 9.618036, 13.745743, 7.551609, 8.966543, 14.070913, 7.495411, 9.618036, 14.115025, 7.3930063, 9.158257, 14.474133, 7.170848, 9.13312, 14.070913, 7.495411, 9.618036, 14.474133, 7.170848, 9.13312, 14.516771, 7.173098, 9.291218, 13.745743, 7.551609, 8.966543, 13.068455, 7.0369205, 8.661718, 13.384867, 7.3345337, 8.193618, 13.402321, 7.300085, 8.886555, 13.068455, 7.0369205, 8.661718, 13.745743, 7.551609, 8.966543, 13.068455, 7.0369205, 8.661718, 13.402321, 7.300085, 8.886555, 13.049887, 6.9149427, 8.958126, 13.068455, 7.0369205, 8.661718, 13.049887, 6.9149427, 8.958126, 12.977623, 6.9004726, 8.814155, 13.745743, 7.551609, 8.966543, 13.079823, 7.007822, 9.05171, 13.402321, 7.300085, 8.886555, 13.745743, 7.551609, 8.966543, 13.15687, 7.046695, 9.214457, 13.079823, 7.007822, 9.05171, 14.049522, 7.2166758, 8.535611, 14.337751, 7.1039696, 8.595517, 13.745743, 7.551609, 8.966543, 14.049522, 7.2166758, 8.535611, 14.270794, 7.0704823, 8.454195, 14.337751, 7.1039696, 8.595517, 14.070913, 7.495411, 9.618036, 13.535355, 7.2501936, 9.400614, 13.745743, 7.551609, 8.966543, 13.535355, 7.2501936, 9.400614, 14.070913, 7.495411, 9.618036, 13.583624, 7.1059093, 9.775603, 13.535355, 7.2501936, 9.400614, 13.583624, 7.1059093, 9.775603, 13.427256, 7.094145, 9.573523, 14.115025, 7.3930063, 9.158257, 14.337751, 7.1039696, 8.595517, 14.450401, 7.090761, 9.003662, 14.115025, 7.3930063, 9.158257, 13.745743, 7.551609, 8.966543, 14.337751, 7.1039696, 8.595517, 14.3754425, 7.195709, 10.181591, 13.968065, 7.2646875, 9.861715, 14.070913, 7.495411, 9.618036, 13.968065, 7.2646875, 9.861715, 14.3754425, 7.195709, 10.181591, 14.275515, 7.113496, 10.233852, 13.968065, 7.2646875, 9.861715, 14.275515, 7.113496, 10.233852, 13.9707985, 7.1304455, 10.03817, 13.577553, 6.959513, 7.7937083, 14.043669, 6.938242, 7.9677033, 13.384867, 7.3345337, 8.193618, 13.577553, 6.959513, 7.7937083, 13.853798, 6.849599, 7.7665267, 14.043669, 6.938242, 7.9677033, 13.501598, 6.832964, 7.5911536, 13.736696, 6.747884, 7.6274853, 13.577553, 6.959513, 7.7937083, 13.501598, 6.832964, 7.5911536, 13.669174, 6.7349615, 7.561953, 13.736696, 6.747884, 7.6274853, 13.501598, 6.832964, 7.5911536, 13.099979, 6.904664, 7.5420585, 13.535788, 6.6803236, 7.4230866, 13.11478, 6.619231, 7.236095, 13.535788, 6.6803236, 7.4230866, 13.099979, 6.904664, 7.5420585, 13.11478, 6.619231, 7.236095, 13.306187, 6.5714235, 7.228906, 13.535788, 6.6803236, 7.4230866, 14.818359, 7.5795794, 9.704287, 14.319729, 7.1040306, 10.273739, 14.231741, 7.193877, 10.164581, 14.818359, 7.5795794, 9.704287, 15.293608, 7.170624, 10.134708, 14.319729, 7.1040306, 10.273739, 14.53705, 7.1410728, 9.124798, 14.520617, 7.3097606, 9.621042, 14.407236, 7.130357, 9.437237, 14.53705, 7.1410728, 9.124798, 14.818359, 7.5795794, 9.704287, 14.520617, 7.3097606, 9.621042, 17.50299, 6.374856, 7.3705115, 16.95148, 6.951913, 7.9566584, 16.564781, 6.648938, 7.610077, 17.50299, 6.374856, 7.3705115, 17.578213, 6.4056153, 7.4395566, 16.95148, 6.951913, 7.9566584, 16.95148, 6.951913, 7.9566584, 16.337984, 6.7154865, 7.697535, 16.564781, 6.648938, 7.610077, 16.95148, 6.951913, 7.9566584, 16.44146, 6.978112, 7.98315, 16.337984, 6.7154865, 7.697535, 16.44146, 6.978112, 7.98315, 16.95148, 6.951913, 7.9566584, 16.283978, 7.4333143, 8.50886, 16.44146, 6.978112, 7.98315, 16.283978, 7.4333143, 8.50886, 15.729202, 6.886676, 7.930798, 16.44146, 6.978112, 7.98315, 15.729202, 6.886676, 7.930798, 16.134073, 6.774377, 7.7760997, 15.729202, 6.886676, 7.930798, 15.686707, 7.0922894, 8.219696, 15.639549, 6.8975234, 8.001675, 15.729202, 6.886676, 7.930798, 16.283978, 7.4333143, 8.50886, 15.686707, 7.0922894, 8.219696, 15.082333, 7.0959883, 8.468272, 14.956093, 7.31973, 8.992256, 14.942156, 7.0999613, 8.635822, 15.082333, 7.0959883, 8.468272, 15.490726, 7.667551, 9.160905, 14.956093, 7.31973, 8.992256, 17.624905, 6.3763094, 7.5322666, 16.95148, 6.951913, 7.9566584, 17.578213, 6.4056153, 7.4395566, 16.95148, 6.951913, 7.9566584, 17.624905, 6.3763094, 7.5322666, 17.296997, 6.6220546, 7.8690057, 17.296997, 6.6220546, 7.8690057, 17.624905, 6.3763094, 7.5322666, 17.441767, 6.4720078, 7.9004264, 16.95148, 6.951913, 7.9566584, 16.778732, 6.9787436, 8.60397, 16.283978, 7.4333143, 8.50886, 16.95148, 6.951913, 7.9566584, 16.988663, 6.836954, 8.447882, 16.778732, 6.9787436, 8.60397, 16.685303, 6.91991, 9.205566, 15.490726, 7.667551, 9.160905, 16.283978, 7.4333143, 8.50886, 15.490726, 7.667551, 9.160905, 16.685303, 6.91991, 9.205566, 16.34667, 6.989422, 9.478991, 15.490726, 7.667551, 9.160905, 16.34667, 6.989422, 9.478991, 16.0481, 7.250002, 9.387381, 15.358458, 7.458866, 9.642784, 14.818359, 7.5795794, 9.704287, 15.490726, 7.667551, 9.160905, 14.818359, 7.5795794, 9.704287, 15.358458, 7.458866, 9.642784, 15.46587, 7.1714168, 10.046328, 14.818359, 7.5795794, 9.704287, 15.46587, 7.1714168, 10.046328, 15.293608, 7.170624, 10.134708, 15.490726, 7.667551, 9.160905, 15.686707, 7.0922894, 8.219696, 16.283978, 7.4333143, 8.50886, 15.509426, 7.387882, 8.707602, 15.686707, 7.0922894, 8.219696, 15.490726, 7.667551, 9.160905, 15.686707, 7.0922894, 8.219696, 15.509426, 7.387882, 8.707602, 15.334177, 6.938472, 8.243979, 15.686707, 7.0922894, 8.219696, 15.334177, 6.938472, 8.243979, 15.486206, 6.9294643, 8.125624, 15.490726, 7.667551, 9.160905, 15.250263, 7.0548754, 8.33129, 15.509426, 7.387882, 8.707602, 15.490726, 7.667551, 9.160905, 15.082333, 7.0959883, 8.468272, 15.250263, 7.0548754, 8.33129, 16.0481, 7.250002, 9.387381, 16.040924, 7.0956764, 9.734488, 15.490726, 7.667551, 9.160905, 16.0481, 7.250002, 9.387381, 16.186634, 7.0604343, 9.615782, 16.040924, 7.0956764, 9.734488, 14.818359, 7.5795794, 9.704287, 14.956093, 7.31973, 8.992256, 15.490726, 7.667551, 9.160905, 14.956093, 7.31973, 8.992256, 14.818359, 7.5795794, 9.704287, 14.53705, 7.1410728, 9.124798, 14.956093, 7.31973, 8.992256, 14.53705, 7.1410728, 9.124798, 14.7350445, 7.1373453, 8.888584, 15.358458, 7.458866, 9.642784, 16.040924, 7.0956764, 9.734488, 15.592907, 7.0656147, 9.958109, 15.358458, 7.458866, 9.642784, 15.490726, 7.667551, 9.160905, 16.040924, 7.0956764, 9.734488, 14.231741, 7.193877, 10.164581, 14.520617, 7.3097606, 9.621042, 14.818359, 7.5795794, 9.704287, 14.520617, 7.3097606, 9.621042, 14.231741, 7.193877, 10.164581, 14.1518955, 7.1016655, 10.051222, 14.520617, 7.3097606, 9.621042, 14.1518955, 7.1016655, 10.051222, 14.318668, 7.147036, 9.652245, 16.778732, 6.9787436, 8.60397, 16.685303, 6.91991, 9.205566, 16.283978, 7.4333143, 8.50886, 16.778732, 6.9787436, 8.60397, 16.869965, 6.8293467, 8.917667, 16.685303, 6.91991, 9.205566, 16.988663, 6.836954, 8.447882, 16.991056, 6.717228, 8.721294, 16.778732, 6.9787436, 8.60397, 16.988663, 6.836954, 8.447882, 17.05367, 6.7085714, 8.62688, 16.991056, 6.717228, 8.721294, 16.988663, 6.836954, 8.447882, 16.95148, 6.951913, 7.9566584, 17.18201, 6.656856, 8.4283905, 17.296997, 6.6220546, 7.8690057, 17.18201, 6.656856, 8.4283905, 16.95148, 6.951913, 7.9566584, 17.296997, 6.6220546, 7.8690057, 17.347916, 6.550868, 8.092527, 17.18201, 6.656856, 8.4283905, 14.640917, 7.231346, 10.77033, 14.15225, 7.201992, 10.013128, 14.311281, 7.246917, 9.992706, 14.640917, 7.231346, 10.77033, 13.950329, 6.9789553, 10.945974, 14.15225, 7.201992, 10.013128, 15.086668, 6.645689, 10.496582, 14.715862, 7.027137, 10.425049, 14.856575, 6.812086, 10.301319, 15.086668, 6.645689, 10.496582, 14.640917, 7.231346, 10.77033, 14.715862, 7.027137, 10.425049, 15.405359, 4.506928, 13.294855, 15.268087, 5.398923, 12.871037, 15.5953045, 5.062126, 12.496258, 15.405359, 4.506928, 13.294855, 15.330811, 4.5490294, 13.357082, 15.268087, 5.398923, 12.871037, 15.268087, 5.398923, 12.871037, 15.614843, 5.2105064, 12.296331, 15.5953045, 5.062126, 12.496258, 15.268087, 5.398923, 12.871037, 15.418043, 5.546952, 12.416988, 15.614843, 5.2105064, 12.296331, 15.418043, 5.546952, 12.416988, 15.268087, 5.398923, 12.871037, 15.162661, 6.218977, 12.31654, 15.418043, 5.546952, 12.416988, 15.162661, 6.218977, 12.31654, 15.6678505, 5.60172, 11.757086, 15.418043, 5.546952, 12.416988, 15.6678505, 5.60172, 11.757086, 15.632406, 5.343074, 12.11624, 15.6678505, 5.60172, 11.757086, 15.4920025, 5.9232817, 11.72563, 15.641216, 5.6641035, 11.663549, 15.6678505, 5.60172, 11.757086, 15.162661, 6.218977, 12.31654, 15.4920025, 5.9232817, 11.72563, 15.469013, 6.1774178, 11.125845, 15.1226845, 6.643654, 10.969555, 15.370789, 6.2906885, 10.961456, 15.469013, 6.1774178, 11.125845, 14.925304, 6.9021482, 11.535374, 15.1226845, 6.643654, 10.969555, 15.222748, 4.557292, 13.366293, 15.268087, 5.398923, 12.871037, 15.330811, 4.5490294, 13.357082, 15.268087, 5.398923, 12.871037, 15.222748, 4.557292, 13.366293, 15.117676, 4.9987955, 13.081551, 15.117676, 4.9987955, 13.081551, 15.222748, 4.557292, 13.366293, 14.994282, 4.8528996, 13.149015, 15.268087, 5.398923, 12.871037, 14.755228, 5.765085, 12.572195, 15.162661, 6.218977, 12.31654, 15.268087, 5.398923, 12.871037, 14.77644, 5.523675, 12.745871, 14.755228, 5.765085, 12.572195, 14.232555, 6.018609, 12.3237705, 14.925304, 6.9021482, 11.535374, 15.162661, 6.218977, 12.31654, 14.925304, 6.9021482, 11.535374, 14.232555, 6.018609, 12.3237705, 14.130428, 6.281682, 11.9799385, 14.925304, 6.9021482, 11.535374, 14.130428, 6.281682, 11.9799385, 14.391569, 6.5280485, 11.829561, 14.462694, 6.9798803, 11.226421, 14.640917, 7.231346, 10.77033, 14.925304, 6.9021482, 11.535374, 14.640917, 7.231346, 10.77033, 14.462694, 6.9798803, 11.226421, 13.968893, 6.899996, 11.121994, 14.640917, 7.231346, 10.77033, 13.968893, 6.899996, 11.121994, 13.950329, 6.9789553, 10.945974, 14.925304, 6.9021482, 11.535374, 15.4920025, 5.9232817, 11.72563, 15.162661, 6.218977, 12.31654, 15.217165, 6.445463, 11.55835, 15.4920025, 5.9232817, 11.72563, 14.925304, 6.9021482, 11.535374, 15.4920025, 5.9232817, 11.72563, 15.217165, 6.445463, 11.55835, 15.550241, 5.8804913, 11.346338, 15.4920025, 5.9232817, 11.72563, 15.550241, 5.8804913, 11.346338, 15.594999, 5.783677, 11.508028, 14.925304, 6.9021482, 11.535374, 15.519932, 6.0406504, 11.294071, 15.217165, 6.445463, 11.55835, 14.925304, 6.9021482, 11.535374, 15.469013, 6.1774178, 11.125845, 15.519932, 6.0406504, 11.294071, 14.391569, 6.5280485, 11.829561, 14.036055, 6.5608177, 11.683962, 14.925304, 6.9021482, 11.535374, 14.391569, 6.5280485, 11.829561, 14.080282, 6.442515, 11.830221, 14.036055, 6.5608177, 11.683962, 14.640917, 7.231346, 10.77033, 15.1226845, 6.643654, 10.969555, 14.925304, 6.9021482, 11.535374, 15.1226845, 6.643654, 10.969555, 14.640917, 7.231346, 10.77033, 15.086668, 6.645689, 10.496582, 15.1226845, 6.643654, 10.969555, 15.086668, 6.645689, 10.496582, 15.225153, 6.48761, 10.729311, 14.462694, 6.9798803, 11.226421, 14.036055, 6.5608177, 11.683962, 13.989519, 6.7390223, 11.216955, 14.462694, 6.9798803, 11.226421, 14.925304, 6.9021482, 11.535374, 14.036055, 6.5608177, 11.683962, 14.311281, 7.246917, 9.992706, 14.715862, 7.027137, 10.425049, 14.640917, 7.231346, 10.77033, 14.715862, 7.027137, 10.425049, 14.311281, 7.246917, 9.992706, 14.403456, 7.132376, 9.914658, 14.715862, 7.027137, 10.425049, 14.403456, 7.132376, 9.914658, 14.700848, 6.947154, 10.176794, 14.755228, 5.765085, 12.572195, 14.232555, 6.018609, 12.3237705, 15.162661, 6.218977, 12.31654, 14.755228, 5.765085, 12.572195, 14.395441, 5.764677, 12.524948, 14.232555, 6.018609, 12.3237705, 14.77644, 5.523675, 12.745871, 14.502789, 5.5489907, 12.638502, 14.755228, 5.765085, 12.572195, 14.77644, 5.523675, 12.745871, 14.557829, 5.4834146, 12.714375, 14.502789, 5.5489907, 12.638502, 14.77644, 5.523675, 12.745871, 15.268087, 5.398923, 12.871037, 14.671074, 5.3173504, 12.858131, 15.117676, 4.9987955, 13.081551, 14.671074, 5.3173504, 12.858131, 15.268087, 5.398923, 12.871037, 15.117676, 4.9987955, 13.081551, 14.877726, 5.0318, 13.048248, 14.671074, 5.3173504, 12.858131, 15.855707, 1.5785854, 8.303576, 15.537302, 3.513335, 8.969594, 15.675556, 3.59141, 8.79463, 15.855707, 1.5785854, 8.303576, 15.684317, 1.4956186, 8.515871, 15.537302, 3.513335, 8.969594, 15.718592, -0.6945374, 7.979791, 15.684317, 1.4956186, 8.515871, 15.855707, 1.5785854, 8.303576, 15.718592, -0.6945374, 7.979791, 15.528423, -0.7451041, 8.231567, 15.684317, 1.4956186, 8.515871, 15.190344, 1.4642165, 8.406557, 15.070126, 3.5373333, 8.645098, 15.119511, 3.4761071, 8.8678665, 15.190344, 1.4642165, 8.406557, 15.135492, 1.5326246, 8.145638, 15.070126, 3.5373333, 8.645098, 15.578283, 1.6655294, 7.8817935, 15.624922, 3.6523986, 8.571777, 15.435907, 3.6665154, 8.430279, 15.578283, 1.6655294, 7.8817935, 15.805153, 1.6476903, 8.039993, 15.624922, 3.6523986, 8.571777, 15.119511, 3.4761071, 8.8678665, 14.720741, 5.3593645, 9.339607, 14.75539, 5.3158965, 9.486606, 15.119511, 3.4761071, 8.8678665, 15.070126, 3.5373333, 8.645098, 14.720741, 5.3593645, 9.339607, 15.675556, 3.59141, 8.79463, 15.031363, 5.349116, 9.558678, 15.124321, 5.4062643, 9.4429035, 15.675556, 3.59141, 8.79463, 15.537302, 3.513335, 8.969594, 15.031363, 5.349116, 9.558678, 15.124321, 5.4062643, 9.4429035, 15.031363, 5.349116, 9.558678, 14.700575, 6.30957, 9.911303, 15.124321, 5.4062643, 9.4429035, 14.700575, 6.30957, 9.911303, 14.765755, 6.3641186, 9.829805, 15.684317, 1.4956186, 8.515871, 15.309, 3.4624698, 9.008275, 15.537302, 3.513335, 8.969594, 15.684317, 1.4956186, 8.515871, 15.412914, 1.4456806, 8.567421, 15.309, 3.4624698, 9.008275, 15.537302, 3.513335, 8.969594, 15.309, 3.4624698, 9.008275, 14.880146, 5.309407, 9.581961, 15.537302, 3.513335, 8.969594, 14.880146, 5.309407, 9.581961, 15.031363, 5.349116, 9.558678, 15.135492, 1.5326246, 8.145638, 15.208366, 3.6164234, 8.469492, 15.070126, 3.5373333, 8.645098, 15.135492, 1.5326246, 8.145638, 15.30286, 1.6151665, 7.9359193, 15.208366, 3.6164234, 8.469492, 15.070126, 3.5373333, 8.645098, 15.208366, 3.6164234, 8.469492, 14.811323, 5.4180756, 9.224138, 15.070126, 3.5373333, 8.645098, 14.811323, 5.4180756, 9.224138, 14.720741, 5.3593645, 9.339607, 15.435907, 3.6665154, 8.430279, 15.090517, 5.4490795, 9.293634, 14.963499, 5.456666, 9.198397, 15.435907, 3.6665154, 8.430279, 15.624922, 3.6523986, 8.571777, 15.090517, 5.4490795, 9.293634, 15.805153, 1.6476903, 8.039993, 15.675556, 3.59141, 8.79463, 15.624922, 3.6523986, 8.571777, 15.805153, 1.6476903, 8.039993, 15.855707, 1.5785854, 8.303576, 15.675556, 3.59141, 8.79463, 15.624922, 3.6523986, 8.571777, 15.124321, 5.4062643, 9.4429035, 15.090517, 5.4490795, 9.293634, 15.624922, 3.6523986, 8.571777, 15.675556, 3.59141, 8.79463, 15.124321, 5.4062643, 9.4429035, 15.30286, 1.6151665, 7.9359193, 15.435907, 3.6665154, 8.430279, 15.208366, 3.6164234, 8.469492, 15.30286, 1.6151665, 7.9359193, 15.578283, 1.6655294, 7.8817935, 15.435907, 3.6665154, 8.430279, 15.080032, -0.60457873, 7.549851, 15.578283, 1.6655294, 7.8817935, 15.30286, 1.6151665, 7.9359193, 15.080032, -0.60457873, 7.549851, 15.392487, -0.5923369, 7.483996, 15.578283, 1.6655294, 7.8817935, 15.208366, 3.6164234, 8.469492, 14.963499, 5.456666, 9.198397, 14.811323, 5.4180756, 9.224138, 15.208366, 3.6164234, 8.469492, 15.435907, 3.6665154, 8.430279, 14.963499, 5.456666, 9.198397, 14.811323, 5.4180756, 9.224138, 14.963499, 5.456666, 9.198397, 14.646309, 6.401553, 9.648203, 14.811323, 5.4180756, 9.224138, 14.646309, 6.401553, 9.648203, 14.5366535, 6.3609424, 9.662085, 14.498898, 6.2705493, 9.8520975, 14.315634, 6.982005, 10.216946, 14.590864, 6.2697954, 9.924797, 14.498898, 6.2705493, 9.8520975, 14.248718, 6.9810524, 10.163109, 14.315634, 6.982005, 10.216946, 14.700575, 6.30957, 9.911303, 14.437746, 7.065465, 10.1538315, 14.765755, 6.3641186, 9.829805, 14.700575, 6.30957, 9.911303, 14.392586, 7.019636, 10.210747, 14.437746, 7.065465, 10.1538315, 14.590864, 6.2697954, 9.924797, 14.392586, 7.019636, 10.210747, 14.700575, 6.30957, 9.911303, 14.590864, 6.2697954, 9.924797, 14.315634, 6.982005, 10.216946, 14.392586, 7.019636, 10.210747, 14.73908, 6.400427, 9.720892, 14.35066, 7.092757, 10.021431, 14.646309, 6.401553, 9.648203, 14.73908, 6.400427, 9.720892, 14.416679, 7.0957565, 10.076281, 14.35066, 7.092757, 10.021431, 14.765755, 6.3641186, 9.829805, 14.416679, 7.0957565, 10.076281, 14.73908, 6.400427, 9.720892, 14.765755, 6.3641186, 9.829805, 14.437746, 7.065465, 10.1538315, 14.416679, 7.0957565, 10.076281, 14.5366535, 6.3609424, 9.662085, 14.22855, 7.0092974, 10.084545, 14.471951, 6.307063, 9.744005, 14.5366535, 6.3609424, 9.662085, 14.27281, 7.057174, 10.028643, 14.22855, 7.0092974, 10.084545, 14.646309, 6.401553, 9.648203, 14.27281, 7.057174, 10.028643, 14.5366535, 6.3609424, 9.662085, 14.646309, 6.401553, 9.648203, 14.35066, 7.092757, 10.021431, 14.27281, 7.057174, 10.028643, 15.214357, -0.7575648, 8.298452, 15.190344, 1.4642165, 8.406557, 15.412914, 1.4456806, 8.567421, 15.214357, -0.7575648, 8.298452, 14.957146, -0.7182157, 8.110065, 15.190344, 1.4642165, 8.406557, 14.471951, 6.307063, 9.744005, 14.248718, 6.9810524, 10.163109, 14.498898, 6.2705493, 9.8520975, 14.471951, 6.307063, 9.744005, 14.22855, 7.0092974, 10.084545, 14.248718, 6.9810524, 10.163109, 15.412914, 1.4456806, 8.567421, 15.119511, 3.4761071, 8.8678665, 15.309, 3.4624698, 9.008275, 15.412914, 1.4456806, 8.567421, 15.190344, 1.4642165, 8.406557, 15.119511, 3.4761071, 8.8678665, 14.957146, -0.7182157, 8.110065, 15.135492, 1.5326246, 8.145638, 15.190344, 1.4642165, 8.406557, 14.957146, -0.7182157, 8.110065, 14.888254, -0.6553638, 7.8026576, 15.135492, 1.5326246, 8.145638, 15.392487, -0.5923369, 7.483996, 15.805153, 1.6476903, 8.039993, 15.578283, 1.6655294, 7.8817935, 15.392487, -0.5923369, 7.483996, 15.648527, -0.6318462, 7.673121, 15.805153, 1.6476903, 8.039993, 15.528423, -0.7451041, 8.231567, 15.412914, 1.4456806, 8.567421, 15.684317, 1.4956186, 8.515871, 15.528423, -0.7451041, 8.231567, 15.214357, -0.7575648, 8.298452, 15.412914, 1.4456806, 8.567421, 14.888254, -0.6553638, 7.8026576, 15.30286, 1.6151665, 7.9359193, 15.135492, 1.5326246, 8.145638, 14.888254, -0.6553638, 7.8026576, 15.080032, -0.60457873, 7.549851, 15.30286, 1.6151665, 7.9359193, 15.648527, -0.6318462, 7.673121, 15.855707, 1.5785854, 8.303576, 15.805153, 1.6476903, 8.039993, 15.648527, -0.6318462, 7.673121, 15.718592, -0.6945374, 7.979791, 15.855707, 1.5785854, 8.303576, 15.309, 3.4624698, 9.008275, 14.75539, 5.3158965, 9.486606, 14.880146, 5.309407, 9.581961, 15.309, 3.4624698, 9.008275, 15.119511, 3.4761071, 8.8678665, 14.75539, 5.3158965, 9.486606, 14.880146, 5.309407, 9.581961, 14.498898, 6.2705493, 9.8520975, 14.590864, 6.2697954, 9.924797, 14.880146, 5.309407, 9.581961, 14.75539, 5.3158965, 9.486606, 14.498898, 6.2705493, 9.8520975, 14.75539, 5.3158965, 9.486606, 14.471951, 6.307063, 9.744005, 14.498898, 6.2705493, 9.8520975, 14.75539, 5.3158965, 9.486606, 14.720741, 5.3593645, 9.339607, 14.471951, 6.307063, 9.744005, 15.031363, 5.349116, 9.558678, 14.590864, 6.2697954, 9.924797, 14.700575, 6.30957, 9.911303, 15.031363, 5.349116, 9.558678, 14.880146, 5.309407, 9.581961, 14.590864, 6.2697954, 9.924797, 14.963499, 5.456666, 9.198397, 14.73908, 6.400427, 9.720892, 14.646309, 6.401553, 9.648203, 14.963499, 5.456666, 9.198397, 15.090517, 5.4490795, 9.293634, 14.73908, 6.400427, 9.720892, 15.090517, 5.4490795, 9.293634, 14.765755, 6.3641186, 9.829805, 14.73908, 6.400427, 9.720892, 15.090517, 5.4490795, 9.293634, 15.124321, 5.4062643, 9.4429035, 14.765755, 6.3641186, 9.829805, 14.720741, 5.3593645, 9.339607, 14.5366535, 6.3609424, 9.662085, 14.471951, 6.307063, 9.744005, 14.720741, 5.3593645, 9.339607, 14.811323, 5.4180756, 9.224138, 14.5366535, 6.3609424, 9.662085, -18.8297, 3.3895998, -9.473499, -19.616299, 3.2075999, -9.711899, -19.1775, 3.1662998, -9.7628, -18.8297, 3.3895998, -9.473499, -19.3743, 3.391, -9.417299, -19.616299, 3.2075999, -9.711899, -19.3743, 3.391, -9.417299, -19.611, 3.187, -9.246899, -19.616299, 3.2075999, -9.711899, 10.0335, 3.3913999, -10.0343, 13.378099, 3.3913999, -13.378599, 13.378099, 3.3913999, -10.0343, 13.378099, 3.3913999, -13.378599, 16.722599, 3.3913999, -10.0343, 13.378099, 3.3913999, -10.0343, 13.378099, 3.3913999, -6.6894, 13.378099, 3.3913999, -10.0343, 16.722599, 3.3913999, -10.0343, 13.378099, 3.3913999, -6.6894, 16.722599, 3.3913999, -10.0343, 16.722599, 3.3913999, -6.6894, 10.0335, 3.3913999, -6.6894, 13.378099, 3.3913999, -10.0343, 13.378099, 3.3913999, -6.6894, 10.0335, 3.3913999, -6.6894, 10.0335, 3.3913999, -10.0343, 13.378099, 3.3913999, -10.0343, 16.722599, 3.3913999, -10.0343, 16.4746, 3.3913999, -13.3993, 16.722599, 3.3913999, -13.109999, 16.722599, 3.3913999, -10.0343, 13.378099, 3.3913999, -13.378599, 16.4746, 3.3913999, -13.3993, 16.4746, 3.3913999, -13.3993, 15.5536995, 3.3913999, -16.3177, 16.4952, 3.3913999, -15.5392, 16.4746, 3.3913999, -13.3993, 12.7383995, 3.3913999, -15.8953, 15.5536995, 3.3913999, -16.3177, 12.7383995, 3.3913999, -15.8953, 16.4746, 3.3913999, -13.3993, 13.378099, 3.3913999, -13.378599, 13.378099, 3.3913999, -13.378599, 12.460499, 3.3913999, -16.1674, 12.7383995, 3.3913999, -15.8953, 13.378099, 3.3913999, -13.378599, 10.0335, 3.3913999, -16.7235, 12.460499, 3.3913999, -16.1674, 10.0335, 3.3913999, -16.7235, 13.378099, 3.3913999, -13.378599, 10.0335, 3.3913999, -13.378599, 10.0335, 3.3913999, -10.0343, 10.0335, 3.3913999, -13.378599, 13.378099, 3.3913999, -13.378599, 6.6889997, 3.3913999, -13.378599, 10.0335, 3.3913999, -16.7235, 10.0335, 3.3913999, -13.378599, 6.6889997, 3.3913999, -13.378599, 6.6889997, 3.3913999, -16.7235, 10.0335, 3.3913999, -16.7235, 6.6889997, 3.3913999, -10.0343, 10.0335, 3.3913999, -13.378599, 10.0335, 3.3913999, -10.0343, 6.6889997, 3.3913999, -10.0343, 6.6889997, 3.3913999, -13.378599, 10.0335, 3.3913999, -13.378599, 4.6816, 3.3913999, -13.378599, 6.6889997, 3.3913999, -16.7235, 6.6889997, 3.3913999, -13.378599, 4.6816, 3.3913999, -13.378599, 3.8384998, 3.3913999, -16.7235, 6.6889997, 3.3913999, -16.7235, 4.5101, 3.3913999, -8.955799, 4.6816, 3.3913999, -13.378599, 6.6889997, 3.3913999, -13.378599, 4.5101, 3.3913999, -8.955799, 6.6889997, 3.3913999, -13.378599, 6.6889997, 3.3913999, -10.0343, 4.6816, 3.3913999, -13.378599, 4.5101, 3.3913999, -8.955799, 4.1105, 3.3913999, -9.3386, 6.6889997, 3.3913999, -6.6894, 6.6889997, 3.3913999, -10.0343, 10.0335, 3.3913999, -10.0343, 6.6889997, 3.3913999, -6.6894, 4.5101, 3.3913999, -8.955799, 6.6889997, 3.3913999, -10.0343, 6.6889997, 3.3913999, -6.6894, 10.0335, 3.3913999, -10.0343, 10.0335, 3.3913999, -6.6894, 4.1105, 3.3913999, -9.3386, 4.5101, 3.3913999, -8.955799, 3.8492, 3.3913999, -8.198, 6.6889997, 3.3913999, -6.6894, 3.8492, 3.3913999, -8.198, 4.5101, 3.3913999, -8.955799, 4.1105, 3.3913999, -9.3386, 3.8492, 3.3913999, -8.198, 3.4176, 3.3913999, -8.714399, 3.3444998, 3.3913999, -6.6894, 3.4176, 3.3913999, -8.714399, 3.8492, 3.3913999, -8.198, 3.8492, 3.3913999, -8.198, 6.6889997, 3.3913999, -6.6894, 3.3444998, 3.3913999, -6.6894, 3.3444998, 3.3913999, -6.6894, 0, 3.3913999, -8.714399, 3.4176, 3.3913999, -8.714399, 6.6889997, 3.3913999, -3.3451, 6.6889997, 3.3913999, -6.6894, 10.0335, 3.3913999, -6.6894, 6.6889997, 3.3913999, -3.3451, 10.0335, 3.3913999, -6.6894, 10.0335, 3.3913999, -3.3451, 3.3444998, 3.3913999, -3.3451, 3.3444998, 3.3913999, -6.6894, 6.6889997, 3.3913999, -6.6894, 3.3444998, 3.3913999, -3.3451, 6.6889997, 3.3913999, -6.6894, 6.6889997, 3.3913999, -3.3451, 0, 3.3913999, -8.714399, 3.3444998, 3.3913999, -6.6894, 0, 3.3913999, -6.6894, 0, 3.3913999, -6.6894, -3.5373998, 3.3913999, -9.750299, 0, 3.3913999, -8.714399, 0, 3.3913999, -3.3451, 3.3444998, 3.3913999, -6.6894, 3.3444998, 3.3913999, -3.3451, 0, 3.3913999, -3.3451, 0, 3.3913999, -6.6894, 3.3444998, 3.3913999, -6.6894, 3.3444998, 3.3913999, -0.000100016594, 3.3444998, 3.3913999, -3.3451, 6.6889997, 3.3913999, -3.3451, 3.3444998, 3.3913999, -0.000100016594, 6.6889997, 3.3913999, -3.3451, 6.6889997, 3.3913999, -0.000100016594, 3.3444998, 3.3913999, 3.3442, 3.3444998, 3.3913999, -0.000100016594, 6.6889997, 3.3913999, -0.000100016594, 3.3444998, 3.3913999, 3.3442, 6.6889997, 3.3913999, -0.000100016594, 6.6889997, 3.3913999, 3.3442, 0, 3.3913999, 3.3442, 3.3444998, 3.3913999, -0.000100016594, 3.3444998, 3.3913999, 3.3442, 3.3444998, 3.3913999, 6.6891, 3.3444998, 3.3913999, 3.3442, 6.6889997, 3.3913999, 3.3442, 0, 3.3913999, 3.3442, 0, 3.3913999, -0.000100016594, 3.3444998, 3.3913999, -0.000100016594, 0, 3.3913999, -0.000100016594, 3.3444998, 3.3913999, -3.3451, 3.3444998, 3.3913999, -0.000100016594, 0, 3.3913999, -0.000100016594, 0, 3.3913999, -3.3451, 3.3444998, 3.3913999, -3.3451, -2.9569001, 3.3913999, 3.3442, 0, 3.3913999, -0.000100016594, 0, 3.3913999, 3.3442, -2.9569001, 3.3913999, 3.3442, -2.9569001, 3.3913999, -0.000100016594, 0, 3.3913999, -0.000100016594, -2.9569001, 3.3913999, -0.000100016594, 0, 3.3913999, -3.3451, 0, 3.3913999, -0.000100016594, -2.9569001, 3.3913999, -0.000100016594, -3.6383996, 3.3913999, -2.0795999, 0, 3.3913999, -3.3451, -3.6383996, 3.3913999, -2.0795999, 0, 3.3913999, -6.6894, 0, 3.3913999, -3.3451, -3.6383996, 3.3913999, -2.0795999, -3.3452, 3.3913999, -6.6894, 0, 3.3913999, -6.6894, -6.6896996, 3.3913999, -3.7331, -3.3452, 3.3913999, -6.6894, -3.6383996, 3.3913999, -2.0795999, -3.8484998, 3.3913999, -9.5107, 0, 3.3913999, -6.6894, -3.3452, 3.3913999, -6.6894, 0, 3.3913999, -6.6894, -3.8484998, 3.3913999, -9.5107, -3.5373998, 3.3913999, -9.750299, -3.5373998, 3.3913999, -9.750299, -3.8484998, 3.3913999, -9.5107, -4.2641, 3.3913999, -9.881699, -3.3452, 3.3913999, -6.6894, -4.2641, 3.3913999, -9.881699, -3.8484998, 3.3913999, -9.5107, -3.5373998, 3.3913999, -9.750299, -4.2641, 3.3913999, -9.881699, -3.9104, 3.3913999, -10.240799, -4.2641, 3.3913999, -9.881699, -3.9104, 3.3913999, -12.1610985, -3.9104, 3.3913999, -10.240799, -6.6896996, 3.3913999, -3.7331, -6.6896996, 3.3913999, -6.6894, -3.3452, 3.3913999, -6.6894, -10.034201, 3.3913999, -3.7331, -6.6896996, 3.3913999, -6.6894, -6.6896996, 3.3913999, -3.7331, -3.3452, 3.3913999, -6.6894, -6.6896996, 3.3913999, -10.0343, -4.2641, 3.3913999, -9.881699, -6.6896996, 3.3913999, -10.0343, -3.3452, 3.3913999, -6.6894, -6.6896996, 3.3913999, -6.6894, -4.2641, 3.3913999, -9.881699, -6.6896996, 3.3913999, -12.5385, -3.9104, 3.3913999, -12.1610985, -6.6896996, 3.3913999, -12.5385, -4.2641, 3.3913999, -9.881699, -6.6896996, 3.3913999, -10.0343, -3.9104, 3.3913999, -12.1610985, -6.6896996, 3.3913999, -12.5385, -4.2794, 3.3913999, -12.5385, -10.034201, 3.3913999, -3.7331, -10.034201, 3.3913999, -6.6894, -6.6896996, 3.3913999, -6.6894, -10.034201, 3.3913999, -6.6894, -6.6896996, 3.3913999, -10.0343, -6.6896996, 3.3913999, -6.6894, -13.378799, 3.3913999, -3.7331, -10.034201, 3.3913999, -6.6894, -10.034201, 3.3913999, -3.7331, -10.034201, 3.3913999, -10.0343, -6.6896996, 3.3913999, -12.5385, -6.6896996, 3.3913999, -10.0343, -10.034201, 3.3913999, -6.6894, -10.034201, 3.3913999, -10.0343, -6.6896996, 3.3913999, -10.0343, -10.034201, 3.3913999, -10.0343, -10.6998005, 3.3913999, -13.1763, -6.6896996, 3.3913999, -12.5385, -13.378799, 3.3913999, -3.7331, -13.378799, 3.3913999, -6.6894, -10.034201, 3.3913999, -6.6894, -13.378799, 3.3913999, -6.6894, -10.034201, 3.3913999, -10.0343, -10.034201, 3.3913999, -6.6894, -16.192, 3.3913999, -3.0972, -13.378799, 3.3913999, -6.6894, -13.378799, 3.3913999, -3.7331, -13.378799, 3.3913999, -10.0343, -10.6998005, 3.3913999, -13.1763, -10.034201, 3.3913999, -10.0343, -13.378799, 3.3913999, -6.6894, -13.378799, 3.3913999, -10.0343, -10.034201, 3.3913999, -10.0343, -13.378799, 3.3913999, -10.0343, -13.378799, 3.3913999, -13.378599, -10.6998005, 3.3913999, -13.1763, -16.192, 3.3913999, -3.0972, -16.723299, 3.3913999, -6.6894, -13.378799, 3.3913999, -6.6894, -19.754599, 3.3913999, -2.7854, -16.723299, 3.3913999, -6.6894, -16.192, 3.3913999, -3.0972, -19.754599, 3.3913999, -2.7854, -19.2812, 3.3913999, -6.6894, -16.723299, 3.3913999, -6.6894, -17.111599, 3.3913999, -0.000100016594, -19.754599, 3.3913999, -2.7854, -16.192, 3.3913999, -3.0972, -17.111599, 3.3913999, -0.000100016594, -19.9515, 3.3913999, -2.5836997, -19.754599, 3.3913999, -2.7854, -19.9515, 3.3913999, -2.5836997, -17.111599, 3.3913999, -0.000100016594, -20.068499, 3.3913999, -0.000100016594, -20.068499, 3.3913999, 3.3442, -20.068499, 3.3913999, -0.000100016594, -17.111599, 3.3913999, -0.000100016594, -20.068499, 3.3913999, 3.3442, -17.111599, 3.3913999, -0.000100016594, -17.111599, 3.3913999, 3.304, -20.068499, 3.3913999, 6.6891, -20.068499, 3.3913999, 3.3442, -17.111599, 3.3913999, 3.304, -22.332499, 3.3913999, 8.990399, -22.315199, 3.3913999, 6.6891, -20.068499, 3.3913999, 6.6891, -22.332499, 3.3913999, 8.990399, -20.068499, 3.3913999, 6.6891, -20.068499, 3.3913999, 9.1673, -20.068499, 3.3913999, 9.1673, -20.068499, 3.3913999, 6.6891, -16.723299, 3.3913999, 6.6891, -20.068499, 3.3913999, 6.6891, -17.111599, 3.3913999, 3.304, -16.723299, 3.3913999, 6.6891, -20.068499, 3.3913999, 9.1673, -16.723299, 3.3913999, 6.6891, -16.285099, 3.3913999, 9.5862, -16.723299, 3.3913999, 6.6891, -16.0544, 3.3913999, 9.808, -16.285099, 3.3913999, 9.5862, -16.0544, 3.3913999, 9.808, -16.723299, 3.3913999, 6.6891, -13.136101, 3.3913999, 6.7548, -17.111599, 3.3913999, 3.304, -13.136101, 3.3913999, 6.7548, -16.723299, 3.3913999, 6.6891, -16.0544, 3.3913999, 9.808, -13.136101, 3.3913999, 6.7548, -13.378799, 3.3913999, 10.0334, -13.378799, 3.3913999, 10.0334, -13.136101, 3.3913999, 6.7548, -10.034201, 3.3913999, 7.0772, -13.378799, 3.3913999, 10.0334, -10.034201, 3.3913999, 7.0772, -10.034201, 3.3913999, 10.0334, -13.136101, 3.3913999, 6.7548, -17.111599, 3.3913999, 3.304, -14.877501, 3.3913999, 4.6651998, -14.877501, 3.3913999, 4.6651998, -17.111599, 3.3913999, 3.304, -15.5823, 3.3913999, 4.0364, -13.136101, 3.3913999, 6.7548, -14.877501, 3.3913999, 4.6651998, -14.0949, 3.3913999, 5.5070996, 3.3444998, 3.3913999, 10.0334, 6.6889997, 3.3913999, 6.6891, 6.6889997, 3.3913999, 10.0334, 0, 3.3913999, 13.3783, 0, 3.3913999, 10.0334, 3.3444998, 3.3913999, 10.0334, 0, 3.3913999, 13.3783, 3.3444998, 3.3913999, 10.0334, 3.3444998, 3.3913999, 13.3783, 3.3444998, 3.3913999, 13.3783, 3.3444998, 3.3913999, 10.0334, 6.6889997, 3.3913999, 10.0334, 3.3444998, 3.3913999, 10.0334, 3.3444998, 3.3913999, 6.6891, 6.6889997, 3.3913999, 6.6891, 3.3444998, 3.3913999, 6.6891, 6.6889997, 3.3913999, 3.3442, 6.6889997, 3.3913999, 6.6891, 6.6889997, 3.3913999, 6.6891, 6.6889997, 3.3913999, 3.3442, 10.0335, 3.3913999, 4.2546997, 6.6889997, 3.3913999, 6.6891, 10.0335, 3.3913999, 4.2546997, 10.0335, 3.3913999, 6.6891, 6.6889997, 3.3913999, 10.0334, 6.6889997, 3.3913999, 6.6891, 10.0335, 3.3913999, 6.6891, 6.6889997, 3.3913999, 10.0334, 10.0335, 3.3913999, 6.6891, 10.0335, 3.3913999, 10.0334, 3.3444998, 3.3913999, 13.3783, 6.6889997, 3.3913999, 10.0334, 6.6889997, 3.3913999, 13.3783, 3.9143, 3.3913999, 16.5144, 3.3444998, 3.3913999, 13.3783, 6.6889997, 3.3913999, 13.3783, 3.3444998, 3.3913999, 13.3783, 3.9143, 3.3913999, 16.5144, 3.6397, 3.3913999, 16.2405, 3.9143, 3.3913999, 16.5144, 6.6889997, 3.3913999, 13.3783, 6.6889997, 3.3913999, 16.7227, -3.3452, 3.3913999, 13.3783, 0, 3.3913999, 10.0334, 0, 3.3913999, 13.3783, -3.3452, 3.3913999, 13.3783, -3.3452, 3.4796999, 10.0334, 0, 3.3913999, 10.0334, 0, 3.3913999, 10.0334, 0, 3.4796999, 6.6891, 3.3444998, 3.3913999, 6.6891, 0, 3.3913999, 10.0334, 3.3444998, 3.3913999, 6.6891, 3.3444998, 3.3913999, 10.0334, 0, 3.4796999, 6.6891, 3.3444998, 3.3913999, 3.3442, 3.3444998, 3.3913999, 6.6891, 0, 3.4796999, 6.6891, 0, 3.3913999, 3.3442, 3.3444998, 3.3913999, 3.3442, 6.6889997, 3.3913999, -0.000100016594, 10.0335, 3.3913999, -3.3451, 10.0335, 3.2212, -2.2310996, 6.6889997, 3.3913999, -0.000100016594, 6.6889997, 3.3913999, -3.3451, 10.0335, 3.3913999, -3.3451, -10.034201, 3.0033998, 6.6891, -13.136101, 3.3913999, 6.7548, -12.747801, 3.0033998, 6.3666997, -10.034201, 3.0033998, 6.6891, -10.034201, 3.3913999, 7.0772, -13.136101, 3.3913999, 6.7548, -2.5679002, 2.9782999, 7.6208997, -6.6896996, 3.4796999, 7.0772, -6.6896996, 3.0033998, 6.6891, -2.5679002, 2.9782999, 7.6208997, -2.1796002, 3.3107998, 8.009, -6.6896996, 3.4796999, 7.0772, -12.747801, 3.0033998, 6.3666997, -14.0949, 3.3913999, 5.5070996, -13.694601, 3.0033998, 5.1793, -12.747801, 3.0033998, 6.3666997, -13.136101, 3.3913999, 6.7548, -14.0949, 3.3913999, 5.5070996, -6.6896996, 3.0033998, 6.6891, -10.034201, 3.3913999, 7.0772, -10.034201, 3.0033998, 6.6891, -6.6896996, 3.0033998, 6.6891, -6.6896996, 3.4796999, 7.0772, -10.034201, 3.3913999, 7.0772, -16.723299, 3.0033998, -0.000100016594, -16.192, 3.3913999, -3.0972, -15.8036995, 3.0033998, -2.7097, -16.723299, 3.0033998, -0.000100016594, -17.111599, 3.3913999, -0.000100016594, -16.192, 3.3913999, -3.0972, -13.378799, 3.3913999, -3.7331, -15.8036995, 3.0033998, -2.7097, -16.192, 3.3913999, -3.0972, -13.378799, 3.3913999, -3.7331, -13.378799, 3.0033998, -3.3451, -15.8036995, 3.0033998, -2.7097, -2.9569001, 3.3913999, -0.000100016594, -4.0260997, 3.0033998, -1.6915, -3.6383996, 3.3913999, -2.0795999, -2.9569001, 3.3913999, -0.000100016594, -3.3452, 3.0033998, -0.000100016594, -4.0260997, 3.0033998, -1.6915, -3.6383996, 3.3913999, -2.0795999, -6.6896996, 3.0033998, -3.3451, -6.6896996, 3.3913999, -3.7331, -3.6383996, 3.3913999, -2.0795999, -4.0260997, 3.0033998, -1.6915, -6.6896996, 3.0033998, -3.3451, -10.034201, 3.3913999, -3.7331, -13.378799, 3.0033998, -3.3451, -13.378799, 3.3913999, -3.7331, -10.034201, 3.3913999, -3.7331, -10.034201, 3.0033998, -3.3451, -13.378799, 3.0033998, -3.3451, -6.6896996, 3.3913999, -3.7331, -10.034201, 3.0033998, -3.3451, -10.034201, 3.3913999, -3.7331, -6.6896996, 3.3913999, -3.7331, -6.6896996, 3.0033998, -3.3451, -10.034201, 3.0033998, -3.3451, -16.723299, 3.0033998, 2.9164999, -17.111599, 3.3913999, -0.000100016594, -16.723299, 3.0033998, -0.000100016594, -16.723299, 3.0033998, 2.9164999, -17.111599, 3.3913999, 3.304, -17.111599, 3.3913999, -0.000100016594, -14.4897995, 3.0033998, 4.2777, -15.5823, 3.3913999, 4.0364, -15.3722, 3.0033998, 3.6128, -14.4897995, 3.0033998, 4.2777, -14.877501, 3.3913999, 4.6651998, -15.5823, 3.3913999, 4.0364, -2.9569001, 3.3913999, 3.3442, -3.3452, 3.0033998, -0.000100016594, -2.9569001, 3.3913999, -0.000100016594, -2.9569001, 3.3913999, 3.3442, -3.3452, 3.0033998, 3.3442, -3.3452, 3.0033998, -0.000100016594, -2.1796002, 3.3107998, 8.009, -2.1397, 3.0033998, 5.1793, -1.7839999, 3.3913999, 5.216, -2.1796002, 3.3107998, 8.009, -2.5679002, 2.9782999, 7.6208997, -2.1397, 3.0033998, 5.1793, -6.6896996, 3.3913999, 10.0334, -2.1796002, 3.3107998, 8.009, -3.3452, 3.4796999, 10.0334, -6.6896996, 3.3913999, 10.0334, -6.6896996, 3.4796999, 7.0772, -2.1796002, 3.3107998, 8.009, -10.034201, 3.3913999, 10.0334, -6.6896996, 3.4796999, 7.0772, -6.6896996, 3.3913999, 10.0334, -10.034201, 3.3913999, 10.0334, -10.034201, 3.3913999, 7.0772, -6.6896996, 3.4796999, 7.0772, -1.7839999, 3.3913999, 5.216, 0, 3.4796999, 6.6891, -2.1796002, 3.3107998, 8.009, 0, 3.4796999, 6.6891, -1.7839999, 3.3913999, 5.216, -2.9569001, 3.3913999, 3.3442, 0, 3.4796999, 6.6891, -2.9569001, 3.3913999, 3.3442, 0, 3.3913999, 3.3442, -3.3452, 3.4796999, 10.0334, 0, 3.4796999, 6.6891, 0, 3.3913999, 10.0334, -3.3452, 3.4796999, 10.0334, -2.1796002, 3.3107998, 8.009, 0, 3.4796999, 6.6891, -2.1397, 3.0033998, 5.1793, -2.9569001, 3.3913999, 3.3442, -1.7839999, 3.3913999, 5.216, -2.1397, 3.0033998, 5.1793, -3.3452, 3.0033998, 3.3442, -2.9569001, 3.3913999, 3.3442, -14.0949, 3.3913999, 5.5070996, -14.4897995, 3.0033998, 4.2777, -13.694601, 3.0033998, 5.1793, -14.0949, 3.3913999, 5.5070996, -14.877501, 3.3913999, 4.6651998, -14.4897995, 3.0033998, 4.2777, -15.5823, 3.3913999, 4.0364, -16.723299, 3.0033998, 2.9164999, -15.3722, 3.0033998, 3.6128, -15.5823, 3.3913999, 4.0364, -17.111599, 3.3913999, 3.304, -16.723299, 3.0033998, 2.9164999, 16.722599, 3.3913999, -13.109999, 16.4746, 3.3913999, -13.3993, 16.8123, 3.1578999, -13.4637985, 10.0335, 3.0523999, -19.0296, 6.6797, 3.2777998, -18.734999, 6.6763997, 3.0570998, -19.0278, 10.0335, 3.0523999, -19.0296, 10.0335, 3.2784998, -18.7001, 6.6797, 3.2777998, -18.734999, 6.6889997, 3.3913999, -16.7235, 10.0335, 3.2784998, -18.7001, 10.0335, 3.3913999, -16.7235, 6.6889997, 3.3913999, -16.7235, 6.6797, 3.2777998, -18.734999, 10.0335, 3.2784998, -18.7001, 12.7383995, 3.3913999, -15.8953, 12.460499, 3.3913999, -16.1674, 12.747099, 3.1769, -16.1994, 4.6816, 3.3913999, -13.378599, 3.5892, 3.2033, -16.7235, 3.8384998, 3.3913999, -16.7235, 4.6816, 3.3913999, -13.378599, 4.413, 3.2091, -13.357899, 3.5892, 3.2033, -16.7235, -10.6998005, 3.3913999, -13.1763, -6.6896996, 3.175, -12.867498, -6.6896996, 3.3913999, -12.5385, -10.6998005, 3.3913999, -13.1763, -10.4046, 3.1608999, -13.429499, -6.6896996, 3.175, -12.867498, -22.8711, 3.4937, 3.3673, -23.3678, 3.4459999, 0.13709998, -23.171, 3.6251, 0.12290001, -22.8711, 3.4937, 3.3673, -23.079899, 3.314, 3.3673, -23.3678, 3.4459999, 0.13709998, -22.8711, 3.4937, 3.3673, -20.068499, 3.3913999, -0.000100016594, -20.068499, 3.3913999, 3.3442, -22.8711, 3.4937, 3.3673, -23.171, 3.6251, 0.12290001, -20.068499, 3.3913999, -0.000100016594, -23.079899, 3.314, 3.3673, -22.315199, 3.3913999, 6.6891, -22.5639, 3.2219, 6.6891, -23.079899, 3.314, 3.3673, -22.8711, 3.4937, 3.3673, -22.315199, 3.3913999, 6.6891, -22.315199, 3.3913999, 6.6891, -20.068499, 3.3913999, 3.3442, -20.068499, 3.3913999, 6.6891, -22.315199, 3.3913999, 6.6891, -22.8711, 3.4937, 3.3673, -20.068499, 3.3913999, 3.3442, -15.739201, 3.2087, 12.341301, -13.378799, 3.3913999, 10.0334, -13.378799, 3.3922, 12.5714, -15.739201, 3.2087, 12.341301, -16.0544, 3.3913999, 9.808, -13.378799, 3.3913999, 10.0334, -10.034201, 3.2142, 12.105801, -13.378799, 3.1938999, 12.808599, -13.378799, 3.3922, 12.5714, -10.034201, 3.2142, 12.105801, -10.034201, 2.9384, 12.453701, -13.378799, 3.1938999, 12.808599, -13.378799, 3.3922, 12.5714, -10.034201, 3.3913999, 10.0334, -10.034201, 3.2142, 12.105801, -13.378799, 3.3922, 12.5714, -13.378799, 3.3913999, 10.0334, -10.034201, 3.3913999, 10.0334, 0, 3.2243998, 15.7092, -3.3452, 3.1497998, 15.914499, -3.3452, 3.3913999, 15.601, 0, 3.2243998, 15.7092, 0, 2.9636998, 16.0175, -3.3452, 3.1497998, 15.914499, -3.3452, 3.3913999, 15.601, 0, 3.3913999, 13.3783, 0, 3.2243998, 15.7092, -3.3452, 3.3913999, 15.601, -3.3452, 3.3913999, 13.3783, 0, 3.3913999, 13.3783, 0, 3.2243998, 15.7092, 3.3444998, 3.3913999, 13.3783, 3.6397, 3.3913999, 16.2405, 0, 3.2243998, 15.7092, 0, 3.3913999, 13.3783, 3.3444998, 3.3913999, 13.3783, 3.6397, 3.3913999, 16.2405, 3.9143, 3.3913999, 16.5144, 3.6729999, 3.1532, 16.5718, 13.2491, 2.9931, -3.474, 16.722599, 3.3913999, -6.6894, 16.722599, 3.1513, -3.6023998, 13.2491, 2.9931, -3.474, 13.378099, 3.3913999, -6.6894, 16.722599, 3.3913999, -6.6894, 10.0335, 3.3913999, -3.3451, 13.378099, 3.3913999, -6.6894, 13.2491, 2.9931, -3.474, 10.0335, 3.3913999, -3.3451, 10.0335, 3.3913999, -6.6894, 13.378099, 3.3913999, -6.6894, 19.8151, 3.3908, -10.0177, 20.1469, 3.0588999, -6.6894, 19.8317, 3.2891998, -6.6894, 19.8151, 3.3908, -10.0177, 20.188799, 3.1655998, -10.0154, 20.1469, 3.0588999, -6.6894, 19.8317, 3.2891998, -6.6894, 20.139599, 2.779, -3.3616, 19.8317, 3.0491, -3.6023998, 19.8317, 3.2891998, -6.6894, 20.1469, 3.0588999, -6.6894, 20.139599, 2.779, -3.3616, 16.722599, 3.1513, -3.6023998, 19.8317, 3.2891998, -6.6894, 19.8317, 3.0491, -3.6023998, 16.722599, 3.1513, -3.6023998, 16.722599, 3.3913999, -6.6894, 19.8317, 3.2891998, -6.6894, 16.722599, 3.3913999, -6.6894, 19.8151, 3.3908, -10.0177, 19.8317, 3.2891998, -6.6894, 16.722599, 3.3913999, -6.6894, 16.722599, 3.3913999, -10.0343, 19.8151, 3.3908, -10.0177, 20.139599, 2.779, -3.3616, 16.722599, 3.1513, -3.6023998, 19.8317, 3.0491, -3.6023998, 20.139599, 2.779, -3.3616, 16.712599, 2.8734, -3.3451, 16.722599, 3.1513, -3.6023998, 19.767899, 3.1308, -13.4058, 19.7985, 3.3913999, -12.747999, 19.4075, 3.3913999, -13.109999, 19.767899, 3.1308, -13.4058, 20.1582, 3.1318998, -13.041399, 19.7985, 3.3913999, -12.747999, 19.767899, 3.1308, -13.4058, 16.722599, 3.3913999, -13.109999, 16.8123, 3.1578999, -13.4637985, 19.767899, 3.1308, -13.4058, 19.4075, 3.3913999, -13.109999, 16.722599, 3.3913999, -13.109999, 20.188799, 3.1655998, -10.0154, 19.7985, 3.3913999, -12.747999, 20.1582, 3.1318998, -13.041399, 20.188799, 3.1655998, -10.0154, 19.8151, 3.3908, -10.0177, 19.7985, 3.3913999, -12.747999, 16.722599, 3.3913999, -13.109999, 19.8151, 3.3908, -10.0177, 16.722599, 3.3913999, -10.0343, 19.8151, 3.3908, -10.0177, 16.722599, 3.3913999, -13.109999, 19.4075, 3.3913999, -13.109999, 19.8151, 3.3908, -10.0177, 19.4075, 3.3913999, -13.109999, 19.7985, 3.3913999, -12.747999, 16.4952, 3.3913999, -15.5392, 15.828299, 3.1678, -16.6064, 16.7698, 3.1685998, -15.8202, 16.4952, 3.3913999, -15.5392, 15.5536995, 3.3913999, -16.3177, 15.828299, 3.1678, -16.6064, 16.8123, 3.1578999, -13.4637985, 16.4952, 3.3913999, -15.5392, 16.7698, 3.1685998, -15.8202, 16.8123, 3.1578999, -13.4637985, 16.4746, 3.3913999, -13.3993, 16.4952, 3.3913999, -15.5392, 15.828299, 3.1678, -16.6064, 12.7383995, 3.3913999, -15.8953, 12.747099, 3.1769, -16.1994, 15.828299, 3.1678, -16.6064, 15.5536995, 3.3913999, -16.3177, 12.7383995, 3.3913999, -15.8953, 12.4485, 3.2073998, -18.4711, 12.299599, 2.942, -19.2491, 12.726398, 2.9599, -18.824299, 12.4485, 3.2073998, -18.4711, 12.0203, 3.1891, -18.8983, 12.299599, 2.942, -19.2491, 12.4485, 3.2073998, -18.4711, 12.747099, 3.1769, -16.1994, 12.460499, 3.3913999, -16.1674, 12.4485, 3.2073998, -18.4711, 12.726398, 2.9599, -18.824299, 12.747099, 3.1769, -16.1994, 12.299599, 2.942, -19.2491, 10.0335, 3.2784998, -18.7001, 10.0335, 3.0523999, -19.0296, 12.299599, 2.942, -19.2491, 12.0203, 3.1891, -18.8983, 10.0335, 3.2784998, -18.7001, 10.0335, 3.2784998, -18.7001, 12.460499, 3.3913999, -16.1674, 10.0335, 3.3913999, -16.7235, 12.460499, 3.3913999, -16.1674, 10.0335, 3.2784998, -18.7001, 12.0203, 3.1891, -18.8983, 12.460499, 3.3913999, -16.1674, 12.0203, 3.1891, -18.8983, 12.4485, 3.2073998, -18.4711, 5.6212, 3.2835999, -18.3475, 3.9036999, 3.1690998, -17.2696, 5.4681997, 3.0900998, -18.607199, 5.6212, 3.2835999, -18.3475, 4.0333, 3.3667998, -17.0276, 3.9036999, 3.1690998, -17.2696, 4.0333, 3.3667998, -17.0276, 6.6889997, 3.3913999, -16.7235, 3.8384998, 3.3913999, -16.7235, 6.6889997, 3.3913999, -16.7235, 4.0333, 3.3667998, -17.0276, 5.6212, 3.2835999, -18.3475, 6.6889997, 3.3913999, -16.7235, 5.6212, 3.2835999, -18.3475, 6.6797, 3.2777998, -18.734999, 3.8384998, 3.3913999, -16.7235, 3.9036999, 3.1690998, -17.2696, 4.0333, 3.3667998, -17.0276, 3.8384998, 3.3913999, -16.7235, 3.5892, 3.2033, -16.7235, 3.9036999, 3.1690998, -17.2696, 6.6763997, 3.0570998, -19.0278, 5.6212, 3.2835999, -18.3475, 5.4681997, 3.0900998, -18.607199, 6.6763997, 3.0570998, -19.0278, 6.6797, 3.2777998, -18.734999, 5.6212, 3.2835999, -18.3475, -15.2073, 3.5182998, -15.4569, -16.124199, 3.3711998, -15.150499, -15.441299, 3.3151, -15.7533, -15.2073, 3.5182998, -15.4569, -15.9035, 3.5797, -14.867699, -16.124199, 3.3711998, -15.150499, -15.9035, 3.5797, -14.867699, -13.378799, 3.3913999, -13.378599, -16.1309, 3.6227, -13.6241, -13.378799, 3.3913999, -13.378599, -15.9035, 3.5797, -14.867699, -15.2073, 3.5182998, -15.4569, -13.378799, 3.3913999, -13.378599, -15.2073, 3.5182998, -15.4569, -13.3874, 3.3918998, -15.424398, -16.1309, 3.6227, -13.6241, -16.124199, 3.3711998, -15.150499, -15.9035, 3.5797, -14.867699, -16.1309, 3.6227, -13.6241, -16.3755, 3.4225, -13.6283, -16.124199, 3.3711998, -15.150499, -13.385401, 3.1832, -15.7598, -15.2073, 3.5182998, -15.4569, -15.441299, 3.3151, -15.7533, -13.385401, 3.1832, -15.7598, -13.3874, 3.3918998, -15.424398, -15.2073, 3.5182998, -15.4569, -10.749701, 3.1581, -16.1136, -10.6998005, 3.3913999, -15.507799, -11.021601, 3.3913999, -15.7846985, -10.749701, 3.1581, -16.1136, -10.429201, 3.1564, -15.8326, -10.6998005, 3.3913999, -15.507799, -10.749701, 3.1581, -16.1136, -13.3874, 3.3918998, -15.424398, -13.385401, 3.1832, -15.7598, -10.749701, 3.1581, -16.1136, -11.021601, 3.3913999, -15.7846985, -13.3874, 3.3918998, -15.424398, -10.4046, 3.1608999, -13.429499, -10.6998005, 3.3913999, -15.507799, -10.429201, 3.1564, -15.8326, -10.4046, 3.1608999, -13.429499, -10.6998005, 3.3913999, -13.1763, -10.6998005, 3.3913999, -15.507799, -13.3874, 3.3918998, -15.424398, -10.6998005, 3.3913999, -13.1763, -13.378799, 3.3913999, -13.378599, -10.6998005, 3.3913999, -13.1763, -13.3874, 3.3918998, -15.424398, -11.021601, 3.3913999, -15.7846985, -10.6998005, 3.3913999, -13.1763, -11.021601, 3.3913999, -15.7846985, -10.6998005, 3.3913999, -15.507799, -3.9104, 3.3913999, -12.1610985, -4.0188, 3.1604998, -12.820099, -3.6476998, 3.1597998, -12.4415, -3.9104, 3.3913999, -12.1610985, -4.2794, 3.3913999, -12.5385, -4.0188, 3.1604998, -12.820099, -4.0188, 3.1604998, -12.820099, -6.6896996, 3.3913999, -12.5385, -6.6896996, 3.175, -12.867498, -4.0188, 3.1604998, -12.820099, -4.2794, 3.3913999, -12.5385, -6.6896996, 3.3913999, -12.5385, -3.5373998, 3.3913999, -9.750299, -3.6144996, 3.1664, -10.4478, -3.2335, 3.1696, -9.965099, -3.5373998, 3.3913999, -9.750299, -3.9104, 3.3913999, -10.240799, -3.6144996, 3.1664, -10.4478, -3.6144996, 3.1664, -10.4478, -3.9104, 3.3913999, -12.1610985, -3.6476998, 3.1597998, -12.4415, -3.6144996, 3.1664, -10.4478, -3.9104, 3.3913999, -10.240799, -3.9104, 3.3913999, -12.1610985, -0.0046999454, 3.1827998, -9.064699, -3.5373998, 3.3913999, -9.750299, -3.2335, 3.1696, -9.965099, -0.0046999454, 3.1827998, -9.064699, 0, 3.3913999, -8.714399, -3.5373998, 3.3913999, -9.750299, 4.1105, 3.3913999, -9.3386, 3.149, 3.1724, -9.052198, 3.8465, 3.1766, -9.664, 4.1105, 3.3913999, -9.3386, 3.4176, 3.3913999, -8.714399, 3.149, 3.1724, -9.052198, 4.1105, 3.3913999, -9.3386, 4.413, 3.2091, -13.357899, 4.6816, 3.3913999, -13.378599, 4.1105, 3.3913999, -9.3386, 3.8465, 3.1766, -9.664, 4.413, 3.2091, -13.357899, 3.149, 3.1724, -9.052198, 0, 3.3913999, -8.714399, -0.0046999454, 3.1827998, -9.064699, 3.149, 3.1724, -9.052198, 3.4176, 3.3913999, -8.714399, 0, 3.3913999, -8.714399, -19.2812, 3.3913999, -6.6894, -19.611, 3.187, -9.246899, -19.3743, 3.391, -9.417299, -19.2812, 3.3913999, -6.6894, -19.515299, 3.2122, -6.6894, -19.611, 3.187, -9.246899, -16.1255, 3.4196, -10.133099, -16.240599, 3.3848999, -8.961699, -16.7605, 3.3848, -9.501299, -16.1255, 3.4196, -10.133099, -15.5737, 3.3848999, -9.614899, -16.240599, 3.3848999, -8.961699, -16.1255, 3.4196, -10.133099, -17.049099, 3.1696, -9.7829, -16.4075, 3.2108998, -10.402899, -16.1255, 3.4196, -10.133099, -16.7605, 3.3848, -9.501299, -17.049099, 3.1696, -9.7829, -16.7605, 3.3848, -9.501299, -19.1775, 3.1662998, -9.7628, -17.049099, 3.1696, -9.7829, -16.7605, 3.3848, -9.501299, -18.8297, 3.3895998, -9.473499, -19.1775, 3.1662998, -9.7628, -16.3755, 3.4225, -13.6283, -16.1255, 3.4196, -10.133099, -16.4075, 3.2108998, -10.402899, -16.3755, 3.4225, -13.6283, -16.1309, 3.6227, -13.6241, -16.1255, 3.4196, -10.133099, -16.240599, 3.3848999, -8.961699, -13.378799, 3.3913999, -6.6894, -16.723299, 3.3913999, -6.6894, -13.378799, 3.3913999, -6.6894, -16.240599, 3.3848999, -8.961699, -15.5737, 3.3848999, -9.614899, -13.378799, 3.3913999, -6.6894, -15.5737, 3.3848999, -9.614899, -13.378799, 3.3913999, -10.0343, -16.1309, 3.6227, -13.6241, -15.5737, 3.3848999, -9.614899, -16.1255, 3.4196, -10.133099, -15.5737, 3.3848999, -9.614899, -16.1309, 3.6227, -13.6241, -13.378799, 3.3913999, -13.378599, -15.5737, 3.3848999, -9.614899, -13.378799, 3.3913999, -13.378599, -13.378799, 3.3913999, -10.0343, -19.3743, 3.391, -9.417299, -16.723299, 3.3913999, -6.6894, -19.2812, 3.3913999, -6.6894, -16.723299, 3.3913999, -6.6894, -19.3743, 3.391, -9.417299, -16.7605, 3.3848, -9.501299, -16.7605, 3.3848, -9.501299, -19.3743, 3.391, -9.417299, -18.8297, 3.3895998, -9.473499, -16.723299, 3.3913999, -6.6894, -16.7605, 3.3848, -9.501299, -16.240599, 3.3848999, -8.961699, -19.754599, 3.3913999, -2.7854, -19.9515, 3.3913999, -2.5836997, -19.9495, 3.2063, -2.8084998, -19.754599, 3.3913999, -2.7854, -19.515299, 3.2122, -6.6894, -19.2812, 3.3913999, -6.6894, -19.754599, 3.3913999, -2.7854, -19.8424, 3.2077, -3.7668996, -19.515299, 3.2122, -6.6894, -22.7335, 3.5105, -2.4837003, -23.3864, 3.3542, -2.205, -22.931599, 3.3177998, -2.7097, -22.7335, 3.5105, -2.4837003, -23.1883, 3.5481, -1.9755001, -23.3864, 3.3542, -2.205, -23.1883, 3.5481, -1.9755001, -20.068499, 3.3913999, -0.000100016594, -23.171, 3.6251, 0.12290001, -20.068499, 3.3913999, -0.000100016594, -23.1883, 3.5481, -1.9755001, -22.7335, 3.5105, -2.4837003, -20.068499, 3.3913999, -0.000100016594, -22.7335, 3.5105, -2.4837003, -19.9515, 3.3913999, -2.5836997, -23.171, 3.6251, 0.12290001, -23.3864, 3.3542, -2.205, -23.1883, 3.5481, -1.9755001, -23.171, 3.6251, 0.12290001, -23.3678, 3.4459999, 0.13709998, -23.3864, 3.3542, -2.205, -22.7335, 3.5105, -2.4837003, -19.9495, 3.2063, -2.8084998, -19.9515, 3.3913999, -2.5836997, -22.7335, 3.5105, -2.4837003, -22.931599, 3.3177998, -2.7097, -19.9495, 3.2063, -2.8084998, -22.332499, 3.3913999, 8.990399, -22.5912, 3.2112, 9.44, -22.7647, 3.2136, 9.1158, -22.7647, 3.2136, 9.1158, -22.315199, 3.3913999, 6.6891, -22.332499, 3.3913999, 8.990399, -22.7647, 3.2136, 9.1158, -22.5639, 3.2219, 6.6891, -22.315199, 3.3913999, 6.6891, -22.5912, 3.2112, 9.44, -20.068499, 3.3913999, 9.1673, -20.068499, 3.1943, 9.406899, -22.5912, 3.2112, 9.44, -22.332499, 3.3913999, 8.990399, -20.068499, 3.3913999, 9.1673, -6.2755, 3.126, 16.1251, -5.9743, 3.3983, 13.383699, -5.9743, 3.393, 15.6513, -6.2755, 3.126, 16.1251, -6.2755, 3.126, 13.6215, -5.9743, 3.3983, 13.383699, -17.1362, 3.192, 9.7157, -16.285099, 3.3913999, 9.5862, -16.2818, 3.1913998, 9.8050995, -16.285099, 3.3913999, 9.5862, -16.0544, 3.3913999, 9.808, -16.2818, 3.1913998, 9.8050995, -16.285099, 3.3913999, 9.5862, -20.068499, 3.1943, 9.406899, -20.068499, 3.3913999, 9.1673, -16.285099, 3.3913999, 9.5862, -17.1362, 3.192, 9.7157, -20.068499, 3.1943, 9.406899, -16.1122, 3.066, 12.232401, -15.739201, 3.2087, 12.341301, -15.571699, 3.0688, 12.748899, -15.739201, 3.2087, 12.341301, -16.2818, 3.1913998, 9.8050995, -16.0544, 3.3913999, 9.808, -15.739201, 3.2087, 12.341301, -16.1122, 3.066, 12.232401, -16.2818, 3.1913998, 9.8050995, -13.378799, 3.3922, 12.5714, -15.571699, 3.0688, 12.748899, -15.739201, 3.2087, 12.341301, -13.378799, 3.3922, 12.5714, -13.378799, 3.1938999, 12.808599, -15.571699, 3.0688, 12.748899, -6.4743, 3.3766, 12.7861, -5.5162, 3.398, 12.9678, -5.9743, 3.3983, 13.383699, -6.4743, 3.3766, 12.7861, -6.088, 3.3981, 12.428801, -5.5162, 3.398, 12.9678, -6.4743, 3.3766, 12.7861, -6.2755, 3.126, 13.6215, -6.7781997, 3.1009, 13.0322, -6.4743, 3.3766, 12.7861, -5.9743, 3.3983, 13.383699, -6.2755, 3.126, 13.6215, -6.4743, 3.3766, 12.7861, -10.034201, 2.9384, 12.453701, -10.034201, 3.2142, 12.105801, -6.4743, 3.3766, 12.7861, -6.7781997, 3.1009, 13.0322, -10.034201, 2.9384, 12.453701, -10.034201, 3.3913999, 10.0334, -6.4743, 3.3766, 12.7861, -10.034201, 3.2142, 12.105801, -6.4743, 3.3766, 12.7861, -10.034201, 3.3913999, 10.0334, -6.6896996, 3.3913999, 10.0334, -6.4743, 3.3766, 12.7861, -6.6896996, 3.3913999, 10.0334, -6.088, 3.3981, 12.428801, -6.6896996, 3.3913999, 10.0334, -5.5162, 3.398, 12.9678, -6.088, 3.3981, 12.428801, -5.5162, 3.398, 12.9678, -6.6896996, 3.3913999, 10.0334, -3.3452, 3.4796999, 10.0334, -5.5162, 3.398, 12.9678, -3.3452, 3.4796999, 10.0334, -3.3452, 3.3913999, 13.3783, -5.9743, 3.393, 15.6513, -6.0454, 3.135, 16.5322, -6.2755, 3.126, 16.1251, -5.9743, 3.393, 15.6513, -5.5313997, 3.3918, 16.0056, -6.0454, 3.135, 16.5322, -6.0454, 3.135, 16.5322, -3.3452, 3.3913999, 15.601, -3.3452, 3.1497998, 15.914499, -6.0454, 3.135, 16.5322, -5.5313997, 3.3918, 16.0056, -3.3452, 3.3913999, 15.601, -5.9743, 3.3983, 13.383699, -5.5313997, 3.3918, 16.0056, -5.9743, 3.393, 15.6513, -5.5313997, 3.3918, 16.0056, -5.9743, 3.3983, 13.383699, -3.3452, 3.3913999, 13.3783, -3.3452, 3.3913999, 13.3783, -5.9743, 3.3983, 13.383699, -5.5162, 3.398, 12.9678, -5.5313997, 3.3918, 16.0056, -3.3452, 3.3913999, 13.3783, -3.3452, 3.3913999, 15.601, 2.7767, 3.0992, 16.3949, 3.6397, 3.3913999, 16.2405, 3.6729999, 3.1532, 16.5718, 3.6397, 3.3913999, 16.2405, 0, 2.9636998, 16.0175, 0, 3.2243998, 15.7092, 3.6397, 3.3913999, 16.2405, 2.7767, 3.0992, 16.3949, 0, 2.9636998, 16.0175, 3.9987998, 3.4764, 18.6069, 3.9143, 3.3913999, 16.5144, 6.6889997, 3.3913999, 16.7227, 3.8212, 3.3128, 18.7347, 4.941, 3.4643, 19.3458, 4.8006997, 3.3019998, 19.5215, 3.8212, 3.3128, 18.7347, 3.9987998, 3.4764, 18.6069, 4.941, 3.4643, 19.3458, 6.6923, 3.3525, 19.0394, 4.8006997, 3.3019998, 19.5215, 4.941, 3.4643, 19.3458, 6.6923, 3.3525, 19.0394, 6.7794, 3.1271, 19.2435, 4.8006997, 3.3019998, 19.5215, 3.6729999, 3.1532, 16.5718, 3.9987998, 3.4764, 18.6069, 3.8212, 3.3128, 18.7347, 3.6729999, 3.1532, 16.5718, 3.9143, 3.3913999, 16.5144, 3.9987998, 3.4764, 18.6069, 6.6889997, 3.3913999, 16.7227, 4.941, 3.4643, 19.3458, 3.9987998, 3.4764, 18.6069, 6.6923, 3.3525, 19.0394, 4.941, 3.4643, 19.3458, 6.6889997, 3.3913999, 16.7227, 8.6452, 3.3837, 18.872, 9.5275, 3.2163, 18.6602, 8.8241, 3.2010999, 19.0991, 8.6452, 3.3837, 18.872, 9.3605995, 3.3913, 18.4371, 9.5275, 3.2163, 18.6602, 8.6452, 3.3837, 18.872, 6.7794, 3.1271, 19.2435, 6.6923, 3.3525, 19.0394, 8.6452, 3.3837, 18.872, 8.8241, 3.2010999, 19.0991, 6.7794, 3.1271, 19.2435, 9.5275, 3.2163, 18.6602, 10.388599, 3.3911998, 16.7085, 10.590099, 3.2050998, 16.7167, 9.5275, 3.2163, 18.6602, 9.3605995, 3.3913, 18.4371, 10.388599, 3.3911998, 16.7085, 6.6889997, 3.3913999, 16.7227, 8.6452, 3.3837, 18.872, 6.6923, 3.3525, 19.0394, 8.6452, 3.3837, 18.872, 6.6889997, 3.3913999, 16.7227, 10.388599, 3.3911998, 16.7085, 8.6452, 3.3837, 18.872, 10.388599, 3.3911998, 16.7085, 9.3605995, 3.3913, 18.4371, 9.8819, 3.359, 13.619101, 9.838699, 3.359, 12.8258, 10.225699, 3.399, 13.197901, 9.8819, 3.359, 13.619101, 9.4411, 3.359, 13.2192, 9.838699, 3.359, 12.8258, 9.8819, 3.359, 13.619101, 10.462399, 3.2359, 13.4067, 10.122, 3.1906, 13.8256, 9.8819, 3.359, 13.619101, 10.225699, 3.399, 13.197901, 10.462399, 3.2359, 13.4067, 12.575499, 3.4812999, 13.4044, 10.225699, 3.399, 13.197901, 12.125999, 3.6032, 13.197901, 12.575499, 3.4812999, 13.4044, 10.462399, 3.2359, 13.4067, 10.225699, 3.399, 13.197901, 6.6889997, 3.3913999, 13.3783, 10.388599, 3.3911998, 16.7085, 6.6889997, 3.3913999, 16.7227, 10.388599, 3.3911998, 16.7085, 6.6889997, 3.3913999, 13.3783, 9.4411, 3.359, 13.2192, 10.388599, 3.3911998, 16.7085, 9.4411, 3.359, 13.2192, 9.8819, 3.359, 13.619101, 6.6889997, 3.3913999, 10.0334, 9.4411, 3.359, 13.2192, 6.6889997, 3.3913999, 13.3783, 9.4411, 3.359, 13.2192, 6.6889997, 3.3913999, 10.0334, 10.0335, 3.3913999, 10.0334, 9.4411, 3.359, 13.2192, 10.0335, 3.3913999, 10.0334, 9.838699, 3.359, 12.8258, 9.8819, 3.359, 13.619101, 10.590099, 3.2050998, 16.7167, 10.388599, 3.3911998, 16.7085, 9.8819, 3.359, 13.619101, 10.122, 3.1906, 13.8256, 10.590099, 3.2050998, 16.7167, 12.125999, 3.6032, 13.197901, 13.189199, 3.2312999, 12.5394, 13.262999, 3.3165998, 13.403801, 12.125999, 3.6032, 13.197901, 12.9445, 3.6148, 12.325901, 13.189199, 3.2312999, 12.5394, 13.262999, 3.3165998, 13.403801, 12.575499, 3.4812999, 13.4044, 12.125999, 3.6032, 13.197901, 13.189199, 3.2312999, 12.5394, 12.732399, 3.3913999, 10.0334, 12.977798, 3.2031, 10.0163, 13.189199, 3.2312999, 12.5394, 12.9445, 3.6148, 12.325901, 12.732399, 3.3913999, 10.0334, 10.0335, 3.3913999, 10.0334, 10.225699, 3.399, 13.197901, 9.838699, 3.359, 12.8258, 10.225699, 3.399, 13.197901, 10.0335, 3.3913999, 10.0334, 12.9445, 3.6148, 12.325901, 12.9445, 3.6148, 12.325901, 10.0335, 3.3913999, 10.0334, 12.732399, 3.3913999, 10.0334, 10.225699, 3.399, 13.197901, 12.9445, 3.6148, 12.325901, 12.125999, 3.6032, 13.197901, 12.8249, 2.8544, 7.9231997, 12.9499, 3.3256998, 8.668, 11.173199, 3.3332, 6.6548, 10.0335, 3.3913999, 6.6891, 10.0335, 3.3913999, 4.2546997, 11.173199, 3.3332, 6.6548, 12.732399, 3.3913999, 10.0334, 13.175299, 2.4743, 8.0415, 12.977798, 3.2031, 10.0163, 13.175299, 2.4743, 8.0415, 12.732399, 3.3913999, 10.0334, 12.9499, 3.3256998, 8.668, 13.175299, 2.4743, 8.0415, 12.9499, 3.3256998, 8.668, 12.8249, 2.8544, 7.9231997, 10.0335, 3.3913999, 6.6891, 12.732399, 3.3913999, 10.0334, 10.0335, 3.3913999, 10.0334, 12.732399, 3.3913999, 10.0334, 10.0335, 3.3913999, 6.6891, 11.173199, 3.3332, 6.6548, 12.732399, 3.3913999, 10.0334, 11.173199, 3.3332, 6.6548, 12.9499, 3.3256998, 8.668, 16.722599, 3.1513, -3.6023998, 15.810999, 2.9368, -3.3060002, 13.2491, 2.9931, -3.474, 16.722599, 3.1513, -3.6023998, 16.712599, 2.8734, -3.3451, 15.810999, 2.9368, -3.3060002, 10.0335, 3.2212, -2.2310996, 10.0335, 3.3913999, -3.3451, 13.2491, 2.9931, -3.474, -16.7232, 0.6137, 0, -15.754801, 0.6137, 3.5372, -16.7232, 0.6137, 2.9164, -15.754801, 0.6137, 3.5372, -16.7232, 0.6137, 0, -13.378601, 0.6137, 0, -16.7232, 0.6137, 0, -15.803801, 0.6137, -2.7093, -13.378601, 0.6137, -3.3446002, -16.7232, 0.6137, 0, -13.378601, 0.6137, -3.3446002, -13.378601, 0.6137, 0, -13.378601, 0.6137, 0, -13.378601, 0.6137, -3.3446002, -10.033899, 0.6137, -3.3446002, -15.754801, 0.6137, 3.5372, -13.378601, 0.6137, 0, -14.489201, 0.6137, 4.2777, -13.378601, 0.6137, 0, -10.033899, 0.6137, -3.3446002, -10.033899, 0.6137, 0, -14.489201, 0.6137, 4.2777, -13.378601, 0.6137, 0, -10.033899, 0.6137, 0, -10.033899, 0.6137, 0, -10.033899, 0.6137, -3.3446002, -6.6893, 0.6137, -3.3446002, -10.033899, 0.6137, 0, -6.6893, 0.6137, -3.3446002, -6.6893, 0.6137, 0, -6.6893, 0.6137, 0, -6.6893, 0.6137, -3.3446002, -4.0259, 0.6137, -1.6911998, -6.6893, 0.6137, 0, -4.0259, 0.6137, -1.6911998, -3.3446999, 0.6137, 0, -10.033899, 0.6137, 3.3446, -10.033899, 0.6137, 0, -6.6893, 0.6137, 0, -14.489201, 0.6137, 4.2777, -10.033899, 0.6137, 0, -10.033899, 0.6137, 3.3446, -6.6893, 0.6137, 3.3446, -6.6893, 0.6137, 0, -3.3446999, 0.6137, 0, -10.033899, 0.6137, 3.3446, -6.6893, 0.6137, 0, -6.6893, 0.6137, 3.3446, -6.6893, 0.6137, 3.3446, -3.3446999, 0.6137, 0, -3.3446999, 0.6137, 3.3446, -10.033899, 0.6137, 6.6893, -14.489201, 0.6137, 4.2777, -10.033899, 0.6137, 3.3446, -10.033899, 0.6137, 6.6893, -10.033899, 0.6137, 3.3446, -6.6893, 0.6137, 3.3446, -10.033899, 0.6137, 6.6893, -6.6893, 0.6137, 3.3446, -6.6893, 0.6137, 6.6893, -10.033899, 0.6137, 6.6893, -13.6772, 0.6137, 5.2616, -14.489201, 0.6137, 4.2777, -13.6772, 0.6137, 5.2616, -10.033899, 0.6137, 6.6893, -12.7477, 0.6137, 6.3669996, -6.6893, 0.6137, 3.3446, -2.5675998, 0.0266, 7.6215, -6.6893, 0.6137, 6.6893, -2.5675998, 0.0266, 7.6215, -6.6893, 0.6137, 3.3446, -3.3446999, 0.6137, 3.3446, -2.5675998, 0.0266, 7.6215, -3.3446999, 0.6137, 3.3446, -2.1193, 0.6137, 5.2736, 10.033899, 0, -20.467, 6.6766996, 3.0570998, -19.0275, 6.6893, 0, -20.1318, 10.033899, 0, -20.467, 10.033899, 3.0523999, -19.0294, 6.6766996, 3.0570998, -19.0275, 3.0140998, 0, -16.7232, 4.4131, 3.2091, -13.357698, 3.3446, -0.3989, -12.9443, 3.0140998, 0, -16.7232, 3.5895, 3.2033, -16.7232, 4.4131, 3.2091, -13.357698, -6.6893, 0, -14.263699, -10.404301, 3.1608999, -13.429499, -9.6168995, 0, -14.127899, -6.6893, 0, -14.263699, -6.6893, 3.175, -12.866999, -10.404301, 3.1608999, -13.429499, -23.412498, -0.2913, 0.26119995, -23.0793, 3.314, 3.3674998, -23.412498, 0, 3.3446, -23.412498, -0.2913, 0.26119995, -23.3676, 3.4459999, 0.1372, -23.0793, 3.314, 3.3674998, -23.412498, 0, 3.3446, -22.563799, 3.2219, 6.6893, -23.8078, 0, 6.6893, -23.412498, 0, 3.3446, -23.0793, 3.314, 3.3674998, -22.563799, 3.2219, 6.6893, -13.378601, 0, 13.378601, -10.033899, 2.9384, 12.453701, -10.033899, 0, 13.378601, -13.378601, 0, 13.378601, -13.378601, 3.194, 12.808901, -10.033899, 2.9384, 12.453701, -3.3446002, 0, 16.7232, 0, 2.9636998, 16.0179, 0, 0, 16.7232, -3.3446002, 0, 16.7232, -3.3446002, 3.1497998, 15.9146, 0, 2.9636998, 16.0179, 21.2349, 0, -6.6893, 20.188799, 3.1655998, -10.014999, 21.8888, 0, -10.033899, 21.2349, 0, -6.6893, 20.147, 3.0588999, -6.6893, 20.188799, 3.1655998, -10.014999, 20.1586, 3.1318998, -13.0413, 20.809599, 0, -13.7309, 21.251299, 0, -13.3043995, 20.1586, 3.1318998, -13.0413, 19.7684, 3.1308, -13.405399, 20.809599, 0, -13.7309, 20.809599, 0, -13.7309, 16.8128, 3.1578999, -13.463299, 18.0241, 0, -14.301899, 20.809599, 0, -13.7309, 19.7684, 3.1308, -13.405399, 16.8128, 3.1578999, -13.463299, 21.8888, 0, -10.033899, 20.1586, 3.1318998, -13.0413, 21.251299, 0, -13.3043995, 21.8888, 0, -10.033899, 20.188799, 3.1655998, -10.014999, 20.1586, 3.1318998, -13.0413, 16.7704, 3.1685998, -15.819698, 16.3113, 0, -16.9629, 17.503, 0, -15.940498, 16.7704, 3.1685998, -15.819698, 15.8286, 3.1678, -16.6059, 16.3113, 0, -16.9629, 16.3113, 0, -16.9629, 12.747099, 3.1769, -16.199, 13.701399, 0, -17.416399, 16.3113, 0, -16.9629, 15.8286, 3.1678, -16.6059, 12.747099, 3.1769, -16.199, 18.0241, 0, -14.301899, 16.7704, 3.1685998, -15.819698, 17.503, 0, -15.940498, 18.0241, 0, -14.301899, 16.8128, 3.1578999, -13.463299, 16.7704, 3.1685998, -15.819698, 12.961399, 0, -20.5828, 12.727098, 2.9599, -18.8239, 12.2999, 2.942, -19.2491, 12.961399, 0, -20.5828, 13.423399, 0, -20.1574, 12.727098, 2.9599, -18.8239, 12.961399, 0, -20.5828, 10.033899, 3.0523999, -19.0294, 10.033899, 0, -20.467, 12.961399, 0, -20.5828, 12.2999, 2.942, -19.2491, 10.033899, 3.0523999, -19.0294, 13.701399, 0, -17.416399, 12.727098, 2.9599, -18.8239, 13.423399, 0, -20.1574, 13.701399, 0, -17.416399, 12.747099, 3.1769, -16.199, 12.727098, 2.9599, -18.8239, 3.2372, 0, -19.3395, 5.4684, 3.0900998, -18.6069, 3.9039, 3.1690998, -17.2691, 3.2372, 0, -19.3395, 4.4523, 0, -20.444399, 5.4684, 3.0900998, -18.6069, 6.6893, 0, -20.1318, 5.4684, 3.0900998, -18.6069, 4.4523, 0, -20.444399, 6.6893, 0, -20.1318, 6.6766996, 3.0570998, -19.0275, 5.4684, 3.0900998, -18.6069, 3.2372, 0, -19.3395, 3.5895, 3.2033, -16.7232, 3.0140998, 0, -16.7232, 3.2372, 0, -19.3395, 3.9039, 3.1690998, -17.2691, 3.5895, 3.2033, -16.7232, -15.441, 3.3151, -15.753099, -16.7232, 0, -16.113699, -16.1493, 0, -16.8284, -15.441, 3.3151, -15.753099, -16.1238, 3.3711998, -15.15, -16.7232, 0, -16.113699, -16.7232, 0, -16.113699, -16.3753, 3.4225, -13.627699, -16.7232, 0, -13.378599, -16.7232, 0, -16.113699, -16.1238, 3.3711998, -15.15, -16.3753, 3.4225, -13.627699, -13.378601, 0, -17.3364, -15.441, 3.3151, -15.753099, -16.1493, 0, -16.8284, -13.378601, 0, -17.3364, -13.3848, 3.1832, -15.7598, -15.441, 3.3151, -15.753099, -10.3452, 0, -16.7803, -10.4286995, 3.1564, -15.832399, -10.7493, 3.1581, -16.1132, -10.3452, 0, -16.7803, -9.983101, 0, -16.4067, -10.4286995, 3.1564, -15.832399, -10.3452, 0, -16.7803, -13.3848, 3.1832, -15.7598, -13.378601, 0, -17.3364, -10.3452, 0, -16.7803, -10.7493, 3.1581, -16.1132, -13.3848, 3.1832, -15.7598, -9.6168995, 0, -14.127899, -10.4286995, 3.1564, -15.832399, -9.983101, 0, -16.4067, -9.6168995, 0, -14.127899, -10.404301, 3.1608999, -13.429499, -10.4286995, 3.1564, -15.832399, -3.6946998, 0, -13.471199, -3.6475997, 3.1597998, -12.441198, -4.0183997, 3.1606, -12.819899, -3.6946998, 0, -13.471199, -3.2535, 0, -13.020899, -3.6475997, 3.1597998, -12.441198, -3.6946998, 0, -13.471199, -6.6893, 3.175, -12.866999, -6.6893, 0, -14.263699, -3.6946998, 0, -13.471199, -4.0183997, 3.1606, -12.819899, -6.6893, 3.175, -12.866999, -3.2331, 3.1696, -9.964899, -2.7220998, 0, -10.936998, -2.2352996, 0, -10.6031, -3.2331, 3.1696, -9.964899, -3.6138997, 3.1664, -10.447599, -2.7220998, 0, -10.936998, 0, 0, -10.7654, -3.2331, 3.1696, -9.964899, -2.2352996, 0, -10.6031, 0, 0, -10.7654, -0.0039999485, 3.1827998, -9.0644, -3.2331, 3.1696, -9.964899, -2.7220998, 0, -10.936998, -3.6475997, 3.1597998, -12.441198, -3.2535, 0, -13.020899, -2.7220998, 0, -10.936998, -3.6138997, 3.1664, -10.447599, -3.6475997, 3.1597998, -12.441198, 3.847, 3.1766, -9.663599, 2.2182999, 0, -10.525, 2.9573, -0.1021, -11.0952, 3.847, 3.1766, -9.663599, 3.1496, 3.1724, -9.052, 2.2182999, 0, -10.525, 3.3446, -0.3989, -12.9443, 3.847, 3.1766, -9.663599, 2.9573, -0.1021, -11.0952, 3.3446, -0.3989, -12.9443, 4.4131, 3.2091, -13.357698, 3.847, 3.1766, -9.663599, 2.2182999, 0, -10.525, -0.0039999485, 3.1827998, -9.0644, 0, 0, -10.7654, 2.2182999, 0, -10.525, 3.1496, 3.1724, -9.052, -0.0039999485, 3.1827998, -9.0644, -20.108599, 0, -9.592199, -19.176899, 3.1662998, -9.762199, -19.6161, 3.2075999, -9.711699, -20.108599, 0, -9.592199, -19.6286, 0, -10.1098995, -19.176899, 3.1662998, -9.762199, -19.6161, 3.2075999, -9.711699, -20.3764, 0, -6.6893, -20.108599, 0, -9.592199, -20.3764, 0, -6.6893, -19.6161, 3.2075999, -9.711699, -19.610699, 3.187, -9.246599, -20.3764, 0, -6.6893, -19.610699, 3.187, -9.246599, -19.515, 3.2122, -6.6893, -16.407099, 3.2108998, -10.4028, -17.8929, 0, -10.41, -17.1586, 0, -11.120298, -16.407099, 3.2108998, -10.4028, -17.0486, 3.1696, -9.782799, -17.8929, 0, -10.41, -16.7232, 0, -13.378599, -16.407099, 3.2108998, -10.4028, -17.1586, 0, -11.120298, -16.7232, 0, -13.378599, -16.3753, 3.4225, -13.627699, -16.407099, 3.2108998, -10.4028, -17.8929, 0, -10.41, -19.176899, 3.1662998, -9.762199, -19.6286, 0, -10.1098995, -17.8929, 0, -10.41, -17.0486, 3.1696, -9.782799, -19.176899, 3.1662998, -9.762199, -19.841799, 3.2077, -3.7668, -19.7545, 3.3913999, -2.7851996, -19.9489, 3.2063, -2.8082, -19.841799, 3.2077, -3.7668, -21.0086, -0.2504, -3.5043998, -20.1607, -0.2436, -4.5060997, -19.841799, 3.2077, -3.7668, -19.9489, 3.2063, -2.8082, -21.0086, -0.2504, -3.5043998, -20.3764, 0, -6.6893, -19.841799, 3.2077, -3.7668, -20.1607, -0.2436, -4.5060997, -20.3764, 0, -6.6893, -19.515, 3.2122, -6.6893, -19.841799, 3.2077, -3.7668, -22.9314, 3.3177998, -2.7092, -23.412498, -0.0385, -2.8684, -22.9552, -0.047599997, -3.375, -22.9314, 3.3177998, -2.7092, -23.3864, 3.3542, -2.2045999, -23.412498, -0.0385, -2.8684, -23.412498, -0.0385, -2.8684, -23.3676, 3.4459999, 0.1372, -23.412498, -0.2913, 0.26119995, -23.412498, -0.0385, -2.8684, -23.3864, 3.3542, -2.2045999, -23.3676, 3.4459999, 0.1372, -21.0086, -0.2504, -3.5043998, -22.9314, 3.3177998, -2.7092, -22.9552, -0.047599997, -3.375, -21.0086, -0.2504, -3.5043998, -19.9489, 3.2063, -2.8082, -22.9314, 3.3177998, -2.7092, -22.756899, 0, 10.033899, -22.7643, 3.2136, 9.1159, -22.5907, 3.2112, 9.440499, -22.756899, 0, 10.033899, -23.489, 0, 9.386499, -22.7643, 3.2136, 9.1159, -23.8078, 0, 6.6893, -22.7643, 3.2136, 9.1159, -23.489, 0, 9.386499, -23.8078, 0, 6.6893, -22.563799, 3.2219, 6.6893, -22.7643, 3.2136, 9.1159, -16.7232, 0, 10.8878, -17.1359, 3.192, 9.715799, -16.2818, 3.1913998, 9.8056, -16.7232, 0, 10.8878, -17.583, 0, 10.033899, -17.1359, 3.192, 9.715799, -20.0679, 0, 10.033899, -17.1359, 3.192, 9.715799, -17.583, 0, 10.033899, -20.0679, 0, 10.033899, -20.0679, 3.1943, 9.407499, -17.1359, 3.192, 9.715799, -16.111599, 3.066, 12.2328, -16.223, 0, 13.378601, -16.7232, 0, 12.878401, -16.111599, 3.066, 12.2328, -15.5713005, 3.0688999, 12.748899, -16.223, 0, 13.378601, -16.223, 0, 13.378601, -13.378601, 3.194, 12.808901, -13.378601, 0, 13.378601, -16.223, 0, 13.378601, -15.5713005, 3.0688999, 12.748899, -13.378601, 3.194, 12.808901, -16.7232, 0, 10.8878, -16.111599, 3.066, 12.2328, -16.7232, 0, 12.878401, -16.7232, 0, 10.8878, -16.2818, 3.1913998, 9.8056, -16.111599, 3.066, 12.2328, -6.6893, 0, 13.8773, -6.7777, 3.1009, 13.0326, -6.2748, 3.126, 13.622, -6.6893, 0, 13.8773, -7.1966996, 0, 13.378601, -6.7777, 3.1009, 13.0326, -10.033899, 0, 13.378601, -6.7777, 3.1009, 13.0326, -7.1966996, 0, 13.378601, -10.033899, 0, 13.378601, -10.033899, 2.9384, 12.453701, -6.7777, 3.1009, 13.0326, -6.1093993, 0, 16.7232, -6.2748, 3.1260998, 16.1257, -6.0452, 3.135, 16.5325, -6.1093993, 0, 16.7232, -6.6893, 0, 16.1455, -6.2748, 3.1260998, 16.1257, -6.1093993, 0, 16.7232, -3.3446002, 3.1497998, 15.9146, -3.3446002, 0, 16.7232, -6.1093993, 0, 16.7232, -6.0452, 3.135, 16.5325, -3.3446002, 3.1497998, 15.9146, -6.6893, 0, 13.8773, -6.2748, 3.1260998, 16.1257, -6.6893, 0, 16.1455, -6.6893, 0, 13.8773, -6.2748, 3.126, 13.622, -6.2748, 3.1260998, 16.1257, 3.3446, 0, 17.592, 2.777, 3.0992, 16.3951, 3.6736, 3.1532, 16.5721, 3.3446, 0, 17.592, 2.4738998, 0, 16.7232, 2.777, 3.0992, 16.3951, 0, 0, 16.7232, 2.777, 3.0992, 16.3951, 2.4738998, 0, 16.7232, 0, 0, 16.7232, 0, 2.9636998, 16.0179, 2.777, 3.0992, 16.3951, 3.3446, 0, 17.592, 3.8212998, 3.3128, 18.735199, 3.3446, 0, 19.1425, 3.3446, 0, 17.592, 3.6736, 3.1532, 16.5721, 3.8212998, 3.3128, 18.735199, 4.275, 0, 20.0679, 6.7795997, 3.1271, 19.243599, 6.6893, 0, 20.0679, 4.275, 0, 20.0679, 4.8012, 3.3019998, 19.5215, 6.7795997, 3.1271, 19.243599, 3.8212998, 3.3128, 18.735199, 4.275, 0, 20.0679, 3.3446, 0, 19.1425, 3.8212998, 3.3128, 18.735199, 4.8012, 3.3019998, 19.5215, 4.275, 0, 20.0679, -22.756899, 0, 10.033899, -20.0679, 3.1943, 9.407499, -20.0679, 0, 10.033899, -22.756899, 0, 10.033899, -22.5907, 3.2112, 9.440499, -20.0679, 3.1943, 9.407499, 10.572599, 0, 19.5895, 8.8245, 3.2012, 19.0993, 9.5278, 3.2163, 18.6607, 10.572599, 0, 19.5895, 10.080299, 0, 20.0679, 8.8245, 3.2012, 19.0993, 6.6893, 0, 20.0679, 8.8245, 3.2012, 19.0993, 10.080299, 0, 20.0679, 6.6893, 0, 20.0679, 6.7795997, 3.1271, 19.243599, 8.8245, 3.2012, 19.0993, 10.572599, 0, 19.5895, 10.5903, 3.2052, 16.7169, 10.572599, 0, 16.7232, 10.572599, 0, 19.5895, 9.5278, 3.2163, 18.6607, 10.5903, 3.2052, 16.7169, 10.986898, 0, 13.9188, 10.122399, 3.1906, 13.826099, 10.462799, 3.2359, 13.406799, 10.986898, 0, 13.9188, 10.572599, 0, 14.3402, 10.122399, 3.1906, 13.826099, 10.572599, 0, 16.7232, 10.122399, 3.1906, 13.826099, 10.572599, 0, 14.3402, 10.572599, 0, 16.7232, 10.5903, 3.2052, 16.7169, 10.122399, 3.1906, 13.826099, 13.8046, 0, 13.2202, 12.5756, 3.4812999, 13.4049, 13.2631, 3.3165998, 13.404301, 13.8046, 0, 13.2202, 13.105099, 0, 13.9188, 12.5756, 3.4812999, 13.4049, 13.2631, 3.3165998, 13.404301, 13.8046, 0, 10.033899, 13.8046, 0, 13.2202, 13.8046, 0, 10.033899, 13.2631, 3.3165998, 13.404301, 13.1894, 3.2312999, 12.539801, 13.8046, 0, 10.033899, 13.1894, 3.2312999, 12.539801, 12.9783, 3.2031, 10.0168, 10.986898, 0, 13.9188, 12.5756, 3.4812999, 13.4049, 13.105099, 0, 13.9188, 10.986898, 0, 13.9188, 10.462799, 3.2359, 13.406799, 12.5756, 3.4812999, 13.4049, 13.8046, 0, 10.033899, 13.175798, 2.4743, 8.042, 14.0257, 0.0442, 7.3252997, 13.8046, 0, 10.033899, 12.9783, 3.2031, 10.0168, 13.175798, 2.4743, 8.042, 20.1399, 2.779, -3.3615003, 21.09, -0.0218, -4.9179, 20.613499, 1.0832, -3.1922002, 21.09, -0.0218, -4.9179, 20.1399, 2.779, -3.3615003, 20.147, 3.0588999, -6.6893, 21.09, -0.0218, -4.9179, 20.147, 3.0588999, -6.6893, 21.2349, 0, -6.6893, 15.6619, 2.2603, -2.7998996, 20.1399, 2.779, -3.3615003, 20.613499, 1.0832, -3.1922002, 20.1399, 2.779, -3.3615003, 15.6619, 2.2603, -2.7998996, 16.7127, 2.8734, -3.3446002, 16.7127, 2.8734, -3.3446002, 15.6619, 2.2603, -2.7998996, 15.810999, 2.9368, -3.3059, 13.249599, 2.9931998, -3.4737, 15.810999, 2.9368, -3.3059, 14.217899, 2.8779, -3.3838, 14.217899, 2.8779, -3.3838, 15.810999, 2.9368, -3.3059, 15.6619, 2.2603, -2.7998996, -16.7232, 3.0033998, 2.9164, -15.754801, 0.6137, 3.5372, -15.372101, 3.0033998, 3.6132, -16.7232, 3.0033998, 2.9164, -16.7232, 0.6137, 2.9164, -15.754801, 0.6137, 3.5372, -14.489201, 3.0033998, 4.2777, -13.6772, 0.6137, 5.2616, -13.694401, 3.0033998, 5.1798, -14.489201, 3.0033998, 4.2777, -14.489201, 0.6137, 4.2777, -13.6772, 0.6137, 5.2616, -15.803801, 3.0033998, -2.7093, -16.7232, 0.6137, 0, -16.7232, 3.0033998, 0, -15.803801, 3.0033998, -2.7093, -15.803801, 0.6137, -2.7093, -16.7232, 0.6137, 0, -16.7232, 3.0033998, 0, -16.7232, 0.6137, 2.9164, -16.7232, 3.0033998, 2.9164, -16.7232, 3.0033998, 0, -16.7232, 0.6137, 0, -16.7232, 0.6137, 2.9164, -3.3446999, 3.0033998, 0, -4.0259, 0.6137, -1.6911998, -4.0259, 3.0033998, -1.6911998, -3.3446999, 3.0033998, 0, -3.3446999, 0.6137, 0, -4.0259, 0.6137, -1.6911998, -3.3446999, 3.0033998, 3.3446, -3.3446999, 0.6137, 0, -3.3446999, 3.0033998, 0, -3.3446999, 3.0033998, 3.3446, -3.3446999, 0.6137, 3.3446, -3.3446999, 0.6137, 0, -12.7477, 3.0033998, 6.3669996, -10.033899, 0.6137, 6.6893, -10.033899, 3.0033998, 6.6893, -12.7477, 3.0033998, 6.3669996, -12.7477, 0.6137, 6.3669996, -10.033899, 0.6137, 6.6893, -10.033899, 3.0033998, -3.3446002, -13.378601, 0.6137, -3.3446002, -13.378601, 3.0033998, -3.3446002, -10.033899, 3.0033998, -3.3446002, -10.033899, 0.6137, -3.3446002, -13.378601, 0.6137, -3.3446002, -6.6893, 3.0033998, -3.3446002, -10.033899, 0.6137, -3.3446002, -10.033899, 3.0033998, -3.3446002, -6.6893, 3.0033998, -3.3446002, -6.6893, 0.6137, -3.3446002, -10.033899, 0.6137, -3.3446002, -13.378601, 3.0033998, -3.3446002, -15.803801, 0.6137, -2.7093, -15.803801, 3.0033998, -2.7093, -13.378601, 3.0033998, -3.3446002, -13.378601, 0.6137, -3.3446002, -15.803801, 0.6137, -2.7093, -4.0259, 3.0033998, -1.6911998, -6.6893, 0.6137, -3.3446002, -6.6893, 3.0033998, -3.3446002, -4.0259, 3.0033998, -1.6911998, -4.0259, 0.6137, -1.6911998, -6.6893, 0.6137, -3.3446002, -6.6893, 3.0033998, 6.6893, -2.5675998, 0.0266, 7.6215, -2.5675998, 2.9782, 7.6215, -6.6893, 3.0033998, 6.6893, -6.6893, 0.6137, 6.6893, -2.5675998, 0.0266, 7.6215, -10.033899, 3.0033998, 6.6893, -6.6893, 0.6137, 6.6893, -6.6893, 3.0033998, 6.6893, -10.033899, 3.0033998, 6.6893, -10.033899, 0.6137, 6.6893, -6.6893, 0.6137, 6.6893, -2.5675998, 2.9782, 7.6215, -2.1193, 0.6137, 5.2736, -2.1397996, 3.0033998, 5.1794, -2.5675998, 2.9782, 7.6215, -2.5675998, 0.0266, 7.6215, -2.1193, 0.6137, 5.2736, -2.1193, 0.6137, 5.2736, -3.3446999, 3.0033998, 3.3446, -2.1397996, 3.0033998, 5.1794, -2.1193, 0.6137, 5.2736, -3.3446999, 0.6137, 3.3446, -3.3446999, 3.0033998, 3.3446, -13.6772, 0.6137, 5.2616, -12.7477, 3.0033998, 6.3669996, -13.694401, 3.0033998, 5.1798, -13.6772, 0.6137, 5.2616, -12.7477, 0.6137, 6.3669996, -12.7477, 3.0033998, 6.3669996, -15.754801, 0.6137, 3.5372, -14.489201, 3.0033998, 4.2777, -15.372101, 3.0033998, 3.6132, -15.754801, 0.6137, 3.5372, -14.489201, 0.6137, 4.2777, -14.489201, 3.0033998, 4.2777, -3.3446002, -1.4008, -23.412498, 0, -1.0608, -26.7572, 0, -0.9271, -23.412498, -3.3446002, -1.4008, -23.412498, -3.3446002, -1.458, -26.7572, 0, -1.0608, -26.7572, -16.7232, -1.7572, -23.412498, -13.378601, -2.7663999, -26.7572, -13.378601, -1.3177999, -23.412498, -16.7232, -1.7572, -23.412498, -16.7232, -3.0929, -26.7572, -13.378601, -2.7663999, -26.7572, 10.033899, 0, -23.412498, 13.378599, -0.8401, -26.7572, 13.378599, 0, -23.412498, 10.033899, 0, -23.412498, 10.033899, -0.8648, -26.7572, 13.378599, -0.8401, -26.7572, 3.3446, -0.3434, -23.412498, 6.6893, -1.0129, -26.7572, 6.6893, 0, -23.412498, 3.3446, -0.3434, -23.412498, 3.3446, -0.98829997, -26.7572, 6.6893, -1.0129, -26.7572, -23.412498, -2.7663999, -23.412498, -20.0679, -3.0929, -26.7572, -20.0679, -2.7663999, -23.412498, -23.412498, -2.7663999, -23.412498, -23.412498, -3.0929, -26.7572, -20.0679, -3.0929, -26.7572, -26.7572, -2.7663999, -23.412498, -23.412498, -3.0929, -26.7572, -23.412498, -2.7663999, -23.412498, -26.7572, -2.7663999, -23.412498, -26.7572, -3.0929, -26.7572, -23.412498, -3.0929, -26.7572, -23.412498, -0.7606, -16.7232, -20.0679, -1.7572, -20.0679, -20.0679, -0.43429998, -16.7232, -23.412498, -0.7606, -16.7232, -23.412498, -2.7663999, -20.0679, -20.0679, -1.7572, -20.0679, -10.033899, -0.2814, 23.412498, -6.6893, 0, 20.0679, -6.6893, -0.2814, 23.412498, -6.6893, -0.2814, 23.412498, -6.6893, 0, 20.0679, -3.3446002, 0, 20.0679, -10.033899, -0.2814, 23.412498, -10.033899, 0, 20.0679, -6.6893, 0, 20.0679, -6.6893, -0.2814, 23.412498, -3.3446002, 0, 20.0679, -3.3446002, -0.2814, 23.412498, -3.3446002, -0.2814, 23.412498, -3.3446002, 0, 20.0679, 0, 0, 20.0679, -3.3446002, -0.2814, 23.412498, 0, 0, 20.0679, 0, -0.2814, 23.412498, -13.378601, 0, 20.0679, -10.033899, 0, 20.0679, -10.033899, -0.2814, 23.412498, -13.378601, 0, 20.0679, -10.033899, -0.2814, 23.412498, -12.357901, -0.2814, 23.412498, -13.378601, -0.2814, 23.412498, -13.378601, 0, 20.0679, -12.357901, -0.2814, 23.412498, -23.412498, -0.36479998, 16.7232, -23.412498, 0, 13.378601, -20.0679, -0.36479998, 16.7232, -23.412498, 0, 13.378601, -20.0679, 0, 13.378601, -20.0679, -0.36479998, 16.7232, -23.412498, -0.7373, 23.412498, -23.412498, -0.692, 20.0679, -20.0679, -0.5589, 23.412498, -16.7232, -0.36479998, 23.412498, -16.7232, -0.36479998, 20.0679, -13.378601, -0.2814, 23.412498, 16.7232, -1.0746, 16.7232, 20.0679, -1.0746, 13.378601, 20.0679, -1.8294, 16.7232, 20.0679, -1.8294, 16.7232, 20.0679, -1.0746, 13.378601, 23.412498, -1.8294, 13.378601, 16.7232, -1.8294, 20.0679, 16.7232, -1.0746, 16.7232, 20.0679, -1.8294, 16.7232, 16.7232, -2.2719998, 23.3902, 20.0679, -1.8294, 20.0679, 20.0679, -2.2719998, 23.3902, 16.7232, -2.2719998, 23.3902, 16.7232, -1.8294, 20.0679, 20.0679, -1.8294, 20.0679, 16.7232, -1.8294, 20.0679, 16.7232, -2.2719998, 23.3902, 13.378599, -2.2719998, 23.3902, 20.0679, -2.2719998, 23.3902, 20.0679, -1.8294, 20.0679, 23.412498, -1.8294, 20.0679, 20.0679, -2.2719998, 23.3902, 23.412498, -1.8294, 20.0679, 23.412498, -2.2719998, 23.3902, 23.412498, -1.8294, 20.0679, 26.7582, -2.2719998, 23.3902, 23.412498, -2.2719998, 23.3902, 23.412498, -1.8294, 20.0679, 26.7582, -1.8294, 20.0679, 26.7582, -2.2719998, 23.3902, -10.033899, -0.9271, -23.412498, -6.6893, -1.458, -26.7572, -6.6893, -1.3241999, -23.412498, -10.033899, -0.9271, -23.412498, -10.033899, -1.8786, -26.7572, -6.6893, -1.458, -26.7572, -3.3446002, -0.9271, -16.7232, 0, -0.9271, -20.0679, 0, -0.9271, -16.7232, -3.3446002, -0.9271, -16.7232, -3.3446002, -0.9271, -20.0679, 0, -0.9271, -20.0679, -6.6893, -0.9271, -20.0679, -3.3446002, -0.9271, -20.0679, -3.3446002, -0.9271, -16.7232, 16.7232, -0.3105, -23.412498, 16.7232, -1.3382, -26.7572, 20.0679, -0.8222, -23.412498, 20.0679, 0, -16.7232, 20.0679, -0.2837, -20.0679, 23.412498, -0.456, -16.7232, 13.378599, 0, -23.412498, 13.378599, -0.8401, -26.7572, 16.7232, -0.3105, -23.412498, 16.7232, 0, -20.0679, 16.7232, -0.3105, -23.412498, 20.0679, -0.2837, -20.0679, -6.6893, 0, -16.7232, -6.6893, -0.9271, -20.0679, -3.3446002, -0.9271, -16.7232, -13.378601, -1.3177999, -23.412498, -10.033899, -1.8786, -26.7572, -10.033899, -0.9271, -23.412498, -13.378601, -1.3177999, -23.412498, -13.378601, -2.7663999, -26.7572, -10.033899, -1.8786, -26.7572, -13.210501, -0.3964, -20.032099, -10.033899, -0.9271, -23.412498, -9.318001, -0.3969, -19.413, -13.210501, -0.3964, -20.032099, -13.378601, -1.3177999, -23.412498, -10.033899, -0.9271, -23.412498, -9.318001, -0.3969, -19.413, -10.033899, -0.9271, -23.412498, -6.6893, -0.9271, -20.0679, 20.0679, -1.0746, 13.378601, 20.0679, 0, 10.033899, 23.412498, -0.35529998, 10.033899, 13.378599, -2.2719998, 23.3902, 16.7232, -2.2194998, 26.781, 13.378599, -2.2194998, 26.781, 13.378599, -2.2719998, 23.3902, 16.7232, -2.2719998, 23.3902, 16.7232, -2.2194998, 26.781, 16.7232, -2.2194998, 26.781, 16.7232, -2.2719998, 23.3902, 20.0679, -2.2719998, 23.3902, 16.7232, -2.2194998, 26.781, 20.0679, -2.2719998, 23.3902, 20.0679, -2.2194998, 26.781, 20.0679, -2.2194998, 26.781, 20.0679, -2.2719998, 23.3902, 23.412498, -2.2719998, 23.3902, 20.0679, -2.2194998, 26.781, 23.412498, -2.2719998, 23.3902, 23.412498, -2.2194998, 26.781, 13.378599, -1.0746, 20.0679, 15.0508995, -0.40039998, 16.7232, 16.7232, -1.0746, 16.7232, 16.7232, -1.0746, 13.378601, 16.7232, 0, 10.033899, 20.0679, 0, 10.033899, 20.0679, 0, 10.033899, 20.0679, -1.0746, 13.378601, 16.7232, -1.0746, 13.378601, 6.6893, -1.1209999, 26.7572, 10.033899, -0.501, 23.412498, 10.033899, -1.1209999, 26.7572, 6.6893, -1.1209999, 26.7572, 6.6893, -0.2814, 23.412498, 10.033899, -0.501, 23.412498, 10.033899, -1.1209999, 26.7572, 13.378599, -2.2719998, 23.3902, 13.378599, -2.2194998, 26.781, 10.033899, -1.1209999, 26.7572, 10.033899, -0.501, 23.412498, 13.378599, -2.2719998, 23.3902, 0, -1.1209999, 26.7572, 3.3446, -0.2814, 23.412498, 3.3446, -1.1209999, 26.7572, 3.3446, -1.1209999, 26.7572, 3.3446, -0.2814, 23.412498, 6.6893, -0.2814, 23.412498, 3.3446, -1.1209999, 26.7572, 6.6893, -0.2814, 23.412498, 6.6893, -1.1209999, 26.7572, 0, -1.1209999, 26.7572, 0, -0.2814, 23.412498, 3.3446, -0.2814, 23.412498, -3.3446002, -1.1209999, 26.7572, 0, -0.2814, 23.412498, 0, -1.1209999, 26.7572, -3.3446002, -1.1209999, 26.7572, -3.3446002, -0.2814, 23.412498, 0, -0.2814, 23.412498, -6.6893, -1.1209999, 26.7572, -3.3446002, -0.2814, 23.412498, -3.3446002, -1.1209999, 26.7572, -6.6893, -1.1209999, 26.7572, -6.6893, -0.2814, 23.412498, -3.3446002, -0.2814, 23.412498, -10.033899, -1.1209999, 26.7572, -6.6893, -0.2814, 23.412498, -6.6893, -1.1209999, 26.7572, -10.033899, -1.1209999, 26.7572, -10.033899, -0.2814, 23.412498, -6.6893, -0.2814, 23.412498, -10.033899, -1.1209999, 26.7572, -12.357901, -0.2814, 23.412498, -10.033899, -0.2814, 23.412498, -10.033899, -1.1209999, 26.7572, -13.378601, -0.2814, 23.412498, -12.357901, -0.2814, 23.412498, -13.378601, -0.2814, 23.412498, -10.033899, -1.1209999, 26.7572, -13.378601, -1.1209999, 26.7572, 16.7232, -1.0746, 13.378601, 15.0508995, -0.6288, 13.378601, 16.7232, 0, 10.033899, -29.294899, -0.6617, 3.3446, -25.479, -0.4588, 0.143, -25.869999, -0.2764, 3.3446, -29.294899, -0.6617, 3.3446, -27.9812, -0.6617, 0, -25.479, -0.4588, 0.143, -29.294899, -0.6617, 6.6893, -25.869999, -0.2764, 3.3446, -25.945799, -0.25779998, 6.6893, -29.294899, -0.6617, 6.6893, -29.294899, -0.6617, 3.3446, -25.869999, -0.2764, 3.3446, -20.0679, -0.5589, 23.412498, -20.0679, -0.36479998, 20.0679, -16.7232, -0.36479998, 23.412498, -20.0679, -1.1374999, 26.7572, -20.0679, -0.5589, 23.412498, -16.7232, -1.1374999, 26.7572, -16.7232, -1.1374999, 26.7572, -16.7232, -0.36479998, 23.412498, -13.378601, -1.1209999, 26.7572, -26.7572, -1.0756, 23.412498, -25.174, -0.7391, 20.0679, -23.412498, -0.7373, 23.412498, -26.7572, -1.0756, 23.412498, -26.7572, -1.0756, 20.0679, -25.174, -0.7391, 20.0679, -26.7572, -1.2701, 26.7572, -23.412498, -0.7373, 23.412498, -23.412498, -1.1374999, 26.7572, -26.7572, -1.2701, 26.7572, -26.7572, -1.0756, 23.412498, -23.412498, -0.7373, 23.412498, -23.412498, -1.1374999, 26.7572, -23.412498, -0.7373, 23.412498, -20.0679, -1.1374999, 26.7572, -29.294899, -1.0265, 16.7232, -25.9681, -0.2875, 13.378601, -25.4212, -0.5907, 16.7232, -29.294899, -1.0265, 16.7232, -29.294899, -0.6617, 13.378601, -25.9681, -0.2875, 13.378601, -25.9681, -0.2875, 13.378601, -23.412498, -0.36479998, 16.7232, -25.4212, -0.5907, 16.7232, -23.412498, -0.36479998, 16.7232, -25.9681, -0.2875, 13.378601, -23.412498, 0, 13.378601, -26.7572, -1.0756, 20.0679, -25.4212, -0.5907, 16.7232, -25.174, -0.7391, 20.0679, -26.7572, -1.0756, 20.0679, -29.294899, -1.0265, 16.7232, -25.4212, -0.5907, 16.7232, -23.412498, -0.692, 20.0679, -23.412498, -0.36479998, 16.7232, -20.0679, -0.36479998, 20.0679, -13.378601, 0, 16.7232, -10.033899, 0, 13.378601, -10.033899, 0, 16.7232, -13.378601, 0, 16.7232, -13.378601, 0, 13.378601, -10.033899, 0, 13.378601, -13.378601, 0, 20.0679, -13.378601, 0, 16.7232, -10.033899, 0, 16.7232, -13.378601, 0, 20.0679, -10.033899, 0, 16.7232, -10.033899, 0, 20.0679, -10.033899, 0, 16.7232, -6.6893, 0, 20.0679, -10.033899, 0, 20.0679, -10.033899, 0, 13.378601, -7.2641997, 0, 16.7232, -10.033899, 0, 16.7232, -6.6893, 0, 20.0679, -10.033899, 0, 16.7232, -7.2641997, 0, 16.7232, -6.6893, 0, 13.8773, -10.033899, 0, 13.378601, -7.1966996, 0, 13.378601, -7.2641997, 0, 16.7232, -10.033899, 0, 13.378601, -6.6893, 0, 13.8773, -7.2641997, 0, 16.7232, -6.6893, 0, 13.8773, -6.6893, 0, 16.1455, -6.1093993, 0, 16.7232, -7.2641997, 0, 16.7232, -6.6893, 0, 16.1455, -6.6893, 0, 20.0679, -7.2641997, 0, 16.7232, -6.6893, 0, 17.298199, -6.1093993, 0, 16.7232, -6.6893, 0, 17.298199, -7.2641997, 0, 16.7232, -6.6893, 0, 17.298199, -3.3446002, 0, 20.0679, -6.6893, 0, 20.0679, -3.3446002, 0, 20.0679, -6.6893, 0, 17.298199, -6.1093993, 0, 16.7232, -3.3446002, 0, 20.0679, -6.1093993, 0, 16.7232, -3.3446002, 0, 16.7232, -3.3446002, 0, 20.0679, -3.3446002, 0, 16.7232, 0, 0, 16.7232, -3.3446002, 0, 20.0679, 0, 0, 16.7232, 0, 0, 20.0679, 0, 0, 16.7232, 2.4261, 0, 20.0679, 0, 0, 20.0679, 2.4261, 0, 20.0679, 0, 0, 16.7232, 3.3446, 0, 17.592, 3.3446, 0, 17.592, 0, 0, 16.7232, 2.4738998, 0, 16.7232, 2.4261, 0, 20.0679, 3.3446, 0, 17.592, 3.3446, 0, 19.1425, 4.275, 0, 20.0679, 2.4261, 0, 20.0679, 3.3446, 0, 19.1425, 4.275, 0, 20.0679, 3.3446, 0, 20.986399, 2.4261, 0, 20.0679, -26.7572, -1.6710999, -17.1511, -23.412498, -2.7663999, -20.0679, -23.412498, -0.7606, -16.7232, -26.7572, -1.6710999, -17.1511, -26.7572, -1.6710999, -20.4958, -23.412498, -2.7663999, -20.0679, -29.294899, -0.6617, -13.378599, -23.412498, -0.7606, -16.7232, -23.412498, 0, -13.378599, -29.294899, -0.6617, -13.378599, -26.7572, -1.6710999, -17.1511, -23.412498, -0.7606, -16.7232, -23.412498, 0, -13.378599, -23.412498, -0.7606, -16.7232, -20.0679, -0.43429998, -16.7232, -26.7572, -1.6710999, -20.4958, -23.412498, -2.7663999, -23.412498, -23.412498, -2.7663999, -20.0679, -26.7572, -1.6710999, -20.4958, -26.7572, -2.7663999, -23.412498, -23.412498, -2.7663999, -23.412498, -23.412498, -2.7663999, -20.0679, -20.0679, -2.7663999, -23.412498, -20.0679, -1.7572, -20.0679, -23.412498, -2.7663999, -20.0679, -23.412498, -2.7663999, -23.412498, -20.0679, -2.7663999, -23.412498, -29.294899, -0.6617, -10.033899, -23.412498, 0, -13.378599, -23.412498, 0, -10.033899, -29.294899, -0.6617, -10.033899, -29.294899, -0.6617, -13.378599, -23.412498, 0, -13.378599, -29.294899, -0.6617, -6.6893, -29.294899, -0.6617, -10.033899, -23.412498, 0, -10.033899, -29.294899, -0.6617, -6.6893, -23.412498, 0, -10.033899, -23.412498, 0, -6.6893, 0, 0, -13.378599, 0, -0.9271, -16.7232, 3.0140998, 0, -16.7232, 0, -0.9271, -23.412498, 3.3446, -0.98829997, -26.7572, 3.3446, -0.3434, -23.412498, 0, -0.9271, -23.412498, 0, -1.0608, -26.7572, 3.3446, -0.98829997, -26.7572, 21.2349, 0, -6.6893, 23.412498, 0, -10.033899, 23.412498, 0, -6.6893, 21.2349, 0, -6.6893, 21.8888, 0, -10.033899, 23.412498, 0, -10.033899, 21.8888, 0, -10.033899, 21.5122, 0, -13.6136, 23.412498, 0, -10.033899, 21.8888, 0, -10.033899, 21.251299, 0, -13.3043995, 21.5122, 0, -13.6136, 21.5122, 0, -13.6136, 23.412498, 0, -13.378599, 23.412498, 0, -10.033899, 20.809599, 0, -13.7309, 21.5122, 0, -13.6136, 21.251299, 0, -13.3043995, 21.0698, 0, -13.966099, 23.412498, 0, -13.378599, 21.5122, 0, -13.6136, 20.809599, 0, -13.7309, 21.0698, 0, -13.966099, 21.5122, 0, -13.6136, 21.0698, 0, -13.966099, 20.0679, 0, -16.7232, 23.412498, 0, -13.378599, 20.809599, 0, -13.7309, 18.053999, 0, -16.7802, 21.0698, 0, -13.966099, 18.053999, 0, -16.7802, 20.0679, 0, -16.7232, 21.0698, 0, -13.966099, 20.809599, 0, -13.7309, 17.503, 0, -15.940498, 18.053999, 0, -16.7802, 17.503, 0, -15.940498, 20.809599, 0, -13.7309, 18.0241, 0, -14.301899, 16.3113, 0, -16.9629, 18.053999, 0, -16.7802, 17.503, 0, -15.940498, 17.1022, 0, -17.6197, 20.0679, 0, -16.7232, 18.053999, 0, -16.7802, 16.3113, 0, -16.9629, 17.1022, 0, -17.6197, 18.053999, 0, -16.7802, 17.1022, 0, -17.6197, 16.7232, 0, -20.0679, 20.0679, 0, -16.7232, 16.3113, 0, -16.9629, 13.748098, 0, -20.540499, 17.1022, 0, -17.6197, 13.748098, 0, -20.540499, 16.7232, 0, -20.0679, 17.1022, 0, -17.6197, 16.3113, 0, -16.9629, 13.423399, 0, -20.1574, 13.748098, 0, -20.540499, 13.423399, 0, -20.1574, 16.3113, 0, -16.9629, 13.701399, 0, -17.416399, 12.961399, 0, -20.5828, 13.748098, 0, -20.540499, 13.423399, 0, -20.1574, 13.378599, 0, -20.9734, 16.7232, 0, -20.0679, 13.748098, 0, -20.540499, 12.961399, 0, -20.5828, 13.378599, 0, -20.9734, 13.748098, 0, -20.540499, 13.378599, 0, -20.9734, 13.378599, 0, -23.412498, 16.7232, 0, -20.0679, 10.033899, 0, -20.467, 13.378599, 0, -20.9734, 12.961399, 0, -20.5828, 10.033899, 0, -23.412498, 13.378599, 0, -23.412498, 13.378599, 0, -20.9734, 10.033899, 0, -20.467, 10.033899, 0, -23.412498, 13.378599, 0, -20.9734, 6.6893, 0, -20.1318, 10.033899, 0, -23.412498, 10.033899, 0, -20.467, 6.6893, 0, -20.1318, 6.6893, 0, -23.412498, 10.033899, 0, -23.412498, 6.6893, 0, -23.412498, 6.6893, 0, -20.1318, 4.4523, 0, -20.444399, 6.6893, 0, -23.412498, 4.4523, 0, -20.444399, 3.3446, 0, -21.6384, 3.2372, 0, -19.3395, 3.3446, 0, -21.6384, 4.4523, 0, -20.444399, 3.2372, 0, -19.3395, 2.3184, 0, -20.4362, 3.3446, 0, -21.6384, 6.6893, 0, -23.412498, 10.033899, -0.8648, -26.7572, 10.033899, 0, -23.412498, 6.6893, 0, -23.412498, 6.6893, -1.0129, -26.7572, 10.033899, -0.8648, -26.7572, -20.0679, -2.7663999, -23.412498, -16.7232, -3.0929, -26.7572, -16.7232, -1.7572, -23.412498, -20.0679, -2.7663999, -23.412498, -20.0679, -3.0929, -26.7572, -16.7232, -3.0929, -26.7572, -20.0679, -1.7572, -20.0679, -16.7232, -1.7572, -23.412498, -17.2383, -0.66719997, -19.6364, -20.0679, -1.7572, -20.0679, -20.0679, -2.7663999, -23.412498, -16.7232, -1.7572, -23.412498, -17.2383, -0.66719997, -19.6364, -13.378601, -1.3177999, -23.412498, -13.210501, -0.3964, -20.032099, -17.2383, -0.66719997, -19.6364, -16.7232, -1.7572, -23.412498, -13.378601, -1.3177999, -23.412498, -20.0679, -0.36479998, 20.0679, -20.0679, -0.36479998, 16.7232, -16.7232, -0.36479998, 20.0679, -23.412498, -0.36479998, 16.7232, -20.0679, -0.36479998, 16.7232, -20.0679, -0.36479998, 20.0679, -20.0679, -0.36479998, 20.0679, -16.7232, -0.36479998, 20.0679, -16.7232, -0.36479998, 23.412498, -16.7232, -0.36479998, 20.0679, -16.7232, -0.26209998, 16.7232, -13.378601, 0, 20.0679, 20.0679, -1.8294, 20.0679, 23.412498, -1.8294, 16.7232, 23.412498, -1.8294, 20.0679, 23.412498, -1.8294, 16.7232, 26.7582, -1.8294, 20.0679, 23.412498, -1.8294, 20.0679, 23.412498, -1.8294, 16.7232, 26.7582, -1.8294, 16.7232, 26.7582, -1.8294, 20.0679, 20.0679, -1.8294, 20.0679, 20.0679, -1.8294, 16.7232, 23.412498, -1.8294, 16.7232, 20.0679, -1.8294, 16.7232, 20.0679, -1.8294, 20.0679, 16.7232, -1.8294, 20.0679, 23.412498, -1.8294, 13.378601, 23.412498, -1.8294, 16.7232, 20.0679, -1.8294, 16.7232, 23.412498, -1.8294, 13.378601, 26.7582, -1.8294, 16.7232, 23.412498, -1.8294, 16.7232, 23.412498, -1.8294, 13.378601, 26.7582, -1.8294, 13.378601, 26.7582, -1.8294, 16.7232, -6.6893, -1.3241999, -23.412498, -3.3446002, -1.458, -26.7572, -3.3446002, -1.4008, -23.412498, -6.6893, -1.3241999, -23.412498, -6.6893, -1.458, -26.7572, -3.3446002, -1.458, -26.7572, -6.6893, -0.9271, -20.0679, -3.3446002, -1.4008, -23.412498, -3.3446002, -0.9271, -20.0679, -6.6893, -0.9271, -20.0679, -6.6893, -1.3241999, -23.412498, -3.3446002, -1.4008, -23.412498, -3.3446002, -0.9271, -20.0679, 0, -0.9271, -23.412498, 0, -0.9271, -20.0679, -3.3446002, -0.9271, -20.0679, -3.3446002, -1.4008, -23.412498, 0, -0.9271, -23.412498, 20.0679, -0.8222, -23.412498, 23.412498, -1.1158999, -26.7572, 23.412498, -1.0924, -23.412498, 20.0679, -0.8222, -23.412498, 20.0679, -1.1922, -26.7572, 23.412498, -1.1158999, -26.7572, 20.0679, -0.2837, -20.0679, 20.0679, -0.8222, -23.412498, 23.412498, -1.0924, -20.0679, 26.7573, -1.3222, -26.7572, 29.564798, -1.6410999, -23.412498, 26.7582, -1.0924, -23.412498, 26.7573, -1.3222, -26.7572, 29.3928, -1.796, -26.7572, 29.564798, -1.6410999, -23.412498, 13.378599, -1.0746, 20.0679, 16.7232, -1.8294, 20.0679, 13.378599, -2.2719998, 23.3902, 13.378599, -1.0746, 20.0679, 15.0508995, -1.0746, 20.0679, 16.7232, -1.8294, 20.0679, 0, -0.9271, -20.0679, 3.3446, 0, -21.6384, 2.3184, 0, -20.4362, 0, -0.9271, -20.0679, 3.3446, -0.3434, -23.412498, 3.3446, 0, -21.6384, 0, -0.9271, -16.7232, 3.2372, 0, -19.3395, 3.0140998, 0, -16.7232, 0, -0.9271, -16.7232, 2.3184, 0, -20.4362, 3.2372, 0, -19.3395, 3.3446, 0, -21.6384, 3.3446, -0.3434, -23.412498, 6.6893, 0, -23.412498, -16.7232, 0, -16.113699, -16.7232, 0, -17.2664, -16.1493, 0, -16.8284, -16.7232, 0, -16.113699, -17.2664, 0, -16.7232, -16.7232, 0, -17.2664, -20.0679, -0.43429998, -16.7232, -16.7232, 0, -17.2664, -17.2664, 0, -16.7232, -20.0679, -0.43429998, -16.7232, -17.2383, -0.66719997, -19.6364, -16.7232, 0, -17.2664, -20.0679, -0.43429998, -16.7232, -16.7232, 0, -13.378599, -20.0679, 0, -13.378599, -16.7232, 0, -13.378599, -20.0679, -0.43429998, -16.7232, -17.2664, 0, -16.7232, -16.7232, 0, -13.378599, -17.2664, 0, -16.7232, -16.7232, 0, -16.113699, -17.2383, -0.66719997, -19.6364, -16.1493, 0, -16.8284, -16.7232, 0, -17.2664, -16.1493, 0, -16.8284, -17.2383, -0.66719997, -19.6364, -13.210501, -0.3964, -20.032099, -16.1493, 0, -16.8284, -13.210501, -0.3964, -20.032099, -13.378601, 0, -17.3364, -10.3452, 0, -16.7803, -9.7488, 0, -16.7232, -9.983101, 0, -16.4067, -10.3452, 0, -16.7803, -10.033899, 0, -17.008299, -9.7488, 0, -16.7232, -6.6893, 0, -14.263699, -9.983101, 0, -16.4067, -9.7488, 0, -16.7232, -9.983101, 0, -16.4067, -6.6893, 0, -14.263699, -9.6168995, 0, -14.127899, -6.6893, 0, -14.263699, -9.7488, 0, -16.7232, -6.6893, 0, -16.7232, -6.6893, 0, -14.263699, -6.6893, 0, -16.7232, -3.3446002, 0, -13.7127, -6.6893, 0, -14.263699, -3.3446002, 0, -13.7127, -3.6946998, 0, -13.471199, -3.6946998, 0, -13.471199, -3.3446002, 0, -13.7127, -3.0105996, 0, -13.378599, 0, 0, -13.378599, -3.0105996, 0, -13.378599, -3.3446002, 0, -13.7127, -3.6946998, 0, -13.471199, -3.0105996, 0, -13.378599, -3.2535, 0, -13.020899, 0, 0, -13.378599, -3.2535, 0, -13.020899, -3.0105996, 0, -13.378599, -2.2352996, 0, -10.6031, -3.2535, 0, -13.020899, 0, 0, -13.378599, -3.2535, 0, -13.020899, -2.2352996, 0, -10.6031, -2.7220998, 0, -10.936998, -2.2352996, 0, -10.6031, 0, 0, -13.378599, 0, 0, -10.7654, -13.210501, -0.3964, -20.032099, -10.3452, 0, -16.7803, -13.378601, 0, -17.3364, -10.3452, 0, -16.7803, -13.210501, -0.3964, -20.032099, -9.318001, -0.3969, -19.413, -10.3452, 0, -16.7803, -9.318001, -0.3969, -19.413, -10.033899, 0, -17.008299, -10.033899, 0, -17.008299, -6.6893, 0, -16.7232, -9.7488, 0, -16.7232, -10.033899, 0, -17.008299, -9.318001, -0.3969, -19.413, -6.6893, 0, -16.7232, -3.3446002, 0, -13.7127, 0, -0.9271, -16.7232, 0, 0, -13.378599, -3.3446002, 0, -13.7127, -3.3446002, -0.9271, -16.7232, 0, -0.9271, -16.7232, 0, 0, -13.378599, 2.2182999, 0, -10.525, 0, 0, -10.7654, 2.2182999, 0, -10.525, 0, 0, -13.378599, 3.3446, -0.3989, -12.9443, 2.2182999, 0, -10.525, 3.3446, -0.3989, -12.9443, 2.9573, -0.1021, -11.0952, -20.108599, 0, -9.592199, -20.0679, 0, -10.476398, -19.6286, 0, -10.1098995, -20.108599, 0, -9.592199, -20.5103, 0, -10.033899, -20.0679, 0, -10.476398, -20.3764, 0, -6.6893, -20.5103, 0, -10.033899, -20.108599, 0, -9.592199, -20.0679, 0, -13.378599, -19.6286, 0, -10.1098995, -20.0679, 0, -10.476398, -20.5103, 0, -10.033899, -20.0679, 0, -13.378599, -20.0679, 0, -10.476398, -19.6286, 0, -10.1098995, -20.0679, 0, -13.378599, -17.1586, 0, -11.120298, -19.6286, 0, -10.1098995, -17.1586, 0, -11.120298, -17.8929, 0, -10.41, -17.1586, 0, -11.120298, -20.0679, 0, -13.378599, -16.7232, 0, -13.378599, -20.5103, 0, -10.033899, -23.412498, 0, -13.378599, -20.0679, 0, -13.378599, -23.412498, 0, -13.378599, -20.5103, 0, -10.033899, -23.412498, 0, -10.033899, -20.3764, 0, -6.6893, -23.412498, 0, -10.033899, -20.5103, 0, -10.033899, -23.412498, 0, -10.033899, -20.3764, 0, -6.6893, -23.412498, 0, -6.6893, -23.412498, -0.0385, -2.8684, -23.412498, 0, -3.7743, -22.9552, -0.047599997, -3.375, -23.412498, -0.0385, -2.8684, -23.8421, 0, -3.3446002, -23.412498, 0, -3.7743, -23.412498, 0, -6.6893, -22.9552, -0.047599997, -3.375, -23.412498, 0, -3.7743, -22.9552, -0.047599997, -3.375, -23.412498, 0, -6.6893, -20.1607, -0.2436, -4.5060997, -20.1607, -0.2436, -4.5060997, -23.412498, 0, -6.6893, -20.3764, 0, -6.6893, -22.9552, -0.047599997, -3.375, -20.1607, -0.2436, -4.5060997, -21.0086, -0.2504, -3.5043998, -29.294899, -0.6617, -6.6893, -23.8421, 0, -3.3446002, -27.9812, -0.6617, -3.3446002, -23.8421, 0, -3.3446002, -29.294899, -0.6617, -6.6893, -23.412498, 0, -6.6893, -23.8421, 0, -3.3446002, -23.412498, 0, -6.6893, -23.412498, 0, -3.7743, -27.9812, -0.6617, -3.3446002, -25.479, -0.4588, 0.143, -27.9812, -0.6617, 0, -25.479, -0.4588, 0.143, -27.9812, -0.6617, -3.3446002, -23.412498, -0.0385, -2.8684, -23.412498, -0.0385, -2.8684, -27.9812, -0.6617, -3.3446002, -23.8421, 0, -3.3446002, -25.479, -0.4588, 0.143, -23.412498, -0.0385, -2.8684, -23.412498, -0.2913, 0.26119995, -22.756899, 0, 10.033899, -24.056599, 0, 10.033899, -23.489, 0, 9.386499, -22.756899, 0, 10.033899, -23.412498, 0, 10.6779995, -24.056599, 0, 10.033899, -20.0679, 0, 13.378601, -23.412498, 0, 10.6779995, -22.756899, 0, 10.033899, -23.412498, 0, 10.6779995, -20.0679, 0, 13.378601, -23.412498, 0, 13.378601, -20.0679, 0, 13.378601, -22.756899, 0, 10.033899, -20.0679, 0, 10.033899, -20.0679, 0, 10.033899, -17.213, 0, 13.378601, -20.0679, 0, 13.378601, -17.213, 0, 13.378601, -20.0679, 0, 10.033899, -16.7232, 0, 10.8878, -16.7232, 0, 10.8878, -20.0679, 0, 10.033899, -17.583, 0, 10.033899, -17.213, 0, 13.378601, -16.7232, 0, 10.8878, -16.7232, 0, 12.878401, -16.223, 0, 13.378601, -17.213, 0, 13.378601, -16.7232, 0, 12.878401, -16.223, 0, 13.378601, -16.7232, 0, 13.868401, -17.213, 0, 13.378601, -29.294899, -0.6617, 10.033899, -25.945799, -0.25779998, 6.6893, -26.0077, -0.2465, 10.033899, -29.294899, -0.6617, 10.033899, -29.294899, -0.6617, 6.6893, -25.945799, -0.25779998, 6.6893, -29.294899, -0.6617, 13.378601, -26.0077, -0.2465, 10.033899, -25.9681, -0.2875, 13.378601, -29.294899, -0.6617, 13.378601, -29.294899, -0.6617, 10.033899, -26.0077, -0.2465, 10.033899, -20.0679, -0.36479998, 16.7232, -20.0679, 0, 13.378601, -16.7232, -0.26209998, 16.7232, -16.7232, 0, 13.868401, -13.378601, 0, 16.7232, -16.7232, -0.26209998, 16.7232, -13.378601, 0, 16.7232, -16.7232, 0, 13.868401, -16.223, 0, 13.378601, -13.378601, 0, 16.7232, -16.223, 0, 13.378601, -13.378601, 0, 13.378601, 0, 0, 20.0679, 3.3446, -0.2814, 23.412498, 0, -0.2814, 23.412498, 3.3446, -0.2814, 23.412498, 0, 0, 20.0679, 2.4261, 0, 20.0679, 3.3446, -0.2814, 23.412498, 2.4261, 0, 20.0679, 3.3446, 0, 20.986399, 3.3446, 0, 20.986399, 6.6893, -0.2814, 23.412498, 3.3446, -0.2814, 23.412498, 6.6893, -0.2814, 23.412498, 3.3446, 0, 20.986399, 4.275, 0, 20.0679, 6.6893, -0.2814, 23.412498, 4.275, 0, 20.0679, 6.6893, 0, 20.0679, 10.572599, 0, 19.5895, 10.501598, 0, 20.5087, 10.080299, 0, 20.0679, 10.572599, 0, 19.5895, 11.0191, 0, 20.0679, 10.501598, 0, 20.5087, 10.572599, 0, 19.5895, 13.378599, 0, 16.7232, 11.0191, 0, 20.0679, 10.572599, 0, 19.5895, 10.572599, 0, 16.7232, 13.378599, 0, 16.7232, 10.572599, 0, 14.3402, 13.378599, 0, 16.7232, 10.572599, 0, 16.7232, 13.378599, 0, 16.7232, 10.572599, 0, 14.3402, 13.105099, 0, 13.9188, 13.105099, 0, 13.9188, 10.572599, 0, 14.3402, 10.986898, 0, 13.9188, 13.378599, 0, 16.7232, 13.105099, 0, 13.9188, 13.700699, 0, 14.602699, 13.8046, 0, 13.2202, 13.700699, 0, 14.602699, 13.105099, 0, 13.9188, 13.8046, 0, 13.2202, 14.439199, 0, 13.6437, 13.700699, 0, 14.602699, 6.6893, -0.2814, 23.412498, 10.501598, 0, 20.5087, 10.033899, -0.501, 23.412498, 10.033899, -0.501, 23.412498, 11.0191, 0, 20.0679, 13.378599, -1.0746, 20.0679, 10.033899, -0.501, 23.412498, 10.501598, 0, 20.5087, 11.0191, 0, 20.0679, 17.4912, 0.0034999999, 7.4080997, 13.8046, 0, 10.033899, 14.0257, 0.0442, 7.3252997, 13.8046, 0, 10.033899, 17.4912, 0.0034999999, 7.4080997, 16.7232, 0, 10.033899, 13.8046, 0, 10.033899, 16.7232, 0, 10.033899, 15.0508995, 0, 10.033899, 16.7232, -1.0746, 13.378601, 15.0508995, -0.40039998, 16.7232, 15.0508995, -0.6288, 13.378601, -16.7232, -1.1374999, 26.7572, -20.0679, -1.9081, 30.3625, -20.0679, -1.1374999, 26.7572, -16.7232, -1.1374999, 26.7572, -16.7232, -1.9068999, 30.3568, -20.0679, -1.9081, 30.3625, -23.412498, -1.9093, 30.3681, -20.0679, -1.1374999, 26.7572, -20.0679, -1.9081, 30.3625, -23.412498, -1.9093, 30.3681, -23.412498, -1.1374999, 26.7572, -20.0679, -1.1374999, 26.7572, -23.412498, -2.6287, 33.7341, -23.412498, -1.9093, 30.3681, -20.0679, -1.9081, 30.3625, -16.7232, -1.9068999, 30.3568, -20.0679, -2.628, 33.7306, -20.0679, -1.9081, 30.3625, -23.412498, -2.6287, 33.7341, -20.0679, -1.9081, 30.3625, -20.0679, -2.628, 33.7306, -16.7232, -1.9068999, 30.3568, -16.7232, -2.6272, 33.726997, -20.0679, -2.628, 33.7306, -23.412498, -3.2477999, 36.6305, -23.412498, -2.6287, 33.7341, -20.0679, -2.628, 33.7306, -16.7232, -2.6272, 33.726997, -20.0679, -3.2473998, 36.6287, -20.0679, -2.628, 33.7306, -23.412498, -3.2477999, 36.6305, -20.0679, -2.628, 33.7306, -20.0679, -3.2473998, 36.6287, -16.7232, -2.6272, 33.726997, -16.7232, -3.247, 36.6269, -20.0679, -3.2473998, 36.6287, -20.0679, -3.8518999, 39.456898, -23.412498, -3.2477999, 36.6305, -20.0679, -3.2473998, 36.6287, -16.7232, -3.247, 36.6269, -20.0679, -3.8518999, 39.456898, -20.0679, -3.2473998, 36.6287, -20.0679, -3.8518999, 39.456898, -23.412498, -3.8518999, 39.456898, -23.412498, -3.2477999, 36.6305, -16.7232, -3.247, 36.6269, -16.7232, -3.8518999, 39.456898, -20.0679, -3.8518999, 39.456898, -27.9812, -0.6617, 0, -30.315699, -0.9931, 3.3446, -30.300001, -1.3205999, 0, -27.9812, -0.6617, 0, -29.294899, -0.6617, 3.3446, -30.315699, -0.9931, 3.3446, -50.290497, -8.1437, 10.033899, -53.117798, -8.6939, 6.6893, -50.290497, -8.1437, 6.6893, -50.290497, -8.1437, 10.033899, -50.290497, -8.1437, 13.378601, -53.117798, -8.6939, 6.6893, -53.117798, -8.6939, 3.3446, -50.290497, -8.1437, 6.6893, -53.117798, -8.6939, 6.6893, -53.117798, -8.6939, 3.3446, -50.290497, -8.1437, 3.3446, -50.290497, -8.1437, 6.6893, -53.117798, -8.6939, 6.6893, -50.290497, -8.1437, 13.378601, -53.117798, -8.6939, 13.378601, -53.117798, -8.6939, 0, -50.290497, -8.1437, 3.3446, -53.117798, -8.6939, 3.3446, -53.117798, -8.6939, 0, -50.290497, -8.1437, 0, -50.290497, -8.1437, 3.3446, -55.5951, -9.176, 3.3446, -53.117798, -8.6939, 3.3446, -53.117798, -8.6939, 6.6893, -53.117798, -8.6939, -3.3446002, -50.290497, -8.1437, 0, -53.117798, -8.6939, 0, -53.117798, -8.6939, -3.3446002, -50.290497, -8.1437, -3.3446002, -50.290497, -8.1437, 0, -55.5951, -9.176, 0, -53.117798, -8.6939, 0, -53.117798, -8.6939, 3.3446, -55.5951, -9.176, 0, -53.117798, -8.6939, 3.3446, -55.5951, -9.176, 3.3446, -55.5951, -9.176, -3.3446002, -53.117798, -8.6939, -3.3446002, -53.117798, -8.6939, 0, -55.5951, -9.176, -3.3446002, -53.117798, -8.6939, 0, -55.5951, -9.176, 0, -53.117798, -8.6939, -6.6893, -50.290497, -8.1437, -3.3446002, -53.117798, -8.6939, -3.3446002, -53.117798, -8.6939, -6.6893, -50.290497, -8.1437, -6.6893, -50.290497, -8.1437, -3.3446002, -55.5951, -9.176, -6.6893, -53.117798, -8.6939, -3.3446002, -55.5951, -9.176, -3.3446002, -55.5951, -9.176, -6.6893, -53.117798, -8.6939, -6.6893, -53.117798, -8.6939, -3.3446002, -53.117798, -8.6939, -10.033899, -50.290497, -8.1437, -6.6893, -53.117798, -8.6939, -6.6893, -53.117798, -8.6939, -10.033899, -50.290497, -8.1437, -10.033899, -50.290497, -8.1437, -6.6893, -55.5951, -9.176, -10.033899, -53.117798, -8.6939, -6.6893, -55.5951, -9.176, -6.6893, -55.5951, -9.176, -10.033899, -53.117798, -8.6939, -10.033899, -53.117798, -8.6939, -6.6893, -53.117798, -8.6939, -13.378599, -50.290497, -8.1437, -10.033899, -53.117798, -8.6939, -10.033899, -53.117798, -8.6939, -13.378599, -50.290497, -8.1437, -13.378599, -50.290497, -8.1437, -10.033899, -53.117798, -8.6939, -13.378599, -50.290497, -8.1437, -16.7232, -50.290497, -8.1437, -13.378599, -53.117798, -8.6939, -13.378599, -50.290497, -8.1437, -16.937199, -50.290497, -8.1437, -16.7232, -50.290497, -8.1437, -16.937199, -53.117798, -8.6939, -13.378599, -53.117798, -8.6939, -16.911499, -53.117798, -8.6939, -16.911499, -50.290497, -8.1437, -20.0679, -50.290497, -8.1437, -16.937199, -55.5951, -9.176, -13.378599, -53.117798, -8.6939, -13.378599, -53.117798, -8.6939, -10.033899, -55.5951, -9.176, -13.378599, -53.117798, -8.6939, -10.033899, -55.5951, -9.176, -10.033899, -53.117798, -8.6939, -16.911499, -50.290497, -8.1437, -20.2818, -50.290497, -8.1437, -20.0679, -55.5951, -9.176, -16.888899, -53.117798, -8.6939, -13.378599, -55.5951, -9.176, -13.378599, -55.5951, -9.176, -16.888899, -53.117798, -8.6939, -16.911499, -53.117798, -8.6939, -13.378599, -58.463497, -9.7343, -13.378599, -55.5951, -9.176, -13.378599, -55.5951, -9.176, -10.033899, -58.463497, -9.7343, -16.8629, -55.5951, -9.176, -16.888899, -55.5951, -9.176, -13.378599, -58.463497, -9.7343, -16.8629, -55.5951, -9.176, -13.378599, -58.463497, -9.7343, -13.378599, -55.5951, -9.176, -20.233599, -53.117798, -8.6939, -16.911499, -55.5951, -9.176, -16.888899, -50.290497, -8.1437, -20.2818, -53.117798, -8.6939, -16.911499, -53.117798, -8.6939, -20.2561, -55.5951, -9.176, -20.233599, -53.117798, -8.6939, -20.2561, -53.117798, -8.6939, -16.911499, -53.117798, -8.6939, -23.412498, -50.290497, -8.1437, -20.2818, -53.117798, -8.6939, -20.2561, -53.117798, -8.6939, -23.412498, -50.290497, -8.1437, -23.412498, -50.290497, -8.1437, -20.2818, -53.117798, -8.6939, -26.7572, -50.290497, -8.1437, -23.412498, -53.117798, -8.6939, -23.412498, -53.117798, -8.6939, -26.7572, -50.290497, -8.1437, -26.7572, -50.290497, -8.1437, -23.412498, -55.5951, -9.176, -26.7572, -53.117798, -8.6939, -26.7572, -53.117798, -8.6939, -23.412498, -55.5951, -9.176, -23.412498, -53.117798, -8.6939, -23.412498, -53.117798, -8.6939, -20.2561, -55.5951, -9.176, -26.7572, -53.117798, -8.6939, -23.412498, -55.5951, -9.176, -23.412498, -55.5951, -9.176, -23.412498, -53.117798, -8.6939, -20.2561, -55.5951, -9.176, -20.233599, -58.463497, -9.7343, -26.7572, -55.5951, -9.176, -26.7572, -55.5951, -9.176, -23.412498, -58.463497, -9.7343, -23.412498, -55.5951, -9.176, -23.412498, -55.5951, -9.176, -20.233599, -58.463497, -9.7343, -26.7572, -55.5951, -9.176, -23.412498, -58.463497, -9.7343, -23.412498, -62.057194, -10.4337, -23.412498, -58.463497, -9.7343, -26.7572, -58.463497, -9.7343, -23.412498, -62.057194, -10.4337, -23.412498, -62.057194, -10.4337, -26.7572, -58.463497, -9.7343, -26.7572, -62.057194, -10.4337, -20.174799, -62.057194, -10.4337, -23.412498, -58.463497, -9.7343, -23.412498, -58.463497, -9.7343, -23.412498, -55.5951, -9.176, -20.233599, -58.463497, -9.7343, -20.207499, -62.057194, -10.4337, -20.174799, -58.463497, -9.7343, -23.412498, -58.463497, -9.7343, -20.207499, -58.463497, -9.7343, -20.207499, -55.5951, -9.176, -20.233599, -55.5951, -9.176, -16.888899, -58.463497, -9.7343, -20.207499, -55.5951, -9.176, -16.888899, -58.463497, -9.7343, -16.8629, -58.463497, -9.7343, -16.8629, -62.057194, -10.4337, -20.174799, -58.463497, -9.7343, -20.207499, -58.463497, -9.7343, -16.8629, -62.057194, -10.4337, -20.0679, -62.057194, -10.4337, -20.174799, -62.057194, -10.4337, -20.0679, -58.463497, -9.7343, -16.8629, -62.057194, -10.4337, -16.8302, -58.463497, -9.7343, -13.378599, -62.057194, -10.4337, -16.8302, -58.463497, -9.7343, -16.8629, -58.463497, -9.7343, -13.378599, -62.057194, -10.4337, -16.7232, -62.057194, -10.4337, -16.8302, -62.057194, -10.4337, -16.7232, -58.463497, -9.7343, -13.378599, -62.057194, -10.4337, -13.378599, -62.057194, -10.4337, -10.033899, -62.057194, -10.4337, -13.378599, -58.463497, -9.7343, -13.378599, -62.057194, -10.4337, -10.033899, -58.463497, -9.7343, -13.378599, -58.463497, -9.7343, -10.033899, -58.463497, -9.7343, -13.378599, -55.5951, -9.176, -10.033899, -58.463497, -9.7343, -10.033899, -62.057194, -10.4337, -6.6893, -62.057194, -10.4337, -10.033899, -58.463497, -9.7343, -10.033899, -58.463497, -9.7343, -10.033899, -55.5951, -9.176, -10.033899, -55.5951, -9.176, -6.6893, -62.057194, -10.4337, -6.6893, -58.463497, -9.7343, -10.033899, -58.463497, -9.7343, -6.6893, -58.463497, -9.7343, -10.033899, -55.5951, -9.176, -6.6893, -58.463497, -9.7343, -6.6893, -62.057194, -10.4337, -3.3446002, -62.057194, -10.4337, -6.6893, -58.463497, -9.7343, -6.6893, -58.463497, -9.7343, -6.6893, -55.5951, -9.176, -6.6893, -55.5951, -9.176, -3.3446002, -62.057194, -10.4337, -3.3446002, -58.463497, -9.7343, -6.6893, -58.463497, -9.7343, -3.3446002, -58.463497, -9.7343, -6.6893, -55.5951, -9.176, -3.3446002, -58.463497, -9.7343, -3.3446002, -62.057194, -10.4337, 0, -62.057194, -10.4337, -3.3446002, -58.463497, -9.7343, -3.3446002, -58.463497, -9.7343, -3.3446002, -55.5951, -9.176, -3.3446002, -55.5951, -9.176, 0, -62.057194, -10.4337, 0, -58.463497, -9.7343, -3.3446002, -58.463497, -9.7343, 0, -58.463497, -9.7343, -3.3446002, -55.5951, -9.176, 0, -58.463497, -9.7343, 0, -62.057194, -10.4337, 3.3446, -62.057194, -10.4337, 0, -58.463497, -9.7343, 0, -58.463497, -9.7343, 0, -55.5951, -9.176, 0, -55.5951, -9.176, 3.3446, -62.057194, -10.4337, 3.3446, -58.463497, -9.7343, 0, -58.463497, -9.7343, 3.3446, -58.463497, -9.7343, 0, -55.5951, -9.176, 3.3446, -58.463497, -9.7343, 3.3446, -62.057194, -10.4337, 6.6893, -62.057194, -10.4337, 3.3446, -58.463497, -9.7343, 3.3446, -62.057194, -10.4337, 6.6893, -58.463497, -9.7343, 3.3446, -58.463497, -9.7343, 6.6893, -58.463497, -9.7343, 6.6893, -62.057194, -10.4337, 10.033899, -62.057194, -10.4337, 6.6893, -58.463497, -9.7343, 3.3446, -55.5951, -9.176, 3.3446, -55.5951, -9.176, 6.6893, -58.463497, -9.7343, 3.3446, -55.5951, -9.176, 6.6893, -58.463497, -9.7343, 6.6893, -55.5951, -9.176, 3.3446, -53.117798, -8.6939, 6.6893, -55.5951, -9.176, 6.6893, -58.463497, -9.7343, 6.6893, -62.057194, -10.4337, 13.378601, -62.057194, -10.4337, 10.033899, -53.117798, -8.6939, 13.378601, -55.5951, -9.176, 6.6893, -53.117798, -8.6939, 6.6893, -55.5951, -9.176, 13.378601, -58.463497, -9.7343, 6.6893, -55.5951, -9.176, 6.6893, -53.117798, -8.6939, 13.378601, -55.5951, -9.176, 13.378601, -55.5951, -9.176, 6.6893, -55.5951, -9.176, 13.378601, -58.463497, -9.7343, 13.378601, -58.463497, -9.7343, 6.6893, -62.057194, -10.4337, 13.378601, -58.463497, -9.7343, 6.6893, -58.463497, -9.7343, 13.378601, -53.117798, -8.6939, 16.7232, -55.5951, -9.176, 13.378601, -53.117798, -8.6939, 13.378601, -50.290497, -8.1437, 13.378601, -53.117798, -8.6939, 16.7232, -53.117798, -8.6939, 13.378601, -50.290497, -8.1437, 13.378601, -50.290497, -8.1437, 16.7232, -53.117798, -8.6939, 16.7232, -53.117798, -8.6939, 16.7232, -55.5951, -9.176, 16.7232, -55.5951, -9.176, 13.378601, -55.5951, -9.176, 16.7232, -58.463497, -9.7343, 13.378601, -55.5951, -9.176, 13.378601, -50.290497, -8.1437, 16.7232, -53.117798, -8.6939, 20.0679, -53.117798, -8.6939, 16.7232, -53.117798, -8.6939, 20.0679, -55.5951, -9.176, 16.7232, -53.117798, -8.6939, 16.7232, -50.290497, -8.1437, 16.7232, -50.290497, -8.1437, 20.0679, -53.117798, -8.6939, 20.0679, -58.463497, -9.7343, 16.7232, -62.057194, -10.4337, 13.378601, -58.463497, -9.7343, 13.378601, -55.5951, -9.176, 16.7232, -58.463497, -9.7343, 16.7232, -58.463497, -9.7343, 13.378601, -58.463497, -9.7343, 16.7232, -62.057194, -10.4337, 16.7232, -62.057194, -10.4337, 13.378601, -53.117798, -8.6939, 20.0679, -55.5951, -9.176, 20.0679, -55.5951, -9.176, 16.7232, -55.5951, -9.176, 20.0679, -58.463497, -9.7343, 16.7232, -55.5951, -9.176, 16.7232, -50.290497, -8.1437, 20.0679, -53.117798, -8.6939, 23.412498, -53.117798, -8.6939, 20.0679, -53.117798, -8.6939, 23.412498, -55.5951, -9.176, 20.0679, -53.117798, -8.6939, 20.0679, -50.290497, -8.1437, 20.0679, -50.290497, -8.1437, 23.412498, -53.117798, -8.6939, 23.412498, -50.290497, -8.1437, 23.412498, -53.117798, -8.6939, 26.7572, -53.117798, -8.6939, 23.412498, -50.290497, -8.1437, 23.412498, -50.290497, -8.1437, 26.7572, -53.117798, -8.6939, 26.7572, -53.117798, -8.6939, 26.7572, -55.5951, -9.176, 23.412498, -53.117798, -8.6939, 23.412498, -53.117798, -8.6939, 23.412498, -55.5951, -9.176, 23.412498, -55.5951, -9.176, 20.0679, -53.117798, -8.6939, 26.7572, -55.5951, -9.176, 26.7572, -55.5951, -9.176, 23.412498, -55.5951, -9.176, 20.0679, -58.463497, -9.7343, 20.0679, -58.463497, -9.7343, 16.7232, -58.463497, -9.7343, 20.0679, -62.057194, -10.4337, 16.7232, -58.463497, -9.7343, 16.7232, -55.5951, -9.176, 23.412498, -58.463497, -9.7343, 20.0679, -55.5951, -9.176, 20.0679, -58.463497, -9.7343, 20.0679, -62.057194, -10.4337, 20.0679, -62.057194, -10.4337, 16.7232, -55.5951, -9.176, 26.7572, -58.463497, -9.7343, 23.412498, -55.5951, -9.176, 23.412498, -55.5951, -9.176, 23.412498, -58.463497, -9.7343, 23.412498, -58.463497, -9.7343, 20.0679, -58.463497, -9.7343, 23.412498, -62.057194, -10.4337, 20.0679, -58.463497, -9.7343, 20.0679, -55.5951, -9.176, 26.7572, -58.463497, -9.7343, 26.7572, -58.463497, -9.7343, 23.412498, -58.463497, -9.7343, 23.412498, -62.057194, -10.4337, 23.412498, -62.057194, -10.4337, 20.0679, -58.463497, -9.7343, 26.7572, -62.057194, -10.4337, 23.412498, -58.463497, -9.7343, 23.412498, -58.463497, -9.7343, 26.7572, -62.057194, -10.4337, 26.7572, -62.057194, -10.4337, 23.412498, -62.057194, -10.4337, 0, -64.963196, -10.4337, -3.3446002, -62.057194, -10.4337, -3.3446002, -64.963196, -10.4337, -6.6893, -62.057194, -10.4337, -3.3446002, -64.963196, -10.4337, -3.3446002, -64.963196, -10.4337, -6.6893, -62.057194, -10.4337, -6.6893, -62.057194, -10.4337, -3.3446002, -62.057194, -10.4337, 0, -64.963196, -10.4337, 0, -64.963196, -10.4337, -3.3446002, -64.963196, -10.4337, 0, -62.057194, -10.4337, 0, -62.057194, -10.4337, 3.3446, -68.2489, -10.4337, -6.6893, -64.963196, -10.4337, -6.6893, -64.963196, -10.4337, -3.3446002, -64.963196, -10.4337, 0, -68.2489, -10.4337, -3.3446002, -64.963196, -10.4337, -3.3446002, -68.2489, -10.4337, -6.6893, -64.963196, -10.4337, -3.3446002, -68.2489, -10.4337, -3.3446002, -64.963196, -10.4337, -10.033899, -62.057194, -10.4337, -6.6893, -64.963196, -10.4337, -6.6893, -64.963196, -10.4337, -10.033899, -62.057194, -10.4337, -10.033899, -62.057194, -10.4337, -6.6893, -68.2489, -10.4337, -10.033899, -64.963196, -10.4337, -6.6893, -68.2489, -10.4337, -6.6893, -68.2489, -10.4337, -10.033899, -64.963196, -10.4337, -10.033899, -64.963196, -10.4337, -6.6893, -64.963196, -10.4337, -13.378599, -62.057194, -10.4337, -10.033899, -64.963196, -10.4337, -10.033899, -64.963196, -10.4337, -13.378599, -62.057194, -10.4337, -13.378599, -62.057194, -10.4337, -10.033899, -64.963196, -10.4337, -13.378599, -62.057194, -10.4337, -16.7232, -62.057194, -10.4337, -13.378599, -64.963196, -10.4337, -13.378599, -62.057194, -10.4337, -16.8302, -62.057194, -10.4337, -16.7232, -62.057194, -10.4337, -16.8302, -64.963196, -10.4337, -13.378599, -64.963196, -10.4337, -16.803799, -64.963196, -10.4337, -16.803799, -62.057194, -10.4337, -20.0679, -62.057194, -10.4337, -16.8302, -68.2489, -10.4337, -13.378599, -64.963196, -10.4337, -13.378599, -64.963196, -10.4337, -10.033899, -68.2489, -10.4337, -13.378599, -64.963196, -10.4337, -10.033899, -68.2489, -10.4337, -10.033899, -64.963196, -10.4337, -16.803799, -62.057194, -10.4337, -20.174799, -62.057194, -10.4337, -20.0679, -68.2489, -10.4337, -16.7739, -64.963196, -10.4337, -13.378599, -68.2489, -10.4337, -13.378599, -68.2489, -10.4337, -16.7739, -64.963196, -10.4337, -16.803799, -64.963196, -10.4337, -13.378599, -71.2999, -10.4337, -13.378599, -68.2489, -10.4337, -13.378599, -68.2489, -10.4337, -10.033899, -71.2999, -10.4337, -16.746199, -68.2489, -10.4337, -16.7739, -68.2489, -10.4337, -13.378599, -71.2999, -10.4337, -16.746199, -68.2489, -10.4337, -13.378599, -71.2999, -10.4337, -13.378599, -73.8239, -10.4337, -13.378599, -71.2999, -10.4337, -16.746199, -71.2999, -10.4337, -13.378599, -73.8239, -10.4337, -13.378599, -73.8239, -10.4337, -16.7232, -71.2999, -10.4337, -16.746199, -73.8239, -10.4337, -10.033899, -73.8239, -10.4337, -13.378599, -71.2999, -10.4337, -13.378599, -73.8239, -10.4337, -16.7232, -71.2999, -10.4337, -20.0908, -71.2999, -10.4337, -16.746199, -71.2999, -10.4337, -20.0908, -68.2489, -10.4337, -16.7739, -71.2999, -10.4337, -16.746199, -73.8239, -10.4337, -16.7232, -73.8239, -10.4337, -20.0679, -71.2999, -10.4337, -20.0908, -71.2999, -10.4337, -13.378599, -68.2489, -10.4337, -10.033899, -71.2999, -10.4337, -10.033899, -73.8239, -10.4337, -10.033899, -71.2999, -10.4337, -13.378599, -71.2999, -10.4337, -10.033899, -71.2999, -10.4337, -10.033899, -68.2489, -10.4337, -10.033899, -68.2489, -10.4337, -6.6893, -73.8239, -10.4337, -6.6893, -73.8239, -10.4337, -10.033899, -71.2999, -10.4337, -10.033899, -71.2999, -10.4337, -10.033899, -68.2489, -10.4337, -6.6893, -71.2999, -10.4337, -6.6893, -73.8239, -10.4337, -6.6893, -71.2999, -10.4337, -10.033899, -71.2999, -10.4337, -6.6893, -71.2999, -10.4337, -6.6893, -68.2489, -10.4337, -6.6893, -68.2489, -10.4337, -3.3446002, -73.8239, -10.4337, -3.3446002, -73.8239, -10.4337, -6.6893, -71.2999, -10.4337, -6.6893, -71.2999, -10.4337, -6.6893, -68.2489, -10.4337, -3.3446002, -71.2999, -10.4337, -3.3446002, -73.8239, -10.4337, -3.3446002, -71.2999, -10.4337, -6.6893, -71.2999, -10.4337, -3.3446002, -68.2489, -10.4337, 0, -71.2999, -10.4337, -3.3446002, -68.2489, -10.4337, -3.3446002, -71.2999, -10.4337, 0, -73.8239, -10.4337, -3.3446002, -71.2999, -10.4337, -3.3446002, -68.2489, -10.4337, 0, -71.2999, -10.4337, 0, -71.2999, -10.4337, -3.3446002, -71.2999, -10.4337, 0, -73.8239, -10.4337, 0, -73.8239, -10.4337, -3.3446002, -64.963196, -10.4337, 0, -68.2489, -10.4337, 0, -68.2489, -10.4337, -3.3446002, -73.8239, -10.4337, 0, -71.2999, -10.4337, 0, -71.2999, -10.4337, 3.3446, -73.8239, -10.4337, 0, -71.2999, -10.4337, 3.3446, -73.8239, -10.4337, 3.3446, -71.2999, -10.4337, 0, -68.2489, -10.4337, 0, -68.2489, -10.4337, 3.3446, -71.2999, -10.4337, 0, -68.2489, -10.4337, 3.3446, -71.2999, -10.4337, 3.3446, -68.2489, -10.4337, 0, -64.963196, -10.4337, 0, -64.963196, -10.4337, 3.3446, -68.2489, -10.4337, 0, -64.963196, -10.4337, 3.3446, -68.2489, -10.4337, 3.3446, -64.963196, -10.4337, 0, -62.057194, -10.4337, 3.3446, -64.963196, -10.4337, 3.3446, -62.057194, -10.4337, 3.3446, -64.963196, -10.4337, 6.6893, -64.963196, -10.4337, 3.3446, -64.963196, -10.4337, 6.6893, -68.2489, -10.4337, 3.3446, -64.963196, -10.4337, 3.3446, -62.057194, -10.4337, 3.3446, -62.057194, -10.4337, 6.6893, -64.963196, -10.4337, 6.6893, -62.057194, -10.4337, 10.033899, -64.963196, -10.4337, 6.6893, -62.057194, -10.4337, 6.6893, -64.963196, -10.4337, 6.6893, -62.057194, -10.4337, 10.033899, -62.057194, -10.4337, 13.378601, -64.963196, -10.4337, 6.6893, -68.2489, -10.4337, 6.6893, -68.2489, -10.4337, 3.3446, -68.2489, -10.4337, 6.6893, -71.2999, -10.4337, 3.3446, -68.2489, -10.4337, 3.3446, -64.963196, -10.4337, 6.6893, -62.057194, -10.4337, 13.378601, -64.963196, -10.4337, 13.378601, -64.963196, -10.4337, 13.378601, -68.2489, -10.4337, 6.6893, -64.963196, -10.4337, 6.6893, -62.057194, -10.4337, 13.378601, -64.963196, -10.4337, 16.7232, -64.963196, -10.4337, 13.378601, -62.057194, -10.4337, 13.378601, -62.057194, -10.4337, 16.7232, -64.963196, -10.4337, 16.7232, -64.963196, -10.4337, 13.378601, -68.2489, -10.4337, 13.378601, -68.2489, -10.4337, 6.6893, -64.963196, -10.4337, 16.7232, -68.2489, -10.4337, 13.378601, -64.963196, -10.4337, 13.378601, -68.2489, -10.4337, 6.6893, -71.2999, -10.4337, 6.6893, -71.2999, -10.4337, 3.3446, -68.2489, -10.4337, 13.378601, -71.2999, -10.4337, 6.6893, -68.2489, -10.4337, 6.6893, -71.2999, -10.4337, 6.6893, -73.8239, -10.4337, 3.3446, -71.2999, -10.4337, 3.3446, -71.2999, -10.4337, 6.6893, -73.8239, -10.4337, 6.6893, -73.8239, -10.4337, 3.3446, -71.2999, -10.4337, 6.6893, -73.8239, -10.4337, 10.033899, -73.8239, -10.4337, 6.6893, -71.2999, -10.4337, 6.6893, -73.8239, -10.4337, 13.378601, -73.8239, -10.4337, 10.033899, -68.2489, -10.4337, 13.378601, -71.2999, -10.4337, 13.378601, -71.2999, -10.4337, 6.6893, -73.8239, -10.4337, 13.378601, -71.2999, -10.4337, 6.6893, -71.2999, -10.4337, 13.378601, -64.963196, -10.4337, 16.7232, -68.2489, -10.4337, 16.7232, -68.2489, -10.4337, 13.378601, -68.2489, -10.4337, 16.7232, -71.2999, -10.4337, 13.378601, -68.2489, -10.4337, 13.378601, -71.2999, -10.4337, 16.7232, -73.8239, -10.4337, 13.378601, -71.2999, -10.4337, 13.378601, -68.2489, -10.4337, 16.7232, -71.2999, -10.4337, 16.7232, -71.2999, -10.4337, 13.378601, -71.2999, -10.4337, 16.7232, -73.8239, -10.4337, 16.7232, -73.8239, -10.4337, 13.378601, -64.963196, -10.4337, 20.0679, -68.2489, -10.4337, 16.7232, -64.963196, -10.4337, 16.7232, -62.057194, -10.4337, 16.7232, -64.963196, -10.4337, 20.0679, -64.963196, -10.4337, 16.7232, -62.057194, -10.4337, 16.7232, -62.057194, -10.4337, 20.0679, -64.963196, -10.4337, 20.0679, -64.963196, -10.4337, 20.0679, -68.2489, -10.4337, 20.0679, -68.2489, -10.4337, 16.7232, -68.2489, -10.4337, 20.0679, -71.2999, -10.4337, 16.7232, -68.2489, -10.4337, 16.7232, -62.057194, -10.4337, 20.0679, -64.963196, -10.4337, 23.412498, -64.963196, -10.4337, 20.0679, -64.963196, -10.4337, 23.412498, -68.2489, -10.4337, 20.0679, -64.963196, -10.4337, 20.0679, -62.057194, -10.4337, 20.0679, -62.057194, -10.4337, 23.412498, -64.963196, -10.4337, 23.412498, -71.2999, -10.4337, 20.0679, -73.8239, -10.4337, 16.7232, -71.2999, -10.4337, 16.7232, -68.2489, -10.4337, 20.0679, -71.2999, -10.4337, 20.0679, -71.2999, -10.4337, 16.7232, -71.2999, -10.4337, 20.0679, -73.8239, -10.4337, 20.0679, -73.8239, -10.4337, 16.7232, -64.963196, -10.4337, 23.412498, -68.2489, -10.4337, 23.412498, -68.2489, -10.4337, 20.0679, -68.2489, -10.4337, 23.412498, -71.2999, -10.4337, 20.0679, -68.2489, -10.4337, 20.0679, -62.057194, -10.4337, 23.412498, -64.963196, -10.4337, 26.7572, -64.963196, -10.4337, 23.412498, -64.963196, -10.4337, 26.7572, -68.2489, -10.4337, 23.412498, -64.963196, -10.4337, 23.412498, -62.057194, -10.4337, 23.412498, -62.057194, -10.4337, 26.7572, -64.963196, -10.4337, 26.7572, -71.2999, -10.4337, 23.412498, -73.8239, -10.4337, 20.0679, -71.2999, -10.4337, 20.0679, -68.2489, -10.4337, 23.412498, -71.2999, -10.4337, 23.412498, -71.2999, -10.4337, 20.0679, -71.2999, -10.4337, 23.412498, -73.8239, -10.4337, 23.412498, -73.8239, -10.4337, 20.0679, -68.2489, -10.4337, 26.7572, -71.2999, -10.4337, 23.412498, -68.2489, -10.4337, 23.412498, -64.963196, -10.4337, 26.7572, -68.2489, -10.4337, 26.7572, -68.2489, -10.4337, 23.412498, -71.2999, -10.4337, 26.7572, -73.8239, -10.4337, 23.412498, -71.2999, -10.4337, 23.412498, -68.2489, -10.4337, 26.7572, -71.2999, -10.4337, 26.7572, -71.2999, -10.4337, 23.412498, -71.2999, -10.4337, 26.7572, -73.8239, -10.4337, 26.7572, -73.8239, -10.4337, 23.412498, -73.8239, -10.4337, 26.7572, -71.2999, -10.4337, 26.7572, -72.9579, -10.4337, 27.7057, -71.2999, -10.4337, 26.7572, -71.694496, -10.4337, 29.089499, -72.9579, -10.4337, 27.7057, -71.2999, -10.4337, 26.7572, -70.1271, -10.4337, 28.0285, -71.694496, -10.4337, 29.089499, -71.2999, -10.4337, 26.7572, -68.2489, -10.4337, 26.7572, -70.1271, -10.4337, 28.0285, -70.1271, -10.4337, 28.0285, -70.3329, -10.4337, 30.580898, -71.694496, -10.4337, 29.089499, -70.1271, -10.4337, 28.0285, -68.7877, -10.4337, 29.480598, -70.3329, -10.4337, 30.580898, -68.2489, -10.4337, 26.7572, -68.7877, -10.4337, 29.480598, -70.1271, -10.4337, 28.0285, -68.7877, -10.4337, 29.480598, -69.0569, -10.4337, 31.9786, -70.3329, -10.4337, 30.580898, -68.2489, -10.4337, 26.7572, -66.948, -10.4337, 28.1705, -68.7877, -10.4337, 29.480598, -68.2489, -10.4337, 26.7572, -64.963196, -10.4337, 26.7572, -66.948, -10.4337, 28.1705, -68.7877, -10.4337, 29.480598, -67.5324, -10.4337, 30.8414, -69.0569, -10.4337, 31.9786, -66.948, -10.4337, 28.1705, -67.5324, -10.4337, 30.8414, -68.7877, -10.4337, 29.480598, -67.5324, -10.4337, 30.8414, -67.618996, -10.4337, 33.553497, -69.0569, -10.4337, 31.9786, -64.963196, -10.4337, 26.7572, -65.7283, -10.4337, 29.495699, -66.948, -10.4337, 28.1705, -66.948, -10.4337, 28.1705, -65.7283, -10.4337, 29.495699, -67.5324, -10.4337, 30.8414, -67.5324, -10.4337, 30.8414, -66.0847, -10.4337, 32.410698, -67.618996, -10.4337, 33.553497, -65.7283, -10.4337, 29.495699, -66.0847, -10.4337, 32.410698, -67.5324, -10.4337, 30.8414, -66.0847, -10.4337, 32.410698, -66.2484, -10.4337, 35.0548, -67.618996, -10.4337, 33.553497, -64.963196, -10.4337, 26.7572, -63.779697, -10.4337, 28.042099, -65.7283, -10.4337, 29.495699, -64.963196, -10.4337, 26.7572, -62.057194, -10.4337, 26.7572, -63.779697, -10.4337, 28.042099, -65.7283, -10.4337, 29.495699, -64.2815, -10.4337, 31.0675, -66.0847, -10.4337, 32.410698, -63.779697, -10.4337, 28.042099, -64.2815, -10.4337, 31.0675, -65.7283, -10.4337, 29.495699, -66.0847, -10.4337, 32.410698, -64.736496, -10.4337, 33.8722, -66.2484, -10.4337, 35.0548, -64.2815, -10.4337, 31.0675, -64.736496, -10.4337, 33.8722, -66.0847, -10.4337, 32.410698, -64.736496, -10.4337, 33.8722, -65.1522, -10.4337, 36.255398, -66.2484, -10.4337, 35.0548, -62.057194, -10.4337, 26.7572, -62.331196, -10.4337, 29.6148, -63.779697, -10.4337, 28.042099, -63.779697, -10.4337, 28.042099, -62.331196, -10.4337, 29.6148, -64.2815, -10.4337, 31.0675, -62.057194, -10.4337, 26.7572, -60.611, -10.4337, 28.3335, -62.331196, -10.4337, 29.6148, -64.2815, -10.4337, 31.0675, -62.971096, -10.4337, 32.4913, -64.736496, -10.4337, 33.8722, -62.331196, -10.4337, 29.6148, -62.971096, -10.4337, 32.4913, -64.2815, -10.4337, 31.0675, -64.736496, -10.4337, 33.8722, -63.642498, -10.4337, 35.0582, -65.1522, -10.4337, 36.255398, -62.971096, -10.4337, 32.4913, -63.642498, -10.4337, 35.0582, -64.736496, -10.4337, 33.8722, -63.642498, -10.4337, 35.0582, -63.8252, -10.4337, 37.7089, -65.1522, -10.4337, 36.255398, -60.611, -10.4337, 28.3335, -61.059097, -10.4337, 30.9959, -62.331196, -10.4337, 29.6148, -62.331196, -10.4337, 29.6148, -61.059097, -10.4337, 30.9959, -62.971096, -10.4337, 32.4913, -60.611, -10.4337, 28.3335, -59.3761, -10.4337, 29.679499, -61.059097, -10.4337, 30.9959, -62.971096, -10.4337, 32.4913, -61.8887, -10.4337, 33.667297, -63.642498, -10.4337, 35.0582, -61.059097, -10.4337, 30.9959, -61.8887, -10.4337, 33.667297, -62.971096, -10.4337, 32.4913, -63.642498, -10.4337, 35.0582, -62.323597, -10.4337, 36.4879, -63.8252, -10.4337, 37.7089, -61.8887, -10.4337, 33.667297, -62.323597, -10.4337, 36.4879, -63.642498, -10.4337, 35.0582, -62.323597, -10.4337, 36.4879, -62.558193, -10.4337, 39.0967, -63.8252, -10.4337, 37.7089, -59.3761, -10.4337, 29.679499, -59.9874, -10.4337, 32.1594, -61.059097, -10.4337, 30.9959, -61.059097, -10.4337, 30.9959, -59.9874, -10.4337, 32.1594, -61.8887, -10.4337, 33.667297, -59.3761, -10.4337, 29.679499, -58.316597, -10.4337, 30.834398, -59.9874, -10.4337, 32.1594, -61.8887, -10.4337, 33.667297, -60.59, -10.4337, 35.078197, -62.323597, -10.4337, 36.4879, -59.9874, -10.4337, 32.1594, -60.59, -10.4337, 35.078197, -61.8887, -10.4337, 33.667297, -62.323597, -10.4337, 36.4879, -61.055798, -10.4337, 37.8623, -62.558193, -10.4337, 39.0967, -60.59, -10.4337, 35.078197, -61.055798, -10.4337, 37.8623, -62.323597, -10.4337, 36.4879, -61.055798, -10.4337, 37.8623, -61.1665, -10.4337, 40.621098, -62.558193, -10.4337, 39.0967, -58.316597, -10.4337, 30.834398, -58.708298, -10.4337, 33.5481, -59.9874, -10.4337, 32.1594, -59.9874, -10.4337, 32.1594, -58.708298, -10.4337, 33.5481, -60.59, -10.4337, 35.078197, -58.316597, -10.4337, 30.834398, -57.058, -10.4337, 32.2062, -58.708298, -10.4337, 33.5481, -60.59, -10.4337, 35.078197, -59.331398, -10.4337, 36.4456, -61.055798, -10.4337, 37.8623, -58.708298, -10.4337, 33.5481, -59.331398, -10.4337, 36.4456, -60.59, -10.4337, 35.078197, -61.055798, -10.4337, 37.8623, -59.6695, -10.4337, 39.3651, -61.1665, -10.4337, 40.621098, -59.331398, -10.4337, 36.4456, -59.6695, -10.4337, 39.3651, -61.055798, -10.4337, 37.8623, -59.6695, -10.4337, 39.3651, -59.815098, -10.4337, 42.1012, -61.1665, -10.4337, 40.621098, -57.058, -10.4337, 32.2062, -57.457497, -10.4337, 34.906097, -58.708298, -10.4337, 33.5481, -58.708298, -10.4337, 33.5481, -57.457497, -10.4337, 34.906097, -59.331398, -10.4337, 36.4456, -57.058, -10.4337, 32.2062, -55.8173, -10.4337, 33.5585, -57.457497, -10.4337, 34.906097, -59.331398, -10.4337, 36.4456, -57.962597, -10.4337, 37.9328, -59.6695, -10.4337, 39.3651, -57.457497, -10.4337, 34.906097, -57.962597, -10.4337, 37.9328, -59.331398, -10.4337, 36.4456, -59.6695, -10.4337, 39.3651, -58.3182, -10.4337, 40.8299, -59.815098, -10.4337, 42.1012, -57.962597, -10.4337, 37.9328, -58.3182, -10.4337, 40.8299, -59.6695, -10.4337, 39.3651, -58.3182, -10.4337, 40.8299, -58.397198, -10.4337, 43.6543, -59.815098, -10.4337, 42.1012, -55.8173, -10.4337, 33.5585, -56.105198, -10.4337, 36.374397, -57.457497, -10.4337, 34.906097, -57.457497, -10.4337, 34.906097, -56.105198, -10.4337, 36.374397, -57.962597, -10.4337, 37.9328, -55.8173, -10.4337, 33.5585, -54.4828, -10.4337, 35.0131, -56.105198, -10.4337, 36.374397, -57.962597, -10.4337, 37.9328, -56.621998, -10.4337, 39.389297, -58.3182, -10.4337, 40.8299, -56.105198, -10.4337, 36.374397, -56.621998, -10.4337, 39.389297, -57.962597, -10.4337, 37.9328, -58.3182, -10.4337, 40.8299, -56.8974, -10.4337, 42.370197, -58.397198, -10.4337, 43.6543, -56.621998, -10.4337, 39.389297, -56.8974, -10.4337, 42.370197, -58.3182, -10.4337, 40.8299, -56.8974, -10.4337, 42.370197, -57.1702, -10.4337, 44.9982, -58.397198, -10.4337, 43.6543, -54.4828, -10.4337, 35.0131, -54.7739, -10.4337, 37.819798, -56.105198, -10.4337, 36.374397, -56.105198, -10.4337, 36.374397, -54.7739, -10.4337, 37.819798, -56.621998, -10.4337, 39.389297, -54.4828, -10.4337, 35.0131, -53.163, -10.4337, 36.4517, -54.7739, -10.4337, 37.819798, -56.621998, -10.4337, 39.389297, -55.2089, -10.4337, 40.9246, -56.8974, -10.4337, 42.370197, -54.7739, -10.4337, 37.819798, -55.2089, -10.4337, 40.9246, -56.621998, -10.4337, 39.389297, -56.8974, -10.4337, 42.370197, -55.676598, -10.4337, 43.6936, -57.1702, -10.4337, 44.9982, -55.2089, -10.4337, 40.9246, -55.676598, -10.4337, 43.6936, -56.8974, -10.4337, 42.370197, -55.676598, -10.4337, 43.6936, -55.7286, -10.4337, 46.577297, -57.1702, -10.4337, 44.9982, -53.163, -10.4337, 36.4517, -53.3668, -10.4337, 39.3474, -54.7739, -10.4337, 37.819798, -54.7739, -10.4337, 37.819798, -53.3668, -10.4337, 39.3474, -55.2089, -10.4337, 40.9246, -53.163, -10.4337, 36.4517, -51.764698, -10.4337, 37.9757, -53.3668, -10.4337, 39.3474, -55.2089, -10.4337, 40.9246, -54.004498, -10.4337, 42.233097, -55.676598, -10.4337, 43.6936, -53.3668, -10.4337, 39.3474, -54.004498, -10.4337, 42.233097, -55.2089, -10.4337, 40.9246, -55.676598, -10.4337, 43.6936, -54.2416, -10.4337, 45.2492, -55.7286, -10.4337, 46.577297, -54.004498, -10.4337, 42.233097, -54.2416, -10.4337, 45.2492, -55.676598, -10.4337, 43.6936, -54.2416, -10.4337, 45.2492, -54.056297, -10.4337, 48.408897, -55.7286, -10.4337, 46.577297, -51.764698, -10.4337, 37.9757, -52.1782, -10.4337, 40.637897, -53.3668, -10.4337, 39.3474, -53.3668, -10.4337, 39.3474, -52.1782, -10.4337, 40.637897, -54.004498, -10.4337, 42.233097, -51.764698, -10.4337, 37.9757, -50.5928, -10.4337, 39.2532, -52.1782, -10.4337, 40.637897, -54.004498, -10.4337, 42.233097, -52.5879, -10.4337, 43.772198, -54.2416, -10.4337, 45.2492, -52.1782, -10.4337, 40.637897, -52.5879, -10.4337, 43.772198, -54.004498, -10.4337, 42.233097, -54.2416, -10.4337, 45.2492, -52.5793, -10.4337, 47.051098, -54.056297, -10.4337, 48.408897, -52.5879, -10.4337, 43.772198, -52.5793, -10.4337, 47.051098, -54.2416, -10.4337, 45.2492, -52.5793, -10.4337, 47.051098, -52.2215, -10.4337, 50.418697, -54.056297, -10.4337, 48.408897, -50.5928, -10.4337, 39.2532, -50.779198, -10.4337, 42.1568, -52.1782, -10.4337, 40.637897, -52.1782, -10.4337, 40.637897, -50.779198, -10.4337, 42.1568, -52.5879, -10.4337, 43.772198, -50.5928, -10.4337, 39.2532, -49.212498, -10.4337, 40.7576, -50.779198, -10.4337, 42.1568, -52.5879, -10.4337, 43.772198, -50.9492, -10.4337, 45.552597, -52.5793, -10.4337, 47.051098, -50.779198, -10.4337, 42.1568, -50.9492, -10.4337, 45.552597, -52.5879, -10.4337, 43.772198, -52.5793, -10.4337, 47.051098, -50.741398, -10.4337, 49.0436, -52.2215, -10.4337, 50.418697, -50.9492, -10.4337, 45.552597, -50.741398, -10.4337, 49.0436, -52.5793, -10.4337, 47.051098, -50.741398, -10.4337, 49.0436, -50.267597, -10.4337, 52.5587, -52.2215, -10.4337, 50.418697, -49.212498, -10.4337, 40.7576, -49.163498, -10.4337, 43.911, -50.779198, -10.4337, 42.1568, -50.779198, -10.4337, 42.1568, -49.163498, -10.4337, 43.911, -50.9492, -10.4337, 45.552597, -49.212498, -10.4337, 40.7576, -47.620697, -10.4337, 42.4927, -49.163498, -10.4337, 43.911, -50.9492, -10.4337, 45.552597, -49.1213, -10.4337, 47.5384, -50.741398, -10.4337, 49.0436, -49.163498, -10.4337, 43.911, -49.1213, -10.4337, 47.5384, -50.9492, -10.4337, 45.552597, -50.741398, -10.4337, 49.0436, -48.787197, -10.4337, 51.162, -50.267597, -10.4337, 52.5587, -49.1213, -10.4337, 47.5384, -48.787197, -10.4337, 51.162, -50.741398, -10.4337, 49.0436, -48.787197, -10.4337, 51.162, -48.3197, -10.4337, 54.6923, -50.267597, -10.4337, 52.5587, -47.620697, -10.4337, 42.4927, -47.343597, -10.4337, 45.8868, -49.163498, -10.4337, 43.911, -49.163498, -10.4337, 43.911, -47.343597, -10.4337, 45.8868, -49.1213, -10.4337, 47.5384, -47.620697, -10.4337, 42.4927, -45.8121, -10.4337, 44.463898, -47.343597, -10.4337, 45.8868, -49.1213, -10.4337, 47.5384, -47.181, -10.4337, 49.6466, -48.787197, -10.4337, 51.162, -47.343597, -10.4337, 45.8868, -47.181, -10.4337, 49.6466, -49.1213, -10.4337, 47.5384, -48.787197, -10.4337, 51.162, -46.8489, -10.4337, 53.263298, -48.3197, -10.4337, 54.6923, -47.181, -10.4337, 49.6466, -46.8489, -10.4337, 53.263298, -48.787197, -10.4337, 51.162, -46.8489, -10.4337, 53.263298, -46.839, -10.4337, 56.314198, -48.3197, -10.4337, 54.6923, -45.8121, -10.4337, 44.463898, -45.4151, -10.4337, 47.9805, -47.343597, -10.4337, 45.8868, -47.343597, -10.4337, 45.8868, -45.4151, -10.4337, 47.9805, -47.181, -10.4337, 49.6466, -45.8121, -10.4337, 44.463898, -43.8985, -10.4337, 46.549698, -45.4151, -10.4337, 47.9805, -47.181, -10.4337, 49.6466, -45.2669, -10.4337, 51.7262, -46.8489, -10.4337, 53.263298, -45.4151, -10.4337, 47.9805, -45.2669, -10.4337, 51.7262, -47.181, -10.4337, 49.6466, -46.8489, -10.4337, 53.263298, -45.3659, -10.4337, 54.8708, -46.839, -10.4337, 56.314198, -45.2669, -10.4337, 51.7262, -45.3659, -10.4337, 54.8708, -46.8489, -10.4337, 53.263298, -45.3659, -10.4337, 54.8708, -45.411, -10.4337, 57.8783, -46.839, -10.4337, 56.314198, -43.8985, -10.4337, 46.549698, -43.524498, -10.4337, 50.0333, -45.4151, -10.4337, 47.9805, -45.4151, -10.4337, 47.9805, -43.524498, -10.4337, 50.0333, -45.2669, -10.4337, 51.7262, -43.8985, -10.4337, 46.549698, -42.032497, -10.4337, 48.5836, -43.524498, -10.4337, 50.0333, -45.2669, -10.4337, 51.7262, -43.792, -10.4337, 53.328598, -45.3659, -10.4337, 54.8708, -43.524498, -10.4337, 50.0333, -43.792, -10.4337, 53.328598, -45.2669, -10.4337, 51.7262, -45.3659, -10.4337, 54.8708, -43.935497, -10.4337, 56.421497, -45.411, -10.4337, 57.8783, -43.792, -10.4337, 53.328598, -43.935497, -10.4337, 56.421497, -45.3659, -10.4337, 54.8708, -43.935497, -10.4337, 56.421497, -43.670998, -10.4337, 59.7842, -45.411, -10.4337, 57.8783, -42.032497, -10.4337, 48.5836, -42.056, -10.4337, 51.6275, -43.524498, -10.4337, 50.0333, -43.524498, -10.4337, 50.0333, -42.056, -10.4337, 51.6275, -43.792, -10.4337, 53.328598, -42.032497, -10.4337, 48.5836, -40.572998, -10.4337, 50.1744, -42.056, -10.4337, 51.6275, -43.792, -10.4337, 53.328598, -42.368797, -10.4337, 54.874798, -43.935497, -10.4337, 56.421497, -42.056, -10.4337, 51.6275, -42.368797, -10.4337, 54.874798, -43.792, -10.4337, 53.328598, -43.935497, -10.4337, 56.421497, -42.2006, -10.4337, 58.3022, -43.670998, -10.4337, 59.7842, -42.368797, -10.4337, 54.874798, -42.2006, -10.4337, 58.3022, -43.935497, -10.4337, 56.421497, -42.2006, -10.4337, 58.3022, -41.9354, -10.4337, 61.685097, -43.670998, -10.4337, 59.7842, -40.572998, -10.4337, 50.1744, -40.6386, -10.4337, 53.1665, -42.056, -10.4337, 51.6275, -42.056, -10.4337, 51.6275, -40.6386, -10.4337, 53.1665, -42.368797, -10.4337, 54.874798, -40.572998, -10.4337, 50.1744, -39.1638, -10.4337, 51.7104, -40.6386, -10.4337, 53.1665, -42.368797, -10.4337, 54.874798, -40.6512, -10.4337, 56.7408, -42.2006, -10.4337, 58.3022, -40.6386, -10.4337, 53.1665, -40.6512, -10.4337, 56.7408, -42.368797, -10.4337, 54.874798, -42.2006, -10.4337, 58.3022, -40.4544, -10.4337, 60.1951, -41.9354, -10.4337, 61.685097, -40.6512, -10.4337, 56.7408, -40.4544, -10.4337, 60.1951, -42.2006, -10.4337, 58.3022, -40.4544, -10.4337, 60.1951, -40.3009, -10.4337, 63.4754, -41.9354, -10.4337, 61.685097, -39.1638, -10.4337, 51.7104, -38.937298, -10.4337, 55.0135, -40.6386, -10.4337, 53.1665, -40.6386, -10.4337, 53.1665, -38.937298, -10.4337, 55.0135, -40.6512, -10.4337, 56.7408, -39.1638, -10.4337, 51.7104, -37.4804, -10.4337, 53.5453, -38.937298, -10.4337, 55.0135, -40.6512, -10.4337, 56.7408, -38.9059, -10.4337, 58.6371, -40.4544, -10.4337, 60.1951, -38.937298, -10.4337, 55.0135, -38.9059, -10.4337, 58.6371, -40.6512, -10.4337, 56.7408, -40.4544, -10.4337, 60.1951, -38.822998, -10.4337, 61.9637, -40.3009, -10.4337, 63.4754, -38.9059, -10.4337, 58.6371, -38.822998, -10.4337, 61.9637, -40.4544, -10.4337, 60.1951, -38.822998, -10.4337, 61.9637, -38.953697, -10.4337, 64.951, -40.3009, -10.4337, 63.4754, -37.4804, -10.4337, 53.5453, -37.1899, -10.4337, 56.910698, -38.937298, -10.4337, 55.0135, -38.937298, -10.4337, 55.0135, -37.1899, -10.4337, 56.910698, -38.9059, -10.4337, 58.6371, -37.4804, -10.4337, 53.5453, -35.735397, -10.4337, 55.4473, -37.1899, -10.4337, 56.910698, -38.9059, -10.4337, 58.6371, -37.2887, -10.4337, 60.3941, -38.822998, -10.4337, 61.9637, -37.1899, -10.4337, 56.910698, -37.2887, -10.4337, 60.3941, -38.9059, -10.4337, 58.6371, -38.822998, -10.4337, 61.9637, -37.4867, -10.4337, 63.4123, -38.953697, -10.4337, 64.951, -37.2887, -10.4337, 60.3941, -37.4867, -10.4337, 63.4123, -38.822998, -10.4337, 61.9637, -37.4867, -10.4337, 63.4123, -37.4493, -10.4337, 66.5989, -38.953697, -10.4337, 64.951, -35.735397, -10.4337, 55.4473, -35.5858, -10.4337, 58.6522, -37.1899, -10.4337, 56.910698, -37.1899, -10.4337, 56.910698, -35.5858, -10.4337, 58.6522, -37.2887, -10.4337, 60.3941, -35.735397, -10.4337, 55.4473, -34.1461, -10.4337, 57.1796, -35.5858, -10.4337, 58.6522, -37.2887, -10.4337, 60.3941, -35.9725, -10.4337, 61.824097, -37.4867, -10.4337, 63.4123, -35.5858, -10.4337, 58.6522, -35.9725, -10.4337, 61.824097, -37.2887, -10.4337, 60.3941, -37.4867, -10.4337, 63.4123, -35.9927, -10.4337, 65.0319, -37.4493, -10.4337, 66.5989, -35.9725, -10.4337, 61.824097, -35.9927, -10.4337, 65.0319, -37.4867, -10.4337, 63.4123, -35.9927, -10.4337, 65.0319, -36.0228, -10.4337, 68.1614, -37.4493, -10.4337, 66.5989, -34.1461, -10.4337, 57.1796, -34.2898, -10.4337, 60.0593, -35.5858, -10.4337, 58.6522, -35.5858, -10.4337, 58.6522, -34.2898, -10.4337, 60.0593, -35.9725, -10.4337, 61.824097, -34.1461, -10.4337, 57.1796, -32.870197, -10.4337, 58.570297, -34.2898, -10.4337, 60.0593, -35.9725, -10.4337, 61.824097, -34.499, -10.4337, 63.425, -35.9927, -10.4337, 65.0319, -34.2898, -10.4337, 60.0593, -34.499, -10.4337, 63.425, -35.9725, -10.4337, 61.824097, -35.9927, -10.4337, 65.0319, -34.5678, -10.4337, 66.5766, -36.0228, -10.4337, 68.1614, -34.499, -10.4337, 63.425, -34.5678, -10.4337, 66.5766, -35.9927, -10.4337, 65.0319, -34.5678, -10.4337, 66.5766, -34.1187, -10.4337, 70.246895, -36.0228, -10.4337, 68.1614, -32.870197, -10.4337, 58.570297, -32.8368, -10.4337, 61.636898, -34.2898, -10.4337, 60.0593, -34.2898, -10.4337, 60.0593, -32.8368, -10.4337, 61.636898, -34.499, -10.4337, 63.425, -32.870197, -10.4337, 58.570297, -31.437601, -10.4337, 60.131798, -32.8368, -10.4337, 61.636898, -34.499, -10.4337, 63.425, -33.0849, -10.4337, 64.961395, -34.5678, -10.4337, 66.5766, -32.8368, -10.4337, 61.636898, -33.0849, -10.4337, 64.961395, -34.499, -10.4337, 63.425, -34.5678, -10.4337, 66.5766, -32.69, -10.4337, 68.6121, -34.1187, -10.4337, 70.246895, -33.0849, -10.4337, 64.961395, -32.69, -10.4337, 68.6121, -34.5678, -10.4337, 66.5766, -32.69, -10.4337, 68.6121, -32.6527, -10.4337, 71.8527, -34.1187, -10.4337, 70.246895, -31.437601, -10.4337, 60.131798, -31.4325, -10.4337, 63.1615, -32.8368, -10.4337, 61.636898, -32.8368, -10.4337, 61.636898, -31.4325, -10.4337, 63.1615, -33.0849, -10.4337, 64.961395, -31.437601, -10.4337, 60.131798, -30.044802, -10.4337, 61.649998, -31.4325, -10.4337, 63.1615, -33.0849, -10.4337, 64.961395, -31.245802, -10.4337, 66.959496, -32.69, -10.4337, 68.6121, -31.4325, -10.4337, 63.1615, -31.245802, -10.4337, 66.959496, -33.0849, -10.4337, 64.961395, -32.69, -10.4337, 68.6121, -31.2026, -10.4337, 70.2246, -32.6527, -10.4337, 71.8527, -31.245802, -10.4337, 66.959496, -31.2026, -10.4337, 70.2246, -32.69, -10.4337, 68.6121, -31.2026, -10.4337, 70.2246, -31.218801, -10.4337, 73.423195, -32.6527, -10.4337, 71.8527, -30.044802, -10.4337, 61.649998, -29.633501, -10.4337, 65.1147, -31.4325, -10.4337, 63.1615, -31.4325, -10.4337, 63.1615, -29.633501, -10.4337, 65.1147, -31.245802, -10.4337, 66.959496, -30.044802, -10.4337, 61.649998, -28.2834, -10.4337, 63.569798, -29.633501, -10.4337, 65.1147, -31.245802, -10.4337, 66.959496, -29.745901, -10.4337, 68.589, -31.2026, -10.4337, 70.2246, -29.633501, -10.4337, 65.1147, -29.745901, -10.4337, 68.589, -31.245802, -10.4337, 66.959496, -31.2026, -10.4337, 70.2246, -29.787, -10.4337, 71.7591, -31.218801, -10.4337, 73.423195, -29.745901, -10.4337, 68.589, -29.787, -10.4337, 71.7591, -31.2026, -10.4337, 70.2246, -29.787, -10.4337, 71.7591, -29.6429, -10.4337, 75.1494, -31.218801, -10.4337, 73.423195, -28.2834, -10.4337, 63.569798, -28.117498, -10.4337, 66.7606, -29.633501, -10.4337, 65.1147, -29.633501, -10.4337, 65.1147, -28.117498, -10.4337, 66.7606, -29.745901, -10.4337, 68.589, -28.2834, -10.4337, 63.569798, -26.7572, -10.4337, 65.2334, -28.117498, -10.4337, 66.7606, -29.745901, -10.4337, 68.589, -28.357399, -10.4337, 70.097496, -29.787, -10.4337, 71.7591, -28.117498, -10.4337, 66.7606, -28.357399, -10.4337, 70.097496, -29.745901, -10.4337, 68.589, -29.787, -10.4337, 71.7591, -28.1941, -10.4337, 73.4859, -29.6429, -10.4337, 75.1494, -28.357399, -10.4337, 70.097496, -28.1941, -10.4337, 73.4859, -29.787, -10.4337, 71.7591, -28.1941, -10.4337, 73.4859, -28.2134, -10.4337, 76.715096, -29.6429, -10.4337, 75.1494, -26.7572, -10.4337, 65.2334, -26.7572, -10.4337, 68.237495, -28.117498, -10.4337, 66.7606, -28.117498, -10.4337, 66.7606, -26.7572, -10.4337, 68.237495, -28.357399, -10.4337, 70.097496, -26.7572, -10.4337, 68.237495, -26.7572, -10.4337, 65.2334, -23.412498, -10.4337, 65.2334, -28.1941, -10.4337, 73.4859, -26.7572, -10.4337, 75.043594, -28.2134, -10.4337, 76.715096, -26.7572, -10.4337, 75.043594, -26.7572, -10.4337, 78.3101, -28.2134, -10.4337, 76.715096, -28.357399, -10.4337, 70.097496, -26.7572, -10.4337, 71.8361, -28.1941, -10.4337, 73.4859, -26.7572, -10.4337, 68.237495, -26.7572, -10.4337, 71.8361, -28.357399, -10.4337, 70.097496, -26.7572, -10.4337, 71.8361, -26.7572, -10.4337, 75.043594, -28.1941, -10.4337, 73.4859, -23.412498, -10.4337, 78.3101, -26.7572, -10.4337, 78.3101, -26.7572, -10.4337, 75.043594, -26.7572, -10.4337, 71.8361, -26.7572, -10.4337, 68.237495, -23.412498, -10.4337, 68.237495, -26.7572, -10.4337, 68.237495, -23.412498, -10.4337, 65.2334, -23.412498, -10.4337, 68.237495, -23.412498, -10.4337, 68.237495, -23.412498, -10.4337, 65.2334, -20.0679, -10.4337, 65.2334, -26.7572, -10.4337, 71.8361, -23.412498, -10.4337, 68.237495, -23.412498, -10.4337, 71.8361, -26.7572, -10.4337, 75.043594, -26.7572, -10.4337, 71.8361, -23.412498, -10.4337, 71.8361, -23.412498, -10.4337, 68.237495, -20.0679, -10.4337, 65.2334, -20.0679, -10.4337, 68.237495, -23.412498, -10.4337, 71.8361, -23.412498, -10.4337, 68.237495, -20.0679, -10.4337, 68.237495, -16.7232, -10.4337, 65.2334, -20.0679, -10.4337, 68.237495, -20.0679, -10.4337, 65.2334, -23.412498, -10.4337, 78.3101, -26.7572, -10.4337, 75.043594, -23.412498, -10.4337, 75.043594, -26.7572, -10.4337, 75.043594, -23.412498, -10.4337, 71.8361, -23.412498, -10.4337, 75.043594, -20.0679, -10.4337, 78.3101, -23.412498, -10.4337, 78.3101, -23.412498, -10.4337, 75.043594, -23.412498, -10.4337, 71.8361, -20.0679, -10.4337, 68.237495, -20.0679, -10.4337, 71.8361, -23.412498, -10.4337, 75.043594, -23.412498, -10.4337, 71.8361, -20.0679, -10.4337, 71.8361, -16.7232, -10.4337, 65.2334, -16.7232, -10.4337, 68.237495, -20.0679, -10.4337, 68.237495, -16.7232, -10.4337, 68.237495, -20.0679, -10.4337, 71.8361, -20.0679, -10.4337, 68.237495, -16.7232, -10.4337, 68.237495, -16.7232, -10.4337, 65.2334, -13.378601, -10.4337, 65.2334, -23.412498, -10.4337, 75.043594, -20.0679, -10.4337, 71.8361, -20.0679, -10.4337, 75.043594, -20.0679, -10.4337, 78.3101, -23.412498, -10.4337, 75.043594, -20.0679, -10.4337, 75.043594, -16.7232, -10.4337, 68.237495, -16.7232, -10.4337, 71.8361, -20.0679, -10.4337, 71.8361, -16.7232, -10.4337, 71.8361, -20.0679, -10.4337, 75.043594, -20.0679, -10.4337, 71.8361, -16.7232, -10.4337, 75.043594, -20.0679, -10.4337, 78.3101, -20.0679, -10.4337, 75.043594, -16.7232, -10.4337, 71.8361, -16.7232, -10.4337, 75.043594, -20.0679, -10.4337, 75.043594, -16.7232, -10.4337, 75.043594, -16.7232, -10.4337, 78.3101, -20.0679, -10.4337, 78.3101, -16.7232, -10.4337, 71.8361, -16.7232, -10.4337, 68.237495, -13.378601, -10.4337, 68.237495, -16.7232, -10.4337, 68.237495, -13.378601, -10.4337, 65.2334, -13.378601, -10.4337, 68.237495, -16.7232, -10.4337, 75.043594, -16.7232, -10.4337, 71.8361, -13.378601, -10.4337, 71.8361, -16.7232, -10.4337, 71.8361, -13.378601, -10.4337, 68.237495, -13.378601, -10.4337, 71.8361, -16.7232, -10.4337, 78.3101, -16.7232, -10.4337, 75.043594, -13.378601, -10.4337, 75.043594, -16.7232, -10.4337, 75.043594, -13.378601, -10.4337, 71.8361, -13.378601, -10.4337, 75.043594, -16.7232, -10.4337, 78.3101, -13.378601, -10.4337, 75.043594, -13.378601, -10.4337, 78.3101, -13.378601, -10.4337, 65.2334, -10.033899, -10.4337, 68.237495, -13.378601, -10.4337, 68.237495, -10.033899, -10.4337, 68.237495, -13.378601, -10.4337, 71.8361, -13.378601, -10.4337, 68.237495, -13.378601, -10.4337, 65.2334, -10.033899, -10.4337, 65.2334, -10.033899, -10.4337, 68.237495, -10.033899, -10.4337, 71.8361, -13.378601, -10.4337, 75.043594, -13.378601, -10.4337, 71.8361, -10.033899, -10.4337, 68.237495, -10.033899, -10.4337, 71.8361, -13.378601, -10.4337, 71.8361, -10.033899, -10.4337, 75.043594, -13.378601, -10.4337, 78.3101, -13.378601, -10.4337, 75.043594, -10.033899, -10.4337, 71.8361, -10.033899, -10.4337, 75.043594, -13.378601, -10.4337, 75.043594, -10.033899, -10.4337, 75.043594, -10.033899, -10.4337, 78.3101, -13.378601, -10.4337, 78.3101, -10.033899, -10.4337, 65.2334, -6.6893, -10.4337, 68.237495, -10.033899, -10.4337, 68.237495, -6.6893, -10.4337, 68.237495, -10.033899, -10.4337, 71.8361, -10.033899, -10.4337, 68.237495, -10.033899, -10.4337, 65.2334, -6.6893, -10.4337, 65.2334, -6.6893, -10.4337, 68.237495, -6.6893, -10.4337, 71.8361, -10.033899, -10.4337, 75.043594, -10.033899, -10.4337, 71.8361, -6.6893, -10.4337, 68.237495, -6.6893, -10.4337, 71.8361, -10.033899, -10.4337, 71.8361, -6.6893, -10.4337, 75.043594, -10.033899, -10.4337, 78.3101, -10.033899, -10.4337, 75.043594, -6.6893, -10.4337, 71.8361, -6.6893, -10.4337, 75.043594, -10.033899, -10.4337, 75.043594, -6.6893, -10.4337, 75.043594, -6.6893, -10.4337, 78.3101, -10.033899, -10.4337, 78.3101, -6.6893, -10.4337, 65.2334, -3.3446002, -10.4337, 68.237495, -6.6893, -10.4337, 68.237495, -3.3446002, -10.4337, 68.237495, -6.6893, -10.4337, 71.8361, -6.6893, -10.4337, 68.237495, -6.6893, -10.4337, 65.2334, -3.3446002, -10.4337, 65.2334, -3.3446002, -10.4337, 68.237495, -3.3446002, -10.4337, 71.8361, -6.6893, -10.4337, 75.043594, -6.6893, -10.4337, 71.8361, -3.3446002, -10.4337, 68.237495, -3.3446002, -10.4337, 71.8361, -6.6893, -10.4337, 71.8361, -3.3446002, -10.4337, 75.043594, -6.6893, -10.4337, 78.3101, -6.6893, -10.4337, 75.043594, -3.3446002, -10.4337, 71.8361, -3.3446002, -10.4337, 75.043594, -6.6893, -10.4337, 75.043594, -3.3446002, -10.4337, 75.043594, -3.3446002, -10.4337, 78.3101, -6.6893, -10.4337, 78.3101, -3.3446002, -10.4337, 65.2334, 0, -10.4337, 68.237495, -3.3446002, -10.4337, 68.237495, 0, -10.4337, 68.237495, -3.3446002, -10.4337, 71.8361, -3.3446002, -10.4337, 68.237495, -3.3446002, -10.4337, 65.2334, 0, -10.4337, 65.2334, 0, -10.4337, 68.237495, 0, -10.4337, 71.8361, -3.3446002, -10.4337, 75.043594, -3.3446002, -10.4337, 71.8361, 0, -10.4337, 68.237495, 0, -10.4337, 71.8361, -3.3446002, -10.4337, 71.8361, 0, -10.4337, 75.043594, -3.3446002, -10.4337, 78.3101, -3.3446002, -10.4337, 75.043594, 0, -10.4337, 71.8361, 0, -10.4337, 75.043594, -3.3446002, -10.4337, 75.043594, 0, -10.4337, 75.043594, 0, -10.4337, 78.3101, -3.3446002, -10.4337, 78.3101, 0, -10.4337, 65.2334, 3.3446, -10.4337, 68.237495, 0, -10.4337, 68.237495, 3.3446, -10.4337, 68.237495, 0, -10.4337, 71.8361, 0, -10.4337, 68.237495, 0, -10.4337, 65.2334, 3.3446, -10.4337, 65.2334, 3.3446, -10.4337, 68.237495, 3.3446, -10.4337, 71.8361, 0, -10.4337, 75.043594, 0, -10.4337, 71.8361, 3.3446, -10.4337, 68.237495, 3.3446, -10.4337, 71.8361, 0, -10.4337, 71.8361, 3.3446, -10.4337, 75.043594, 0, -10.4337, 78.3101, 0, -10.4337, 75.043594, 3.3446, -10.4337, 71.8361, 3.3446, -10.4337, 75.043594, 0, -10.4337, 75.043594, 3.3446, -10.4337, 75.043594, 3.3446, -10.4337, 78.3101, 0, -10.4337, 78.3101, 3.3446, -10.4337, 65.2334, 6.6893, -10.4337, 68.237495, 3.3446, -10.4337, 68.237495, 6.6893, -10.4337, 68.237495, 3.3446, -10.4337, 71.8361, 3.3446, -10.4337, 68.237495, 3.3446, -10.4337, 65.2334, 6.6893, -10.4337, 65.2334, 6.6893, -10.4337, 68.237495, 6.6893, -10.4337, 71.8361, 3.3446, -10.4337, 75.043594, 3.3446, -10.4337, 71.8361, 6.6893, -10.4337, 68.237495, 6.6893, -10.4337, 71.8361, 3.3446, -10.4337, 71.8361, 6.6893, -10.4337, 75.043594, 3.3446, -10.4337, 78.3101, 3.3446, -10.4337, 75.043594, 6.6893, -10.4337, 71.8361, 6.6893, -10.4337, 75.043594, 3.3446, -10.4337, 75.043594, 6.6893, -10.4337, 75.043594, 6.6893, -10.4337, 78.3101, 3.3446, -10.4337, 78.3101, 6.6893, -10.4337, 65.2334, 10.033899, -10.4337, 68.237495, 6.6893, -10.4337, 68.237495, 10.033899, -10.4337, 68.237495, 6.6893, -10.4337, 71.8361, 6.6893, -10.4337, 68.237495, 6.6893, -10.4337, 65.2334, 10.033899, -10.4337, 65.2334, 10.033899, -10.4337, 68.237495, 10.033899, -10.4337, 71.8361, 6.6893, -10.4337, 75.043594, 6.6893, -10.4337, 71.8361, 10.033899, -10.4337, 68.237495, 10.033899, -10.4337, 71.8361, 6.6893, -10.4337, 71.8361, 10.033899, -10.4337, 75.043594, 6.6893, -10.4337, 78.3101, 6.6893, -10.4337, 75.043594, 10.033899, -10.4337, 71.8361, 10.033899, -10.4337, 75.043594, 6.6893, -10.4337, 75.043594, 10.033899, -10.4337, 75.043594, 10.033899, -10.4337, 78.3101, 6.6893, -10.4337, 78.3101, 10.033899, -10.4337, 65.2334, 13.378599, -10.4337, 68.237495, 10.033899, -10.4337, 68.237495, 13.378599, -10.4337, 68.237495, 10.033899, -10.4337, 71.8361, 10.033899, -10.4337, 68.237495, 10.033899, -10.4337, 65.2334, 13.378599, -10.4337, 65.2334, 13.378599, -10.4337, 68.237495, 13.378599, -10.4337, 71.8361, 10.033899, -10.4337, 75.043594, 10.033899, -10.4337, 71.8361, 13.378599, -10.4337, 68.237495, 13.378599, -10.4337, 71.8361, 10.033899, -10.4337, 71.8361, 13.378599, -10.4337, 75.043594, 10.033899, -10.4337, 78.3101, 10.033899, -10.4337, 75.043594, 13.378599, -10.4337, 71.8361, 13.378599, -10.4337, 75.043594, 10.033899, -10.4337, 75.043594, 13.378599, -10.4337, 75.043594, 13.378599, -10.4337, 78.3101, 10.033899, -10.4337, 78.3101, 13.378599, -10.4337, 65.2334, 16.7232, -10.4337, 68.237495, 13.378599, -10.4337, 68.237495, 16.7232, -10.4337, 68.237495, 13.378599, -10.4337, 71.8361, 13.378599, -10.4337, 68.237495, 13.378599, -10.4337, 65.2334, 16.7232, -10.4337, 65.2334, 16.7232, -10.4337, 68.237495, 16.7232, -10.4337, 71.8361, 13.378599, -10.4337, 75.043594, 13.378599, -10.4337, 71.8361, 16.7232, -10.4337, 68.237495, 16.7232, -10.4337, 71.8361, 13.378599, -10.4337, 71.8361, 16.7232, -10.4337, 75.043594, 13.378599, -10.4337, 78.3101, 13.378599, -10.4337, 75.043594, 16.7232, -10.4337, 71.8361, 16.7232, -10.4337, 75.043594, 13.378599, -10.4337, 75.043594, 16.7232, -10.4337, 75.043594, 16.7232, -10.4337, 78.3101, 13.378599, -10.4337, 78.3101, 16.7232, -10.4337, 65.2334, 20.0679, -10.4337, 68.237495, 16.7232, -10.4337, 68.237495, 20.0679, -10.4337, 68.237495, 16.7232, -10.4337, 71.8361, 16.7232, -10.4337, 68.237495, 16.7232, -10.4337, 65.2334, 20.0679, -10.4337, 65.2334, 20.0679, -10.4337, 68.237495, 20.0679, -10.4337, 71.8361, 16.7232, -10.4337, 75.043594, 16.7232, -10.4337, 71.8361, 20.0679, -10.4337, 68.237495, 20.0679, -10.4337, 71.8361, 16.7232, -10.4337, 71.8361, 20.0679, -10.4337, 75.043594, 16.7232, -10.4337, 78.3101, 16.7232, -10.4337, 75.043594, 20.0679, -10.4337, 71.8361, 20.0679, -10.4337, 75.043594, 16.7232, -10.4337, 75.043594, 20.0679, -10.4337, 75.043594, 20.0679, -10.4337, 78.3101, 16.7232, -10.4337, 78.3101, 20.0679, -10.4337, 65.2334, 23.412498, -10.4337, 68.237495, 20.0679, -10.4337, 68.237495, 23.412498, -10.4337, 68.237495, 20.0679, -10.4337, 71.8361, 20.0679, -10.4337, 68.237495, 20.0679, -10.4337, 65.2334, 23.412498, -10.4337, 65.2334, 23.412498, -10.4337, 68.237495, 23.412498, -10.4337, 71.8361, 20.0679, -10.4337, 75.043594, 20.0679, -10.4337, 71.8361, 23.412498, -10.4337, 68.237495, 23.412498, -10.4337, 71.8361, 20.0679, -10.4337, 71.8361, 23.412498, -10.4337, 75.043594, 20.0679, -10.4337, 78.3101, 20.0679, -10.4337, 75.043594, 23.412498, -10.4337, 71.8361, 23.412498, -10.4337, 75.043594, 20.0679, -10.4337, 75.043594, 23.412498, -10.4337, 75.043594, 23.412498, -10.4337, 78.3101, 20.0679, -10.4337, 78.3101, 23.412498, -10.4337, 65.2334, 26.7582, -10.4337, 68.237495, 23.412498, -10.4337, 68.237495, 26.7582, -10.4337, 68.237495, 23.412498, -10.4337, 71.8361, 23.412498, -10.4337, 68.237495, 23.412498, -10.4337, 65.2334, 26.7582, -10.4337, 65.2334, 26.7582, -10.4337, 68.237495, 26.7582, -10.4337, 71.8361, 23.412498, -10.4337, 75.043594, 23.412498, -10.4337, 71.8361, 26.7582, -10.4337, 68.237495, 26.7582, -10.4337, 71.8361, 23.412498, -10.4337, 71.8361, 26.7582, -10.4337, 75.043594, 23.412498, -10.4337, 78.3101, 23.412498, -10.4337, 75.043594, 26.7582, -10.4337, 71.8361, 26.7582, -10.4337, 75.043594, 23.412498, -10.4337, 75.043594, 26.7582, -10.4337, 75.043594, 26.7582, -10.4337, 78.3101, 23.412498, -10.4337, 78.3101, 26.7582, -10.4337, 75.043594, 75.8818, -10.4337, 26.7572, 26.7582, -10.4337, 78.3101, 26.7582, -10.4337, 71.8361, 75.8818, -10.4337, 26.7572, 26.7582, -10.4337, 75.043594, 26.7582, -10.4337, 78.3101, 75.8818, -10.4337, 26.7572, 79.236, -10.4337, 26.7572, 79.236, -10.4337, 23.412498, 79.236, -10.4337, 26.7572, 75.8818, -10.4337, 26.7572, 71.9379, -10.4337, 26.7572, 26.7582, -10.4337, 71.8361, 26.7582, -10.4337, 68.237495, 75.8818, -10.4337, 26.7572, 26.7582, -10.4337, 71.8361, 71.9379, -10.4337, 26.7572, 68.9355, -10.4337, 26.7572, 26.7582, -10.4337, 68.237495, 26.7582, -10.4337, 65.2334, 71.9379, -10.4337, 26.7572, 26.7582, -10.4337, 68.237495, 68.9355, -10.4337, 26.7572, 68.9355, -10.4337, 26.7572, 26.7582, -10.4337, 65.2334, 66.1166, -10.4337, 26.7572, 68.9355, -10.4337, 26.7572, 66.1166, -10.4337, 26.7572, 66.1166, -10.4337, 23.412498, 68.9355, -10.4337, 26.7572, 66.1166, -10.4337, 23.412498, 68.937, -10.4337, 23.412498, 71.9379, -10.4337, 26.7572, 68.9355, -10.4337, 26.7572, 68.937, -10.4337, 23.412498, 68.937, -10.4337, 23.412498, 66.1166, -10.4337, 23.412498, 66.1166, -10.4337, 20.0679, 75.8818, -10.4337, 26.7572, 71.9379, -10.4337, 26.7572, 71.9555, -10.4337, 23.412498, 71.9379, -10.4337, 26.7572, 68.937, -10.4337, 23.412498, 71.9555, -10.4337, 23.412498, 79.236, -10.4337, 23.412498, 75.8818, -10.4337, 26.7572, 75.8708, -10.4337, 23.412498, 75.8818, -10.4337, 26.7572, 71.9555, -10.4337, 23.412498, 75.8708, -10.4337, 23.412498, 79.236, -10.4337, 20.0679, 79.236, -10.4337, 23.412498, 75.8708, -10.4337, 23.412498, 68.937, -10.4337, 23.412498, 66.1166, -10.4337, 20.0679, 68.9385, -10.4337, 20.0679, 71.9555, -10.4337, 23.412498, 68.937, -10.4337, 23.412498, 68.9385, -10.4337, 20.0679, 68.9385, -10.4337, 20.0679, 66.1166, -10.4337, 20.0679, 66.1166, -10.4337, 16.7232, 75.8708, -10.4337, 23.412498, 71.9555, -10.4337, 23.412498, 71.9732, -10.4337, 20.0679, 71.9555, -10.4337, 23.412498, 68.9385, -10.4337, 20.0679, 71.9732, -10.4337, 20.0679, 79.236, -10.4337, 20.0679, 75.8708, -10.4337, 23.412498, 75.859795, -10.4337, 20.0679, 75.8708, -10.4337, 23.412498, 71.9732, -10.4337, 20.0679, 75.859795, -10.4337, 20.0679, 79.236, -10.4337, 16.7232, 79.236, -10.4337, 20.0679, 75.859795, -10.4337, 20.0679, 68.9385, -10.4337, 20.0679, 66.1166, -10.4337, 16.7232, 68.9401, -10.4337, 16.7232, 71.9732, -10.4337, 20.0679, 68.9385, -10.4337, 20.0679, 68.9401, -10.4337, 16.7232, 68.9401, -10.4337, 16.7232, 66.1166, -10.4337, 16.7232, 66.1166, -10.4337, 13.378601, 75.859795, -10.4337, 20.0679, 71.9732, -10.4337, 20.0679, 71.9908, -10.4337, 16.7232, 71.9732, -10.4337, 20.0679, 68.9401, -10.4337, 16.7232, 71.9908, -10.4337, 16.7232, 79.236, -10.4337, 16.7232, 75.859795, -10.4337, 20.0679, 75.8488, -10.4337, 16.7232, 75.859795, -10.4337, 20.0679, 71.9908, -10.4337, 16.7232, 75.8488, -10.4337, 16.7232, 79.236, -10.4337, 13.378601, 79.236, -10.4337, 16.7232, 75.8488, -10.4337, 16.7232, 68.9401, -10.4337, 16.7232, 66.1166, -10.4337, 13.378601, 68.9416, -10.4337, 13.378601, 71.9908, -10.4337, 16.7232, 68.9401, -10.4337, 16.7232, 68.9416, -10.4337, 13.378601, 68.9416, -10.4337, 13.378601, 66.1166, -10.4337, 13.378601, 66.1166, -10.4337, 10.033899, 75.8488, -10.4337, 16.7232, 71.9908, -10.4337, 16.7232, 72.0085, -10.4337, 13.378601, 71.9908, -10.4337, 16.7232, 68.9416, -10.4337, 13.378601, 72.0085, -10.4337, 13.378601, 79.236, -10.4337, 13.378601, 75.8488, -10.4337, 16.7232, 75.8377, -10.4337, 13.378601, 75.8488, -10.4337, 16.7232, 72.0085, -10.4337, 13.378601, 75.8377, -10.4337, 13.378601, 79.236, -10.4337, 10.033899, 79.236, -10.4337, 13.378601, 75.8377, -10.4337, 13.378601, 68.9416, -10.4337, 13.378601, 66.1166, -10.4337, 10.033899, 68.9432, -10.4337, 10.033899, 72.0085, -10.4337, 13.378601, 68.9416, -10.4337, 13.378601, 68.9432, -10.4337, 10.033899, 68.9432, -10.4337, 10.033899, 66.1166, -10.4337, 10.033899, 66.1166, -10.4337, 6.6893, 75.8377, -10.4337, 13.378601, 72.0085, -10.4337, 13.378601, 72.0261, -10.4337, 10.033899, 72.0085, -10.4337, 13.378601, 68.9432, -10.4337, 10.033899, 72.0261, -10.4337, 10.033899, 79.236, -10.4337, 10.033899, 75.8377, -10.4337, 13.378601, 75.8267, -10.4337, 10.033899, 75.8377, -10.4337, 13.378601, 72.0261, -10.4337, 10.033899, 75.8267, -10.4337, 10.033899, 79.236, -10.4337, 6.6893, 79.236, -10.4337, 10.033899, 75.8267, -10.4337, 10.033899, 68.9432, -10.4337, 10.033899, 66.1166, -10.4337, 6.6893, 68.944695, -10.4337, 6.6893, 72.0261, -10.4337, 10.033899, 68.9432, -10.4337, 10.033899, 68.944695, -10.4337, 6.6893, 68.944695, -10.4337, 6.6893, 66.1166, -10.4337, 6.6893, 66.1166, -10.4337, 3.3446, 75.8267, -10.4337, 10.033899, 72.0261, -10.4337, 10.033899, 72.0437, -10.4337, 6.6893, 72.0261, -10.4337, 10.033899, 68.944695, -10.4337, 6.6893, 72.0437, -10.4337, 6.6893, 79.236, -10.4337, 6.6893, 75.8267, -10.4337, 10.033899, 75.8157, -10.4337, 6.6893, 75.8267, -10.4337, 10.033899, 72.0437, -10.4337, 6.6893, 75.8157, -10.4337, 6.6893, 79.236, -10.4337, 3.3446, 79.236, -10.4337, 6.6893, 75.8157, -10.4337, 6.6893, 68.944695, -10.4337, 6.6893, 66.1166, -10.4337, 3.3446, 68.9463, -10.4337, 3.3446, 72.0437, -10.4337, 6.6893, 68.944695, -10.4337, 6.6893, 68.9463, -10.4337, 3.3446, 68.9463, -10.4337, 3.3446, 66.1166, -10.4337, 3.3446, 66.1166, -10.4337, 0, 75.8157, -10.4337, 6.6893, 72.0437, -10.4337, 6.6893, 72.0614, -10.4337, 3.3446, 72.0437, -10.4337, 6.6893, 68.9463, -10.4337, 3.3446, 72.0614, -10.4337, 3.3446, 79.236, -10.4337, 3.3446, 75.8157, -10.4337, 6.6893, 75.804695, -10.4337, 3.3446, 75.8157, -10.4337, 6.6893, 72.0614, -10.4337, 3.3446, 75.804695, -10.4337, 3.3446, 79.236, -10.4337, 0, 79.236, -10.4337, 3.3446, 75.804695, -10.4337, 3.3446, 68.9463, -10.4337, 3.3446, 66.1166, -10.4337, 0, 68.9478, -10.4337, 0, 72.0614, -10.4337, 3.3446, 68.9463, -10.4337, 3.3446, 68.9478, -10.4337, 0, 68.9478, -10.4337, 0, 66.1166, -10.4337, 0, 66.1166, -10.4337, -3.3446002, 75.804695, -10.4337, 3.3446, 72.0614, -10.4337, 3.3446, 72.078995, -10.4337, 0, 72.0614, -10.4337, 3.3446, 68.9478, -10.4337, 0, 72.078995, -10.4337, 0, 79.236, -10.4337, 0, 75.804695, -10.4337, 3.3446, 75.793594, -10.4337, 0, 75.804695, -10.4337, 3.3446, 72.078995, -10.4337, 0, 75.793594, -10.4337, 0, 79.236, -10.4337, -3.3446002, 79.236, -10.4337, 0, 75.793594, -10.4337, 0, 68.9478, -10.4337, 0, 66.1166, -10.4337, -3.3446002, 68.9494, -10.4337, -3.3446002, 72.078995, -10.4337, 0, 68.9478, -10.4337, 0, 68.9494, -10.4337, -3.3446002, 68.9494, -10.4337, -3.3446002, 66.1166, -10.4337, -3.3446002, 66.1166, -10.4337, -6.6893, 75.793594, -10.4337, 0, 72.078995, -10.4337, 0, 72.096695, -10.4337, -3.3446002, 72.078995, -10.4337, 0, 68.9494, -10.4337, -3.3446002, 72.096695, -10.4337, -3.3446002, 79.236, -10.4337, -3.3446002, 75.793594, -10.4337, 0, 75.7826, -10.4337, -3.3446002, 75.793594, -10.4337, 0, 72.096695, -10.4337, -3.3446002, 75.7826, -10.4337, -3.3446002, 79.236, -10.4337, -6.6893, 79.236, -10.4337, -3.3446002, 75.7826, -10.4337, -3.3446002, 68.9494, -10.4337, -3.3446002, 66.1166, -10.4337, -6.6893, 68.9509, -10.4337, -6.6893, 72.096695, -10.4337, -3.3446002, 68.9494, -10.4337, -3.3446002, 68.9509, -10.4337, -6.6893, 68.9509, -10.4337, -6.6893, 66.1166, -10.4337, -6.6893, 66.1166, -10.4337, -10.033899, 75.7826, -10.4337, -3.3446002, 72.096695, -10.4337, -3.3446002, 72.114296, -10.4337, -6.6893, 72.096695, -10.4337, -3.3446002, 68.9509, -10.4337, -6.6893, 72.114296, -10.4337, -6.6893, 79.236, -10.4337, -6.6893, 75.7826, -10.4337, -3.3446002, 75.7716, -10.4337, -6.6893, 75.7826, -10.4337, -3.3446002, 72.114296, -10.4337, -6.6893, 75.7716, -10.4337, -6.6893, 79.236, -10.4337, -10.033899, 79.236, -10.4337, -6.6893, 75.7716, -10.4337, -6.6893, 68.9509, -10.4337, -6.6893, 66.1166, -10.4337, -10.033899, 68.9524, -10.4337, -10.033899, 72.114296, -10.4337, -6.6893, 68.9509, -10.4337, -6.6893, 68.9524, -10.4337, -10.033899, 68.9524, -10.4337, -10.033899, 66.1166, -10.4337, -10.033899, 66.1166, -10.4337, -13.378599, 75.7716, -10.4337, -6.6893, 72.114296, -10.4337, -6.6893, 72.1319, -10.4337, -10.033899, 72.114296, -10.4337, -6.6893, 68.9524, -10.4337, -10.033899, 72.1319, -10.4337, -10.033899, 79.236, -10.4337, -10.033899, 75.7716, -10.4337, -6.6893, 75.7606, -10.4337, -10.033899, 75.7716, -10.4337, -6.6893, 72.1319, -10.4337, -10.033899, 75.7606, -10.4337, -10.033899, 79.236, -10.4337, -13.378599, 79.236, -10.4337, -10.033899, 75.7606, -10.4337, -10.033899, 68.9524, -10.4337, -10.033899, 66.1166, -10.4337, -13.378599, 68.953995, -10.4337, -13.378599, 72.1319, -10.4337, -10.033899, 68.9524, -10.4337, -10.033899, 68.953995, -10.4337, -13.378599, 68.953995, -10.4337, -13.378599, 66.1166, -10.4337, -13.378599, 66.1166, -10.4337, -16.7232, 75.7606, -10.4337, -10.033899, 72.1319, -10.4337, -10.033899, 72.1496, -10.4337, -13.378599, 72.1319, -10.4337, -10.033899, 68.953995, -10.4337, -13.378599, 72.1496, -10.4337, -13.378599, 79.236, -10.4337, -13.378599, 75.7606, -10.4337, -10.033899, 75.749596, -10.4337, -13.378599, 75.7606, -10.4337, -10.033899, 72.1496, -10.4337, -13.378599, 75.749596, -10.4337, -13.378599, 79.236, -10.4337, -16.7232, 79.236, -10.4337, -13.378599, 75.749596, -10.4337, -13.378599, 68.953995, -10.4337, -13.378599, 66.1166, -10.4337, -16.7232, 68.9555, -10.4337, -16.7232, 72.1496, -10.4337, -13.378599, 68.953995, -10.4337, -13.378599, 68.9555, -10.4337, -16.7232, 68.9555, -10.4337, -16.7232, 66.1166, -10.4337, -16.7232, 66.1166, -10.4337, -20.0679, 75.749596, -10.4337, -13.378599, 72.1496, -10.4337, -13.378599, 72.1672, -10.4337, -16.7232, 72.1496, -10.4337, -13.378599, 68.9555, -10.4337, -16.7232, 72.1672, -10.4337, -16.7232, 79.236, -10.4337, -16.7232, 75.749596, -10.4337, -13.378599, 75.738495, -10.4337, -16.7232, 75.749596, -10.4337, -13.378599, 72.1672, -10.4337, -16.7232, 75.738495, -10.4337, -16.7232, 79.236, -10.4337, -20.0679, 79.236, -10.4337, -16.7232, 75.738495, -10.4337, -16.7232, 68.9555, -10.4337, -16.7232, 66.1166, -10.4337, -20.0679, 68.9571, -10.4337, -20.0679, 72.1672, -10.4337, -16.7232, 68.9555, -10.4337, -16.7232, 68.9571, -10.4337, -20.0679, 68.9571, -10.4337, -20.0679, 66.1166, -10.4337, -20.0679, 66.1166, -10.4337, -23.412498, 68.9571, -10.4337, -20.0679, 66.1166, -10.4337, -23.412498, 68.958595, -10.4337, -23.412498, 72.1672, -10.4337, -16.7232, 68.9571, -10.4337, -20.0679, 72.1849, -10.4337, -20.0679, 72.1849, -10.4337, -20.0679, 68.9571, -10.4337, -20.0679, 68.958595, -10.4337, -23.412498, 75.738495, -10.4337, -16.7232, 72.1672, -10.4337, -16.7232, 72.1849, -10.4337, -20.0679, 72.1849, -10.4337, -20.0679, 68.958595, -10.4337, -23.412498, 72.2025, -10.4337, -23.412498, 79.236, -10.4337, -20.0679, 75.738495, -10.4337, -16.7232, 75.7275, -10.4337, -20.0679, 75.738495, -10.4337, -16.7232, 72.1849, -10.4337, -20.0679, 75.7275, -10.4337, -20.0679, 75.7275, -10.4337, -20.0679, 72.1849, -10.4337, -20.0679, 72.2025, -10.4337, -23.412498, 79.236, -10.4337, -23.412498, 79.236, -10.4337, -20.0679, 75.7275, -10.4337, -20.0679, 75.7275, -10.4337, -20.0679, 72.2025, -10.4337, -23.412498, 75.7165, -10.4337, -23.412498, 79.236, -10.4337, -23.412498, 75.7275, -10.4337, -20.0679, 75.7165, -10.4337, -23.412498, -38.5239, -3.6573, 10.033899, -41.627, -4.8403997, 6.6893, -38.5239, -3.6573, 6.6893, -38.5239, -3.6573, 10.033899, -38.5239, -3.6573, 13.378601, -41.627, -4.8403997, 6.6893, -41.627, -4.8403997, 3.3446, -38.5239, -3.6573, 6.6893, -41.627, -4.8403997, 6.6893, -41.627, -4.8403997, 3.3446, -38.5239, -3.6573, 3.3446, -38.5239, -3.6573, 6.6893, -41.627, -4.8403997, 6.6893, -38.5239, -3.6573, 13.378601, -41.627, -4.8403997, 13.378601, -41.627, -4.8403997, 0, -38.5239, -3.6573, 3.3446, -41.627, -4.8403997, 3.3446, -41.627, -4.8403997, 0, -38.5239, -3.6573, 0, -38.5239, -3.6573, 3.3446, -44.537098, -5.95, 3.3446, -41.627, -4.8403997, 3.3446, -41.627, -4.8403997, 6.6893, -41.627, -4.8403997, -3.3446002, -38.5239, -3.6573, 0, -41.627, -4.8403997, 0, -41.627, -4.8403997, -3.3446002, -38.5239, -3.6573, -3.3446002, -38.5239, -3.6573, 0, -44.537098, -5.95, 0, -41.627, -4.8403997, 0, -41.627, -4.8403997, 3.3446, -44.537098, -5.95, 0, -41.627, -4.8403997, 3.3446, -44.537098, -5.95, 3.3446, -44.537098, -5.95, -3.3446002, -41.627, -4.8403997, -3.3446002, -41.627, -4.8403997, 0, -44.537098, -5.95, -3.3446002, -41.627, -4.8403997, 0, -44.537098, -5.95, 0, -41.627, -4.8403997, -6.6893, -38.5239, -3.6573, -3.3446002, -41.627, -4.8403997, -3.3446002, -41.627, -4.8403997, -6.6893, -38.5239, -3.6573, -6.6893, -38.5239, -3.6573, -3.3446002, -44.537098, -5.95, -6.6893, -41.627, -4.8403997, -3.3446002, -44.537098, -5.95, -3.3446002, -44.537098, -5.95, -6.6893, -41.627, -4.8403997, -6.6893, -41.627, -4.8403997, -3.3446002, -41.627, -4.8403997, -10.033899, -38.5239, -3.6573, -6.6893, -41.627, -4.8403997, -6.6893, -41.627, -4.8403997, -10.033899, -38.5239, -3.6573, -10.033899, -38.5239, -3.6573, -6.6893, -44.537098, -5.95, -10.033899, -41.627, -4.8403997, -6.6893, -44.537098, -5.95, -6.6893, -44.537098, -5.95, -10.033899, -41.627, -4.8403997, -10.033899, -41.627, -4.8403997, -6.6893, -41.627, -4.8403997, -13.378599, -38.5239, -3.6573, -10.033899, -41.627, -4.8403997, -10.033899, -41.627, -4.8403997, -13.378599, -38.5239, -3.6573, -13.378599, -38.5239, -3.6573, -10.033899, -41.627, -4.8403997, -13.378599, -38.5239, -3.6573, -16.7232, -38.5239, -3.6573, -13.378599, -41.627, -4.8403997, -13.378599, -38.5239, -3.6573, -17.0441, -38.5239, -3.6573, -16.7232, -38.5239, -3.6573, -17.0441, -41.627, -4.8403997, -13.378599, -41.627, -4.8403997, -17.0159, -41.627, -4.8403997, -17.0159, -38.5239, -3.6573, -20.0679, -38.5239, -3.6573, -17.0441, -44.537098, -5.95, -13.378599, -41.627, -4.8403997, -13.378599, -41.627, -4.8403997, -10.033899, -44.537098, -5.95, -13.378599, -41.627, -4.8403997, -10.033899, -44.537098, -5.95, -10.033899, -41.627, -4.8403997, -17.0159, -38.5239, -3.6573, -20.3888, -38.5239, -3.6573, -20.0679, -44.537098, -5.95, -16.9895, -41.627, -4.8403997, -13.378599, -44.537098, -5.95, -13.378599, -44.537098, -5.95, -16.9895, -41.627, -4.8403997, -17.0159, -41.627, -4.8403997, -13.378599, -47.4473, -7.0596, -13.378599, -44.537098, -5.95, -13.378599, -44.537098, -5.95, -10.033899, -47.4473, -7.0596, -16.963, -44.537098, -5.95, -16.9895, -44.537098, -5.95, -13.378599, -47.4473, -7.0596, -16.963, -44.537098, -5.95, -13.378599, -47.4473, -7.0596, -13.378599, -44.537098, -5.95, -20.334099, -41.627, -4.8403997, -17.0159, -44.537098, -5.95, -16.9895, -38.5239, -3.6573, -20.3888, -41.627, -4.8403997, -17.0159, -41.627, -4.8403997, -20.3606, -44.537098, -5.95, -20.334099, -41.627, -4.8403997, -20.3606, -41.627, -4.8403997, -17.0159, -41.627, -4.8403997, -23.412498, -38.5239, -3.6573, -20.3888, -41.627, -4.8403997, -20.3606, -41.627, -4.8403997, -23.412498, -38.5239, -3.6573, -23.412498, -38.5239, -3.6573, -20.3888, -41.627, -4.8403997, -26.7572, -38.5239, -3.6573, -23.412498, -41.627, -4.8403997, -23.412498, -41.627, -4.8403997, -26.7572, -38.5239, -3.6573, -26.7572, -38.5239, -3.6573, -23.412498, -44.537098, -5.95, -26.7572, -41.627, -4.8403997, -26.7572, -41.627, -4.8403997, -23.412498, -44.537098, -5.95, -23.412498, -41.627, -4.8403997, -23.412498, -41.627, -4.8403997, -20.3606, -44.537098, -5.95, -26.7572, -41.627, -4.8403997, -23.412498, -44.537098, -5.95, -23.412498, -44.537098, -5.95, -23.412498, -41.627, -4.8403997, -20.3606, -44.537098, -5.95, -20.334099, -47.4473, -7.0596, -26.7572, -44.537098, -5.95, -26.7572, -44.537098, -5.95, -23.412498, -47.4473, -7.0596, -23.412498, -44.537098, -5.95, -23.412498, -44.537098, -5.95, -20.334099, -47.4473, -7.0596, -26.7572, -44.537098, -5.95, -23.412498, -47.4473, -7.0596, -23.412498, -50.290497, -8.1437, -23.412498, -47.4473, -7.0596, -26.7572, -47.4473, -7.0596, -23.412498, -50.290497, -8.1437, -23.412498, -50.290497, -8.1437, -26.7572, -47.4473, -7.0596, -26.7572, -50.290497, -8.1437, -20.2818, -50.290497, -8.1437, -23.412498, -47.4473, -7.0596, -23.412498, -47.4473, -7.0596, -23.412498, -44.537098, -5.95, -20.334099, -47.4473, -7.0596, -20.3077, -50.290497, -8.1437, -20.2818, -47.4473, -7.0596, -23.412498, -47.4473, -7.0596, -20.3077, -47.4473, -7.0596, -20.3077, -44.537098, -5.95, -20.334099, -44.537098, -5.95, -16.9895, -47.4473, -7.0596, -20.3077, -44.537098, -5.95, -16.9895, -47.4473, -7.0596, -16.963, -47.4473, -7.0596, -16.963, -50.290497, -8.1437, -20.2818, -47.4473, -7.0596, -20.3077, -47.4473, -7.0596, -16.963, -50.290497, -8.1437, -20.0679, -50.290497, -8.1437, -20.2818, -50.290497, -8.1437, -20.0679, -47.4473, -7.0596, -16.963, -50.290497, -8.1437, -16.937199, -47.4473, -7.0596, -13.378599, -50.290497, -8.1437, -16.937199, -47.4473, -7.0596, -16.963, -47.4473, -7.0596, -13.378599, -50.290497, -8.1437, -16.7232, -50.290497, -8.1437, -16.937199, -50.290497, -8.1437, -16.7232, -47.4473, -7.0596, -13.378599, -50.290497, -8.1437, -13.378599, -50.290497, -8.1437, -10.033899, -50.290497, -8.1437, -13.378599, -47.4473, -7.0596, -13.378599, -50.290497, -8.1437, -10.033899, -47.4473, -7.0596, -13.378599, -47.4473, -7.0596, -10.033899, -47.4473, -7.0596, -13.378599, -44.537098, -5.95, -10.033899, -47.4473, -7.0596, -10.033899, -50.290497, -8.1437, -6.6893, -50.290497, -8.1437, -10.033899, -47.4473, -7.0596, -10.033899, -47.4473, -7.0596, -10.033899, -44.537098, -5.95, -10.033899, -44.537098, -5.95, -6.6893, -50.290497, -8.1437, -6.6893, -47.4473, -7.0596, -10.033899, -47.4473, -7.0596, -6.6893, -47.4473, -7.0596, -10.033899, -44.537098, -5.95, -6.6893, -47.4473, -7.0596, -6.6893, -50.290497, -8.1437, -3.3446002, -50.290497, -8.1437, -6.6893, -47.4473, -7.0596, -6.6893, -47.4473, -7.0596, -6.6893, -44.537098, -5.95, -6.6893, -44.537098, -5.95, -3.3446002, -50.290497, -8.1437, -3.3446002, -47.4473, -7.0596, -6.6893, -47.4473, -7.0596, -3.3446002, -47.4473, -7.0596, -6.6893, -44.537098, -5.95, -3.3446002, -47.4473, -7.0596, -3.3446002, -50.290497, -8.1437, 0, -50.290497, -8.1437, -3.3446002, -47.4473, -7.0596, -3.3446002, -47.4473, -7.0596, -3.3446002, -44.537098, -5.95, -3.3446002, -44.537098, -5.95, 0, -50.290497, -8.1437, 0, -47.4473, -7.0596, -3.3446002, -47.4473, -7.0596, 0, -47.4473, -7.0596, -3.3446002, -44.537098, -5.95, 0, -47.4473, -7.0596, 0, -50.290497, -8.1437, 3.3446, -50.290497, -8.1437, 0, -47.4473, -7.0596, 0, -47.4473, -7.0596, 0, -44.537098, -5.95, 0, -44.537098, -5.95, 3.3446, -50.290497, -8.1437, 3.3446, -47.4473, -7.0596, 0, -47.4473, -7.0596, 3.3446, -47.4473, -7.0596, 0, -44.537098, -5.95, 3.3446, -47.4473, -7.0596, 3.3446, -50.290497, -8.1437, 6.6893, -50.290497, -8.1437, 3.3446, -47.4473, -7.0596, 3.3446, -50.290497, -8.1437, 6.6893, -47.4473, -7.0596, 3.3446, -47.4473, -7.0596, 6.6893, -47.4473, -7.0596, 6.6893, -50.290497, -8.1437, 10.033899, -50.290497, -8.1437, 6.6893, -47.4473, -7.0596, 3.3446, -44.537098, -5.95, 3.3446, -44.537098, -5.95, 6.6893, -47.4473, -7.0596, 3.3446, -44.537098, -5.95, 6.6893, -47.4473, -7.0596, 6.6893, -44.537098, -5.95, 3.3446, -41.627, -4.8403997, 6.6893, -44.537098, -5.95, 6.6893, -47.4473, -7.0596, 6.6893, -50.290497, -8.1437, 13.378601, -50.290497, -8.1437, 10.033899, -41.627, -4.8403997, 13.378601, -44.537098, -5.95, 6.6893, -41.627, -4.8403997, 6.6893, -44.537098, -5.95, 13.378601, -47.4473, -7.0596, 6.6893, -44.537098, -5.95, 6.6893, -41.627, -4.8403997, 13.378601, -44.537098, -5.95, 13.378601, -44.537098, -5.95, 6.6893, -44.537098, -5.95, 13.378601, -47.4473, -7.0596, 13.378601, -47.4473, -7.0596, 6.6893, -50.290497, -8.1437, 13.378601, -47.4473, -7.0596, 6.6893, -47.4473, -7.0596, 13.378601, -41.627, -4.8403997, 16.7232, -44.537098, -5.95, 13.378601, -41.627, -4.8403997, 13.378601, -38.5239, -3.6573, 13.378601, -41.627, -4.8403997, 16.7232, -41.627, -4.8403997, 13.378601, -38.5239, -3.6573, 13.378601, -38.5239, -3.6573, 16.7232, -41.627, -4.8403997, 16.7232, -41.627, -4.8403997, 16.7232, -44.537098, -5.95, 16.7232, -44.537098, -5.95, 13.378601, -44.537098, -5.95, 16.7232, -47.4473, -7.0596, 13.378601, -44.537098, -5.95, 13.378601, -38.5239, -3.6573, 16.7232, -41.627, -4.8403997, 20.0679, -41.627, -4.8403997, 16.7232, -41.627, -4.8403997, 20.0679, -44.537098, -5.95, 16.7232, -41.627, -4.8403997, 16.7232, -38.5239, -3.6573, 16.7232, -38.5239, -3.6573, 20.0679, -41.627, -4.8403997, 20.0679, -47.4473, -7.0596, 16.7232, -50.290497, -8.1437, 13.378601, -47.4473, -7.0596, 13.378601, -44.537098, -5.95, 16.7232, -47.4473, -7.0596, 16.7232, -47.4473, -7.0596, 13.378601, -47.4473, -7.0596, 16.7232, -50.290497, -8.1437, 16.7232, -50.290497, -8.1437, 13.378601, -41.627, -4.8403997, 20.0679, -44.537098, -5.95, 20.0679, -44.537098, -5.95, 16.7232, -44.537098, -5.95, 20.0679, -47.4473, -7.0596, 16.7232, -44.537098, -5.95, 16.7232, -38.5239, -3.6573, 20.0679, -41.627, -4.8403997, 23.412498, -41.627, -4.8403997, 20.0679, -41.627, -4.8403997, 23.412498, -44.537098, -5.95, 20.0679, -41.627, -4.8403997, 20.0679, -38.5239, -3.6573, 20.0679, -38.5239, -3.6573, 23.412498, -41.627, -4.8403997, 23.412498, -38.5239, -3.6573, 23.412498, -41.627, -4.8403997, 26.7572, -41.627, -4.8403997, 23.412498, -38.5239, -3.6573, 23.412498, -38.5239, -3.6573, 26.7572, -41.627, -4.8403997, 26.7572, -41.627, -4.8403997, 26.7572, -44.537098, -5.95, 23.412498, -41.627, -4.8403997, 23.412498, -41.627, -4.8403997, 23.412498, -44.537098, -5.95, 23.412498, -44.537098, -5.95, 20.0679, -41.627, -4.8403997, 26.7572, -44.537098, -5.95, 26.7572, -44.537098, -5.95, 23.412498, -44.537098, -5.95, 20.0679, -47.4473, -7.0596, 20.0679, -47.4473, -7.0596, 16.7232, -47.4473, -7.0596, 20.0679, -50.290497, -8.1437, 16.7232, -47.4473, -7.0596, 16.7232, -44.537098, -5.95, 23.412498, -47.4473, -7.0596, 20.0679, -44.537098, -5.95, 20.0679, -47.4473, -7.0596, 20.0679, -50.290497, -8.1437, 20.0679, -50.290497, -8.1437, 16.7232, -44.537098, -5.95, 26.7572, -47.4473, -7.0596, 23.412498, -44.537098, -5.95, 23.412498, -44.537098, -5.95, 23.412498, -47.4473, -7.0596, 23.412498, -47.4473, -7.0596, 20.0679, -47.4473, -7.0596, 23.412498, -50.290497, -8.1437, 20.0679, -47.4473, -7.0596, 20.0679, -44.537098, -5.95, 26.7572, -47.4473, -7.0596, 26.7572, -47.4473, -7.0596, 23.412498, -47.4473, -7.0596, 23.412498, -50.290497, -8.1437, 23.412498, -50.290497, -8.1437, 20.0679, -47.4473, -7.0596, 26.7572, -50.290497, -8.1437, 23.412498, -47.4473, -7.0596, 23.412498, -47.4473, -7.0596, 26.7572, -50.290497, -8.1437, 26.7572, -50.290497, -8.1437, 23.412498, -6.6893, -8.1437, -54.500298, -3.4038, -8.6382, -57.495697, -3.4199996, -8.1437, -54.500298, -3.4038, -8.6382, -57.495697, -3.3446002, -8.1437, -54.500298, -3.4199996, -8.1437, -54.500298, -6.6893, -8.1437, -54.500298, -6.7919, -8.1437, -54.500298, -3.4038, -8.6382, -57.495697, -3.4038, -8.6382, -57.495697, -0.048199892, -8.1437, -54.500298, -3.3446002, -8.1437, -54.500298, -3.4038, -8.6382, -57.495697, -6.7919, -8.1437, -54.500298, -6.7697997, -8.6367, -57.4865, -10.033899, -8.1437, -54.500298, -6.7697997, -8.6367, -57.4865, -6.7919, -8.1437, -54.500298, -0.048199892, -8.1437, -54.500298, -3.4038, -8.6382, -57.495697, -0.037699938, -8.6397, -57.504997, -6.7697997, -8.6367, -57.4865, -10.033899, -8.1437, -54.500298, -10.141901, -8.1437, -54.500298, -0.037699938, -8.6397, -57.504997, 0, -8.1437, -54.500298, -0.048199892, -8.1437, -54.500298, -6.7697997, -8.6367, -57.4865, -10.141901, -8.1437, -54.500298, -10.1187, -8.635099, -57.4772, -13.378601, -8.1437, -54.500298, -10.1187, -8.635099, -57.4772, -10.141901, -8.1437, -54.500298, -0.037699938, -8.6397, -57.504997, 3.3446, -8.1437, -54.500298, 0, -8.1437, -54.500298, -10.1187, -8.635099, -57.4772, -13.378601, -8.1437, -54.500298, -13.4286, -8.1437, -54.500298, 3.3446, -8.1437, -54.500298, -0.037699938, -8.6397, -57.504997, 3.3446, -8.6413, -57.514297, -10.1187, -8.635099, -57.4772, -6.749, -9.1004, -60.295498, -6.7697997, -8.6367, -57.4865, 6.6893, -8.642799, -57.5235, 3.3446, -8.1437, -54.500298, 3.3446, -8.6413, -57.514297, 6.6893, -8.642799, -57.5235, 6.6893, -8.1437, -54.500298, 3.3446, -8.1437, -54.500298, 3.3446, -9.0949, -60.262497, 3.3446, -8.6413, -57.514297, -0.037699938, -8.6397, -57.504997, 10.033899, -8.6442995, -57.5327, 6.6893, -8.1437, -54.500298, 6.6893, -8.642799, -57.5235, 10.033899, -8.6442995, -57.5327, 10.033899, -8.1437, -54.500298, 6.6893, -8.1437, -54.500298, 10.033899, -8.6442995, -57.5327, 13.378599, -8.1437, -54.500298, 10.033899, -8.1437, -54.500298, 10.033899, -8.6442995, -57.5327, 13.444599, -8.1437, -54.500298, 13.378599, -8.1437, -54.500298, 13.444599, -8.1437, -54.500298, 10.033899, -8.6442995, -57.5327, 13.4300995, -8.6458, -57.5421, 13.4300995, -8.6458, -57.5421, 16.7232, -8.1437, -54.500298, 13.444599, -8.1437, -54.500298, 13.4300995, -8.6458, -57.5421, 16.8188, -8.1437, -54.500298, 16.7232, -8.1437, -54.500298, 16.8188, -8.1437, -54.500298, 13.4300995, -8.6458, -57.5421, 16.7978, -8.6474, -57.5514, 16.7978, -8.6474, -57.5514, 20.0679, -8.1437, -54.500298, 16.8188, -8.1437, -54.500298, 16.7978, -8.6474, -57.5514, 20.1975, -8.1437, -54.500298, 20.0679, -8.1437, -54.500298, 20.1975, -8.1437, -54.500298, 16.7978, -8.6474, -57.5514, 20.1689, -8.6489, -57.5607, 20.1442, -9.085899, -60.207497, 20.1689, -8.6489, -57.5607, 16.7978, -8.6474, -57.5514, 20.1442, -9.085899, -60.207497, 16.7978, -8.6474, -57.5514, 16.7794, -9.0877, -60.2185, 16.7794, -9.0877, -60.2185, 16.7978, -8.6474, -57.5514, 13.4300995, -8.6458, -57.5421, 20.112099, -9.6529, -63.642, 20.1442, -9.085899, -60.207497, 16.7794, -9.0877, -60.2185, 16.7794, -9.0877, -60.2185, 13.4300995, -8.6458, -57.5421, 13.417299, -9.089499, -60.2295, 13.417299, -9.089499, -60.2295, 13.4300995, -8.6458, -57.5421, 10.033899, -8.6442995, -57.5327, 20.112099, -9.6529, -63.642, 16.7794, -9.0877, -60.2185, 16.7557, -9.6571, -63.6679, 16.7557, -9.6571, -63.6679, 16.7794, -9.0877, -60.2185, 13.417299, -9.089499, -60.2295, 16.7232, -10.4337, -68.371895, 20.112099, -9.6529, -63.642, 16.7557, -9.6571, -63.6679, 16.7232, -10.4337, -68.371895, 20.0679, -10.4337, -68.371895, 20.112099, -9.6529, -63.642, 13.378599, -10.4337, -68.371895, 16.7232, -10.4337, -68.371895, 16.7557, -9.6571, -63.6679, 13.378599, -10.4337, -68.371895, 16.7557, -9.6571, -63.6679, 13.400799, -9.6614, -63.693794, 16.7557, -9.6571, -63.6679, 13.417299, -9.089499, -60.2295, 13.400799, -9.6614, -63.693794, 10.033899, -10.4337, -68.371895, 13.378599, -10.4337, -68.371895, 13.400799, -9.6614, -63.693794, 13.417299, -9.089499, -60.2295, 10.033899, -8.6442995, -57.5327, 10.033899, -9.0913, -60.240597, 13.400799, -9.6614, -63.693794, 13.417299, -9.089499, -60.2295, 10.033899, -9.0913, -60.240597, 10.033899, -9.0913, -60.240597, 10.033899, -8.6442995, -57.5327, 6.6893, -8.642799, -57.5235, 10.033899, -10.4337, -68.371895, 13.400799, -9.6614, -63.693794, 10.033899, -9.6657, -63.719696, 13.400799, -9.6614, -63.693794, 10.033899, -9.0913, -60.240597, 10.033899, -9.6657, -63.719696, 6.6893, -10.4337, -68.371895, 10.033899, -10.4337, -68.371895, 10.033899, -9.6657, -63.719696, 10.033899, -9.0913, -60.240597, 6.6893, -8.642799, -57.5235, 6.6893, -9.0931, -60.2515, 10.033899, -9.6657, -63.719696, 10.033899, -9.0913, -60.240597, 6.6893, -9.0931, -60.2515, 6.6893, -9.0931, -60.2515, 6.6893, -8.642799, -57.5235, 3.3446, -8.6413, -57.514297, 6.6893, -10.4337, -68.371895, 10.033899, -9.6657, -63.719696, 6.6893, -9.6699, -63.7455, 10.033899, -9.6657, -63.719696, 6.6893, -9.0931, -60.2515, 6.6893, -9.6699, -63.7455, 3.3446, -10.4337, -68.371895, 6.6893, -10.4337, -68.371895, 6.6893, -9.6699, -63.7455, 6.6893, -9.0931, -60.2515, 3.3446, -8.6413, -57.514297, 3.3446, -9.0949, -60.262497, 6.6893, -9.6699, -63.7455, 6.6893, -9.0931, -60.2515, 3.3446, -9.0949, -60.262497, 3.3446, -10.4337, -68.371895, 6.6893, -9.6699, -63.7455, 3.3446, -9.6742, -63.771194, 6.6893, -9.6699, -63.7455, 3.3446, -9.0949, -60.262497, 3.3446, -9.6742, -63.771194, 0, -10.4337, -68.371895, 3.3446, -10.4337, -68.371895, 3.3446, -9.6742, -63.771194, 3.3446, -9.6742, -63.771194, 3.3446, -9.0949, -60.262497, -0.028100014, -9.0968, -60.2735, 3.3446, -9.0949, -60.262497, -0.037699938, -8.6397, -57.504997, -0.028100014, -9.0968, -60.2735, -0.028100014, -9.0968, -60.2735, -0.037699938, -8.6397, -57.504997, -3.4038, -8.6382, -57.495697, 0, -10.4337, -68.371895, 3.3446, -9.6742, -63.771194, -0.015899897, -9.6785, -63.797096, 3.3446, -9.6742, -63.771194, -0.028100014, -9.0968, -60.2735, -0.015899897, -9.6785, -63.797096, -3.3446002, -10.4337, -68.371895, 0, -10.4337, -68.371895, -0.015899897, -9.6785, -63.797096, -0.028100014, -9.0968, -60.2735, -3.4038, -8.6382, -57.495697, -3.3885999, -9.098599, -60.2845, -0.015899897, -9.6785, -63.797096, -0.028100014, -9.0968, -60.2735, -3.3885999, -9.098599, -60.2845, -6.7697997, -8.6367, -57.4865, -3.3885999, -9.098599, -60.2845, -3.4038, -8.6382, -57.495697, -6.7697997, -8.6367, -57.4865, -6.749, -9.1004, -60.295498, -3.3885999, -9.098599, -60.2845, -0.015899897, -9.6785, -63.797096, -3.3885999, -9.098599, -60.2845, -3.3694, -9.6827, -63.8229, -3.3446002, -10.4337, -68.371895, -0.015899897, -9.6785, -63.797096, -3.3694, -9.6827, -63.8229, -6.749, -9.1004, -60.295498, -3.3694, -9.6827, -63.8229, -3.3885999, -9.098599, -60.2845, -6.7226996, -9.686999, -63.848793, -3.3446002, -10.4337, -68.371895, -3.3694, -9.6827, -63.8229, -6.749, -9.1004, -60.295498, -6.7226996, -9.686999, -63.848793, -3.3694, -9.6827, -63.8229, -6.7226996, -9.686999, -63.848793, -6.6893, -10.4337, -68.371895, -3.3446002, -10.4337, -68.371895, -10.096701, -9.1022, -60.3065, -6.7226996, -9.686999, -63.848793, -6.749, -9.1004, -60.295498, -10.068901, -9.691199, -63.874596, -6.6893, -10.4337, -68.371895, -6.7226996, -9.686999, -63.848793, -10.096701, -9.1022, -60.3065, -10.068901, -9.691199, -63.874596, -6.7226996, -9.686999, -63.848793, -10.068901, -9.691199, -63.874596, -10.033899, -10.4337, -68.371895, -6.6893, -10.4337, -68.371895, -10.1187, -8.635099, -57.4772, -10.096701, -9.1022, -60.3065, -6.749, -9.1004, -60.295498, -13.394701, -9.695499, -63.9002, -10.033899, -10.4337, -68.371895, -10.068901, -9.691199, -63.874596, -13.394701, -9.695499, -63.9002, -13.378601, -10.4337, -68.371895, -10.033899, -10.4337, -68.371895, -13.4076, -9.104, -60.3173, -10.068901, -9.691199, -63.874596, -10.096701, -9.1022, -60.3065, -13.4076, -9.104, -60.3173, -13.394701, -9.695499, -63.9002, -10.068901, -9.691199, -63.874596, -13.4179, -8.6336, -57.468098, -10.096701, -9.1022, -60.3065, -10.1187, -8.635099, -57.4772, -13.4179, -8.6336, -57.468098, -13.4076, -9.104, -60.3173, -10.096701, -9.1022, -60.3065, -10.1187, -8.635099, -57.4772, -13.4286, -8.1437, -54.500298, -13.4179, -8.6336, -57.468098, -13.4286, -8.1437, -54.500298, -16.7232, -8.6321, -57.459, -13.4179, -8.6336, -57.468098, -16.7232, -8.6321, -57.459, -13.4076, -9.104, -60.3173, -13.4179, -8.6336, -57.468098, -13.4286, -8.1437, -54.500298, -16.7232, -8.1437, -54.500298, -16.7232, -8.6321, -57.459, -16.7232, -9.1058, -60.3281, -13.394701, -9.695499, -63.9002, -13.4076, -9.104, -60.3173, -16.7232, -8.6321, -57.459, -16.7232, -9.1058, -60.3281, -13.4076, -9.104, -60.3173, -16.7232, -9.699699, -63.925797, -13.378601, -10.4337, -68.371895, -13.394701, -9.695499, -63.9002, -16.7232, -9.1058, -60.3281, -16.7232, -9.699699, -63.925797, -13.394701, -9.695499, -63.9002, -16.7232, -9.699699, -63.925797, -16.7232, -10.4337, -68.371895, -13.378601, -10.4337, -68.371895, -16.7232, -8.1437, -54.500298, -20.0679, -8.6306, -57.4498, -16.7232, -8.6321, -57.459, -20.0679, -8.6306, -57.4498, -16.7232, -9.1058, -60.3281, -16.7232, -8.6321, -57.459, -16.7232, -8.1437, -54.500298, -20.0679, -8.1437, -54.500298, -20.0679, -8.6306, -57.4498, -20.0679, -9.1076, -60.3391, -16.7232, -9.699699, -63.925797, -16.7232, -9.1058, -60.3281, -20.0679, -8.6306, -57.4498, -20.0679, -9.1076, -60.3391, -16.7232, -9.1058, -60.3281, -20.0679, -9.7039995, -63.9516, -16.7232, -10.4337, -68.371895, -16.7232, -9.699699, -63.925797, -20.0679, -9.1076, -60.3391, -20.0679, -9.7039995, -63.9516, -16.7232, -9.699699, -63.925797, -20.0679, -9.7039995, -63.9516, -20.0679, -10.4337, -68.371895, -16.7232, -10.4337, -68.371895, -20.0679, -8.1437, -54.500298, -23.412498, -8.6291, -57.440598, -20.0679, -8.6306, -57.4498, -23.412498, -8.6291, -57.440598, -20.0679, -9.1076, -60.3391, -20.0679, -8.6306, -57.4498, -20.0679, -8.1437, -54.500298, -23.412498, -8.1437, -54.500298, -23.412498, -8.6291, -57.440598, -23.412498, -8.1437, -54.500298, -26.7572, -8.6276, -57.4314, -23.412498, -8.6291, -57.440598, -23.412498, -8.1437, -54.500298, -26.7572, -8.1437, -54.500298, -26.7572, -8.6276, -57.4314, -23.412498, -8.6291, -57.440598, -23.412498, -9.1094, -60.35, -20.0679, -9.1076, -60.3391, -26.7572, -8.6276, -57.4314, -23.412498, -9.1094, -60.35, -23.412498, -8.6291, -57.440598, -23.412498, -9.1094, -60.35, -20.0679, -9.7039995, -63.9516, -20.0679, -9.1076, -60.3391, -26.7572, -8.6276, -57.4314, -26.7572, -9.111199, -60.361, -23.412498, -9.1094, -60.35, -23.412498, -9.7081995, -63.977394, -20.0679, -10.4337, -68.371895, -20.0679, -9.7039995, -63.9516, -23.412498, -9.1094, -60.35, -23.412498, -9.7081995, -63.977394, -20.0679, -9.7039995, -63.9516, -26.7572, -9.111199, -60.361, -23.412498, -9.7081995, -63.977394, -23.412498, -9.1094, -60.35, -23.412498, -9.7081995, -63.977394, -23.412498, -10.4337, -68.371895, -20.0679, -10.4337, -68.371895, -26.7572, -9.111199, -60.361, -26.7572, -9.7125, -64.0031, -23.412498, -9.7081995, -63.977394, -26.7572, -9.7125, -64.0031, -23.412498, -10.4337, -68.371895, -23.412498, -9.7081995, -63.977394, -26.7572, -9.7125, -64.0031, -26.7572, -10.4337, -68.371895, -23.412498, -10.4337, -68.371895, -3.3446002, -10.4337, -68.371895, -0.0020999908, -10.5633, -70.6314, 0, -10.4337, -68.371895, -3.3446002, -10.4337, -68.371895, -3.3514, -10.5471, -70.669, -0.0020999908, -10.5633, -70.6314, 23.412498, 0, -3.3446002, 26.7577, -0.6956, 0, 23.412498, 0, 0, 23.412498, 0, -3.3446002, 26.7577, -0.4894, -3.3446002, 26.7577, -0.6956, 0, 23.412498, 0, -6.6893, 26.7577, -0.4894, -3.3446002, 23.412498, 0, -3.3446002, 23.412498, 0, -6.6893, 26.7577, -0.3464, -6.6893, 26.7577, -0.4894, -3.3446002, 23.412498, -1.0924, -23.412498, 26.7582, -1.0924, -20.0679, 23.412498, -1.0924, -20.0679, 23.412498, -1.0924, -23.412498, 26.7582, -1.0924, -23.412498, 26.7582, -1.0924, -20.0679, 23.412498, 0, -10.033899, 26.7577, -0.3464, -6.6893, 23.412498, 0, -6.6893, 23.412498, 0, -10.033899, 26.572199, -0.4003, -10.314899, 26.7577, -0.3464, -6.6893, 23.412498, -0.35529998, 10.033899, 26.7582, -1.7623999, 10.033899, 23.412498, -1.8294, 13.378601, 23.412498, -0.456, -16.7232, 26.7577, -0.8804, -16.7232, 26.572199, -0.95549995, -13.659599, 23.412498, -1.1158999, -26.7572, 26.7582, -1.0924, -23.412498, 23.412498, -1.0924, -23.412498, 23.412498, -1.1158999, -26.7572, 26.7573, -1.3222, -26.7572, 26.7582, -1.0924, -23.412498, 23.412498, -2.2719998, 23.3902, 26.7582, -2.7837, 26.6955, 23.412498, -2.2194998, 26.781, 23.412498, -2.2719998, 23.3902, 26.7582, -2.2719998, 23.3902, 26.7582, -2.7837, 26.6955, 23.412498, 0, -13.378599, 26.572199, -0.4003, -10.314899, 23.412498, 0, -10.033899, 23.412498, 0, -13.378599, 26.572199, -0.95549995, -13.659599, 26.572199, -0.4003, -10.314899, 23.412498, 0, 0, 26.7577, -0.6956, 0, 26.7577, -0.73109996, 3.3446, 23.412498, -1.0924, -20.0679, 26.7582, -1.0924, -20.0679, 26.7577, -0.8804, -16.7232, 52.997097, -8.1437, -6.6893, 56.213997, -8.7052, -3.3446002, 52.997097, -8.1437, -3.3446002, 56.173397, -8.6981, 0, 52.997097, -8.1437, -3.3446002, 56.213997, -8.7052, -3.3446002, 56.173397, -8.6981, 0, 52.997097, -8.1437, 0, 52.997097, -8.1437, -3.3446002, 52.997097, -8.1437, -6.6893, 56.254498, -8.712299, -6.6893, 56.213997, -8.7052, -3.3446002, 56.254498, -8.712299, -6.6893, 52.997097, -8.1437, -6.6893, 52.997097, -8.1437, -10.033899, 59.1829, -9.2234, 0, 56.173397, -8.6981, 0, 56.213997, -8.7052, -3.3446002, 56.254498, -8.712299, -6.6893, 59.216, -9.229199, -3.3446002, 56.213997, -8.7052, -3.3446002, 59.1829, -9.2234, 0, 56.213997, -8.7052, -3.3446002, 59.216, -9.229199, -3.3446002, 56.1329, -8.691, 3.3446, 52.997097, -8.1437, 0, 56.173397, -8.6981, 0, 56.1329, -8.691, 3.3446, 52.997097, -8.1437, 3.3446, 52.997097, -8.1437, 0, 59.1498, -9.2176, 3.3446, 56.173397, -8.6981, 0, 59.1829, -9.2234, 0, 59.1498, -9.2176, 3.3446, 56.1329, -8.691, 3.3446, 56.173397, -8.6981, 0, 56.0924, -8.684, 6.6893, 52.997097, -8.1437, 3.3446, 56.1329, -8.691, 3.3446, 56.0924, -8.684, 6.6893, 52.997097, -8.1437, 6.6893, 52.997097, -8.1437, 3.3446, 59.1167, -9.2119, 6.6893, 56.1329, -8.691, 3.3446, 59.1498, -9.2176, 3.3446, 59.1167, -9.2119, 6.6893, 56.0924, -8.684, 6.6893, 56.1329, -8.691, 3.3446, 56.0518, -8.6769, 10.033899, 52.997097, -8.1437, 6.6893, 56.0924, -8.684, 6.6893, 56.0518, -8.6769, 10.033899, 52.997097, -8.1437, 10.033899, 52.997097, -8.1437, 6.6893, 59.0836, -9.2060995, 10.033899, 56.0924, -8.684, 6.6893, 59.1167, -9.2119, 6.6893, 59.0836, -9.2060995, 10.033899, 56.0518, -8.6769, 10.033899, 56.0924, -8.684, 6.6893, 56.0113, -8.6698, 13.378601, 52.997097, -8.1437, 10.033899, 56.0518, -8.6769, 10.033899, 56.0113, -8.6698, 13.378601, 52.997097, -8.1437, 13.378601, 52.997097, -8.1437, 10.033899, 59.0505, -9.2003, 13.378601, 56.0518, -8.6769, 10.033899, 59.0836, -9.2060995, 10.033899, 59.0505, -9.2003, 13.378601, 56.0113, -8.6698, 13.378601, 56.0518, -8.6769, 10.033899, 55.9708, -8.6627, 16.7232, 52.997097, -8.1437, 13.378601, 56.0113, -8.6698, 13.378601, 55.9708, -8.6627, 16.7232, 52.997097, -8.1437, 16.7232, 52.997097, -8.1437, 13.378601, 59.017498, -9.1945, 16.7232, 56.0113, -8.6698, 13.378601, 59.0505, -9.2003, 13.378601, 59.017498, -9.1945, 16.7232, 55.9708, -8.6627, 16.7232, 56.0113, -8.6698, 13.378601, 55.930298, -8.6557, 20.0679, 52.997097, -8.1437, 16.7232, 55.9708, -8.6627, 16.7232, 55.930298, -8.6557, 20.0679, 52.997097, -8.1437, 20.0679, 52.997097, -8.1437, 16.7232, 58.984398, -9.1888, 20.0679, 55.9708, -8.6627, 16.7232, 59.017498, -9.1945, 16.7232, 58.984398, -9.1888, 20.0679, 55.930298, -8.6557, 20.0679, 55.9708, -8.6627, 16.7232, 55.889698, -8.6486, 23.412498, 52.997097, -8.1437, 20.0679, 55.930298, -8.6557, 20.0679, 55.889698, -8.6486, 23.412498, 52.997097, -8.1437, 23.412498, 52.997097, -8.1437, 20.0679, 55.849197, -8.6414995, 26.7572, 52.997097, -8.1437, 23.412498, 55.889698, -8.6486, 23.412498, 55.849197, -8.6414995, 26.7572, 52.997097, -8.1437, 26.7572, 52.997097, -8.1437, 23.412498, 58.918198, -9.177199, 26.7572, 55.849197, -8.6414995, 26.7572, 55.889698, -8.6486, 23.412498, 58.951298, -9.183, 23.412498, 55.889698, -8.6486, 23.412498, 55.930298, -8.6557, 20.0679, 58.918198, -9.177199, 26.7572, 55.889698, -8.6486, 23.412498, 58.951298, -9.183, 23.412498, 58.951298, -9.183, 23.412498, 55.930298, -8.6557, 20.0679, 58.984398, -9.1888, 20.0679, 62.4636, -9.7961, 26.7572, 58.918198, -9.177199, 26.7572, 58.951298, -9.183, 23.412498, 62.4577, -9.795, 23.412498, 58.951298, -9.183, 23.412498, 58.984398, -9.1888, 20.0679, 62.4636, -9.7961, 26.7572, 58.951298, -9.183, 23.412498, 62.4577, -9.795, 23.412498, 66.1166, -10.4337, 23.412498, 62.4636, -9.7961, 26.7572, 62.4577, -9.795, 23.412498, 66.1166, -10.4337, 23.412498, 66.1166, -10.4337, 26.7572, 62.4636, -9.7961, 26.7572, 66.1166, -10.4337, 20.0679, 66.1166, -10.4337, 23.412498, 62.4577, -9.795, 23.412498, 62.4577, -9.795, 23.412498, 58.984398, -9.1888, 20.0679, 62.451897, -9.794, 20.0679, 66.1166, -10.4337, 20.0679, 62.4577, -9.795, 23.412498, 62.451897, -9.794, 20.0679, 62.451897, -9.794, 20.0679, 58.984398, -9.1888, 20.0679, 59.017498, -9.1945, 16.7232, 66.1166, -10.4337, 16.7232, 66.1166, -10.4337, 20.0679, 62.451897, -9.794, 20.0679, 62.451897, -9.794, 20.0679, 59.017498, -9.1945, 16.7232, 62.446, -9.793, 16.7232, 66.1166, -10.4337, 16.7232, 62.451897, -9.794, 20.0679, 62.446, -9.793, 16.7232, 62.446, -9.793, 16.7232, 59.017498, -9.1945, 16.7232, 59.0505, -9.2003, 13.378601, 66.1166, -10.4337, 13.378601, 66.1166, -10.4337, 16.7232, 62.446, -9.793, 16.7232, 62.446, -9.793, 16.7232, 59.0505, -9.2003, 13.378601, 62.440098, -9.792, 13.378601, 66.1166, -10.4337, 13.378601, 62.446, -9.793, 16.7232, 62.440098, -9.792, 13.378601, 62.440098, -9.792, 13.378601, 59.0505, -9.2003, 13.378601, 59.0836, -9.2060995, 10.033899, 66.1166, -10.4337, 10.033899, 66.1166, -10.4337, 13.378601, 62.440098, -9.792, 13.378601, 62.440098, -9.792, 13.378601, 59.0836, -9.2060995, 10.033899, 62.4343, -9.790899, 10.033899, 66.1166, -10.4337, 10.033899, 62.440098, -9.792, 13.378601, 62.4343, -9.790899, 10.033899, 62.4343, -9.790899, 10.033899, 59.0836, -9.2060995, 10.033899, 59.1167, -9.2119, 6.6893, 66.1166, -10.4337, 6.6893, 66.1166, -10.4337, 10.033899, 62.4343, -9.790899, 10.033899, 62.4343, -9.790899, 10.033899, 59.1167, -9.2119, 6.6893, 62.4284, -9.7899, 6.6893, 66.1166, -10.4337, 6.6893, 62.4343, -9.790899, 10.033899, 62.4284, -9.7899, 6.6893, 62.4284, -9.7899, 6.6893, 59.1167, -9.2119, 6.6893, 59.1498, -9.2176, 3.3446, 66.1166, -10.4337, 3.3446, 66.1166, -10.4337, 6.6893, 62.4284, -9.7899, 6.6893, 62.4284, -9.7899, 6.6893, 59.1498, -9.2176, 3.3446, 62.422497, -9.788899, 3.3446, 66.1166, -10.4337, 3.3446, 62.4284, -9.7899, 6.6893, 62.422497, -9.788899, 3.3446, 62.422497, -9.788899, 3.3446, 59.1498, -9.2176, 3.3446, 59.1829, -9.2234, 0, 66.1166, -10.4337, 0, 66.1166, -10.4337, 3.3446, 62.422497, -9.788899, 3.3446, 62.422497, -9.788899, 3.3446, 59.1829, -9.2234, 0, 62.4167, -9.7879, 0, 66.1166, -10.4337, 0, 62.422497, -9.788899, 3.3446, 62.4167, -9.7879, 0, 62.4167, -9.7879, 0, 59.1829, -9.2234, 0, 59.216, -9.229199, -3.3446002, 66.1166, -10.4337, -3.3446002, 66.1166, -10.4337, 0, 62.4167, -9.7879, 0, 62.4167, -9.7879, 0, 59.216, -9.229199, -3.3446002, 62.410797, -9.786799, -3.3446002, 66.1166, -10.4337, -3.3446002, 62.4167, -9.7879, 0, 62.410797, -9.786799, -3.3446002, 59.248997, -9.235, -6.6893, 62.410797, -9.786799, -3.3446002, 59.216, -9.229199, -3.3446002, 62.4049, -9.7858, -6.6893, 66.1166, -10.4337, -3.3446002, 62.410797, -9.786799, -3.3446002, 59.248997, -9.235, -6.6893, 62.4049, -9.7858, -6.6893, 62.410797, -9.786799, -3.3446002, 62.4049, -9.7858, -6.6893, 66.1166, -10.4337, -6.6893, 66.1166, -10.4337, -3.3446002, 56.254498, -8.712299, -6.6893, 59.248997, -9.235, -6.6893, 59.216, -9.229199, -3.3446002, 66.1166, -10.4337, -6.6893, 62.4049, -9.7858, -6.6893, 62.399, -9.7848, -10.033899, 66.1166, -10.4337, -6.6893, 62.399, -9.7848, -10.033899, 66.1166, -10.4337, -10.033899, 62.4049, -9.7858, -6.6893, 59.248997, -9.235, -6.6893, 59.282097, -9.2407, -10.033899, 62.4049, -9.7858, -6.6893, 59.282097, -9.2407, -10.033899, 62.399, -9.7848, -10.033899, 59.248997, -9.235, -6.6893, 56.254498, -8.712299, -6.6893, 56.295, -8.719299, -10.033899, 59.248997, -9.235, -6.6893, 56.295, -8.719299, -10.033899, 59.282097, -9.2407, -10.033899, 56.254498, -8.712299, -6.6893, 52.997097, -8.1437, -10.033899, 56.295, -8.719299, -10.033899, 52.997097, -8.1437, -10.033899, 56.3355, -8.726399, -13.378599, 56.295, -8.719299, -10.033899, 56.3355, -8.726399, -13.378599, 59.282097, -9.2407, -10.033899, 56.295, -8.719299, -10.033899, 52.997097, -8.1437, -10.033899, 52.997097, -8.1437, -13.378599, 56.3355, -8.726399, -13.378599, 59.315197, -9.2465, -13.378599, 62.399, -9.7848, -10.033899, 59.282097, -9.2407, -10.033899, 56.3355, -8.726399, -13.378599, 59.315197, -9.2465, -13.378599, 59.282097, -9.2407, -10.033899, 52.997097, -8.1437, -13.378599, 56.3761, -8.7335, -16.7232, 56.3355, -8.726399, -13.378599, 56.3761, -8.7335, -16.7232, 59.315197, -9.2465, -13.378599, 56.3355, -8.726399, -13.378599, 52.997097, -8.1437, -13.378599, 52.997097, -8.1437, -16.7232, 56.3761, -8.7335, -16.7232, 59.315197, -9.2465, -13.378599, 62.3932, -9.7838, -13.378599, 62.399, -9.7848, -10.033899, 62.3932, -9.7838, -13.378599, 66.1166, -10.4337, -10.033899, 62.399, -9.7848, -10.033899, 62.3932, -9.7838, -13.378599, 66.1166, -10.4337, -13.378599, 66.1166, -10.4337, -10.033899, 59.348297, -9.252299, -16.7232, 62.3932, -9.7838, -13.378599, 59.315197, -9.2465, -13.378599, 56.3761, -8.7335, -16.7232, 59.348297, -9.252299, -16.7232, 59.315197, -9.2465, -13.378599, 62.3873, -9.7827, -16.7232, 66.1166, -10.4337, -13.378599, 62.3932, -9.7838, -13.378599, 59.348297, -9.252299, -16.7232, 62.3873, -9.7827, -16.7232, 62.3932, -9.7838, -13.378599, 62.3873, -9.7827, -16.7232, 66.1166, -10.4337, -16.7232, 66.1166, -10.4337, -13.378599, 56.4166, -8.7406, -20.0679, 59.348297, -9.252299, -16.7232, 56.3761, -8.7335, -16.7232, 52.997097, -8.1437, -16.7232, 56.4166, -8.7406, -20.0679, 56.3761, -8.7335, -16.7232, 52.997097, -8.1437, -16.7232, 52.997097, -8.1437, -20.0679, 56.4166, -8.7406, -20.0679, 52.997097, -8.1437, -20.0679, 56.4571, -8.7476, -23.412498, 56.4166, -8.7406, -20.0679, 52.997097, -8.1437, -20.0679, 52.997097, -8.1437, -23.412498, 56.4571, -8.7476, -23.412498, 56.4166, -8.7406, -20.0679, 59.381397, -9.2581, -20.0679, 59.348297, -9.252299, -16.7232, 56.4571, -8.7476, -23.412498, 59.381397, -9.2581, -20.0679, 56.4166, -8.7406, -20.0679, 59.381397, -9.2581, -20.0679, 62.3873, -9.7827, -16.7232, 59.348297, -9.252299, -16.7232, 56.4571, -8.7476, -23.412498, 59.414497, -9.2638, -23.412498, 59.381397, -9.2581, -20.0679, 62.381397, -9.7817, -20.0679, 66.1166, -10.4337, -16.7232, 62.3873, -9.7827, -16.7232, 59.381397, -9.2581, -20.0679, 62.381397, -9.7817, -20.0679, 62.3873, -9.7827, -16.7232, 59.414497, -9.2638, -23.412498, 62.381397, -9.7817, -20.0679, 59.381397, -9.2581, -20.0679, 62.381397, -9.7817, -20.0679, 66.1166, -10.4337, -20.0679, 66.1166, -10.4337, -16.7232, 59.414497, -9.2638, -23.412498, 62.3756, -9.7807, -23.412498, 62.381397, -9.7817, -20.0679, 62.3756, -9.7807, -23.412498, 66.1166, -10.4337, -20.0679, 62.381397, -9.7817, -20.0679, 62.3756, -9.7807, -23.412498, 66.1166, -10.4337, -23.412498, 66.1166, -10.4337, -20.0679, 66.1166, -10.4572, -26.7572, 68.958595, -10.4337, -23.412498, 66.1166, -10.4337, -23.412498, 66.1166, -10.4572, -26.7572, 68.9481, -10.4572, -26.7572, 68.958595, -10.4337, -23.412498, 68.9481, -10.4572, -26.7572, 72.2025, -10.4337, -23.412498, 68.958595, -10.4337, -23.412498, 68.9481, -10.4572, -26.7572, 72.2092, -10.4572, -26.7572, 72.2025, -10.4337, -23.412498, 72.2092, -10.4572, -26.7572, 75.7165, -10.4337, -23.412498, 72.2025, -10.4337, -23.412498, 72.2092, -10.4572, -26.7572, 75.6957, -10.4572, -26.7572, 75.7165, -10.4337, -23.412498, 75.6957, -10.4572, -26.7572, 79.236, -10.4337, -23.412498, 75.7165, -10.4337, -23.412498, 75.6957, -10.4572, -26.7572, 79.236, -10.4572, -26.7572, 79.236, -10.4337, -23.412498, 39.877598, -3.6573, -13.378599, 42.9754, -4.7166, -10.033899, 39.877598, -3.6573, -10.033899, 39.877598, -3.6573, -13.378599, 42.951797, -4.7086, -13.378599, 42.9754, -4.7166, -10.033899, 42.951797, -4.7086, -13.378599, 46.3474, -5.8697, -10.033899, 42.9754, -4.7166, -10.033899, 42.951797, -4.7086, -13.378599, 39.877598, -3.6573, -13.378599, 39.877598, -3.6573, -16.7232, 42.951797, -4.7086, -13.378599, 46.326397, -5.8626, -13.378599, 46.3474, -5.8697, -10.033899, 46.326397, -5.8626, -13.378599, 49.4666, -6.9364, -10.033899, 46.3474, -5.8697, -10.033899, 42.951797, -4.7086, -13.378599, 39.877598, -3.6573, -16.7232, 42.9282, -4.7005, -16.7232, 46.326397, -5.8626, -13.378599, 42.951797, -4.7086, -13.378599, 42.9282, -4.7005, -16.7232, 39.877598, -3.6573, -16.7232, 42.9046, -4.6924, -20.0679, 42.9282, -4.7005, -16.7232, 39.877598, -3.6573, -16.7232, 39.877598, -3.6573, -20.0679, 42.9046, -4.6924, -20.0679, 46.326397, -5.8626, -13.378599, 42.9282, -4.7005, -16.7232, 46.3055, -5.8554, -16.7232, 42.9046, -4.6924, -20.0679, 46.3055, -5.8554, -16.7232, 42.9282, -4.7005, -16.7232, 46.326397, -5.8626, -13.378599, 49.4568, -6.9329996, -13.378599, 49.4666, -6.9364, -10.033899, 49.4568, -6.9329996, -13.378599, 46.326397, -5.8626, -13.378599, 46.3055, -5.8554, -16.7232, 49.4568, -6.9329996, -13.378599, 52.997097, -8.1437, -10.033899, 49.4666, -6.9364, -10.033899, 49.4568, -6.9329996, -13.378599, 52.997097, -8.1437, -13.378599, 52.997097, -8.1437, -10.033899, 39.877598, -3.6573, -20.0679, 42.881, -4.6844, -23.412498, 42.9046, -4.6924, -20.0679, 39.877598, -3.6573, -20.0679, 39.877598, -3.6573, -23.412498, 42.881, -4.6844, -23.412498, 42.881, -4.6844, -23.412498, 46.2845, -5.8482, -20.0679, 42.9046, -4.6924, -20.0679, 42.9046, -4.6924, -20.0679, 46.2845, -5.8482, -20.0679, 46.3055, -5.8554, -16.7232, 42.881, -4.6844, -23.412498, 46.2636, -5.8410997, -23.412498, 46.2845, -5.8482, -20.0679, 52.997097, -8.1437, -13.378599, 49.4568, -6.9329996, -13.378599, 49.447, -6.9297, -16.7232, 49.4568, -6.9329996, -13.378599, 46.3055, -5.8554, -16.7232, 49.447, -6.9297, -16.7232, 46.2845, -5.8482, -20.0679, 49.447, -6.9297, -16.7232, 46.3055, -5.8554, -16.7232, 52.997097, -8.1437, -13.378599, 49.447, -6.9297, -16.7232, 52.997097, -8.1437, -16.7232, 46.2636, -5.8410997, -23.412498, 49.4372, -6.9263997, -20.0679, 46.2845, -5.8482, -20.0679, 46.2845, -5.8482, -20.0679, 49.4372, -6.9263997, -20.0679, 49.447, -6.9297, -16.7232, 49.4372, -6.9263997, -20.0679, 52.997097, -8.1437, -16.7232, 49.447, -6.9297, -16.7232, 46.2636, -5.8410997, -23.412498, 49.427498, -6.923, -23.412498, 49.4372, -6.9263997, -20.0679, 49.4372, -6.9263997, -20.0679, 52.997097, -8.1437, -20.0679, 52.997097, -8.1437, -16.7232, 49.427498, -6.923, -23.412498, 52.997097, -8.1437, -20.0679, 49.4372, -6.9263997, -20.0679, 49.427498, -6.923, -23.412498, 52.997097, -8.1437, -23.412498, 52.997097, -8.1437, -20.0679, 20.0679, -8.1437, 52.156597, 16.7232, -8.658199, 55.0947, 16.7232, -8.1437, 52.156597, 13.378599, -8.658199, 55.0947, 16.7232, -8.1437, 52.156597, 16.7232, -8.658199, 55.0947, 13.378599, -8.658199, 55.0947, 13.378599, -8.1437, 52.156597, 16.7232, -8.1437, 52.156597, 20.0679, -8.1437, 52.156597, 20.0679, -8.658199, 55.0947, 16.7232, -8.658199, 55.0947, 20.0679, -8.658199, 55.0947, 20.0679, -8.1437, 52.156597, 23.412498, -8.1437, 52.156597, 13.378599, -9.1788, 58.067497, 13.378599, -8.658199, 55.0947, 16.7232, -8.658199, 55.0947, 20.0679, -8.658199, 55.0947, 16.7232, -9.1788, 58.067497, 16.7232, -8.658199, 55.0947, 13.378599, -9.1788, 58.067497, 16.7232, -8.658199, 55.0947, 16.7232, -9.1788, 58.067497, 10.033899, -8.658199, 55.0947, 13.378599, -8.1437, 52.156597, 13.378599, -8.658199, 55.0947, 10.033899, -8.658199, 55.0947, 10.033899, -8.1437, 52.156597, 13.378599, -8.1437, 52.156597, 10.033899, -9.1788, 58.067497, 13.378599, -8.658199, 55.0947, 13.378599, -9.1788, 58.067497, 10.033899, -9.1788, 58.067497, 10.033899, -8.658199, 55.0947, 13.378599, -8.658199, 55.0947, 6.6893, -8.658199, 55.0947, 10.033899, -8.1437, 52.156597, 10.033899, -8.658199, 55.0947, 6.6893, -8.658199, 55.0947, 6.6893, -8.1437, 52.156597, 10.033899, -8.1437, 52.156597, 6.6893, -9.1788, 58.067497, 10.033899, -8.658199, 55.0947, 10.033899, -9.1788, 58.067497, 6.6893, -9.1788, 58.067497, 6.6893, -8.658199, 55.0947, 10.033899, -8.658199, 55.0947, 3.3446, -8.658199, 55.0947, 6.6893, -8.1437, 52.156597, 6.6893, -8.658199, 55.0947, 3.3446, -8.658199, 55.0947, 3.3446, -8.1437, 52.156597, 6.6893, -8.1437, 52.156597, 3.3446, -9.1788, 58.067497, 6.6893, -8.658199, 55.0947, 6.6893, -9.1788, 58.067497, 3.3446, -9.1788, 58.067497, 3.3446, -8.658199, 55.0947, 6.6893, -8.658199, 55.0947, 0, -8.658199, 55.0947, 3.3446, -8.1437, 52.156597, 3.3446, -8.658199, 55.0947, 0, -8.658199, 55.0947, 0, -8.1437, 52.156597, 3.3446, -8.1437, 52.156597, 0, -9.1788, 58.067497, 3.3446, -8.658199, 55.0947, 3.3446, -9.1788, 58.067497, 0, -9.1788, 58.067497, 0, -8.658199, 55.0947, 3.3446, -8.658199, 55.0947, -3.3446002, -8.658199, 55.0947, 0, -8.1437, 52.156597, 0, -8.658199, 55.0947, -3.3446002, -8.658199, 55.0947, -3.3446002, -8.1437, 52.156597, 0, -8.1437, 52.156597, -3.3446002, -9.1788, 58.067497, 0, -8.658199, 55.0947, 0, -9.1788, 58.067497, -3.3446002, -9.1788, 58.067497, -3.3446002, -8.658199, 55.0947, 0, -8.658199, 55.0947, -6.6893, -8.658199, 55.0947, -3.3446002, -8.1437, 52.156597, -3.3446002, -8.658199, 55.0947, -6.6893, -8.658199, 55.0947, -6.6893, -8.1437, 52.156597, -3.3446002, -8.1437, 52.156597, -6.6893, -9.1788, 58.067497, -3.3446002, -8.658199, 55.0947, -3.3446002, -9.1788, 58.067497, -6.6893, -9.1788, 58.067497, -6.6893, -8.658199, 55.0947, -3.3446002, -8.658199, 55.0947, -10.033899, -8.658199, 55.0947, -6.6893, -8.1437, 52.156597, -6.6893, -8.658199, 55.0947, -10.033899, -8.658199, 55.0947, -10.033899, -8.1437, 52.156597, -6.6893, -8.1437, 52.156597, -10.033899, -9.1788, 58.067497, -6.6893, -8.658199, 55.0947, -6.6893, -9.1788, 58.067497, -10.033899, -9.1788, 58.067497, -10.033899, -8.658199, 55.0947, -6.6893, -8.658199, 55.0947, -13.378601, -8.658199, 55.0947, -10.033899, -8.1437, 52.156597, -10.033899, -8.658199, 55.0947, -13.378601, -8.658199, 55.0947, -13.378601, -8.1437, 52.156597, -10.033899, -8.1437, 52.156597, -13.378601, -9.1788, 58.067497, -10.033899, -8.658199, 55.0947, -10.033899, -9.1788, 58.067497, -13.378601, -9.1788, 58.067497, -13.378601, -8.658199, 55.0947, -10.033899, -8.658199, 55.0947, -16.7232, -8.658199, 55.0947, -13.378601, -8.1437, 52.156597, -13.378601, -8.658199, 55.0947, -16.7232, -8.658199, 55.0947, -16.7232, -8.1437, 52.156597, -13.378601, -8.1437, 52.156597, -16.7232, -9.1788, 58.067497, -13.378601, -8.658199, 55.0947, -13.378601, -9.1788, 58.067497, -16.7232, -9.1788, 58.067497, -16.7232, -8.658199, 55.0947, -13.378601, -8.658199, 55.0947, -20.0679, -8.658199, 55.0947, -16.7232, -8.1437, 52.156597, -16.7232, -8.658199, 55.0947, -20.0679, -8.658199, 55.0947, -20.0679, -8.1437, 52.156597, -16.7232, -8.1437, 52.156597, -20.0679, -9.1788, 58.067497, -16.7232, -8.658199, 55.0947, -16.7232, -9.1788, 58.067497, -20.0679, -9.1788, 58.067497, -20.0679, -8.658199, 55.0947, -16.7232, -8.658199, 55.0947, -23.412498, -8.658199, 55.0947, -20.0679, -8.1437, 52.156597, -20.0679, -8.658199, 55.0947, -23.412498, -8.658199, 55.0947, -23.412498, -8.1437, 52.156597, -20.0679, -8.1437, 52.156597, -26.7572, -8.658199, 55.0947, -23.412498, -8.1437, 52.156597, -23.412498, -8.658199, 55.0947, -26.7572, -8.658199, 55.0947, -26.7572, -8.1437, 52.156597, -23.412498, -8.1437, 52.156597, -26.7572, -9.1788, 58.067497, -26.7572, -8.658199, 55.0947, -23.412498, -8.658199, 55.0947, -23.412498, -9.1788, 58.067497, -23.412498, -8.658199, 55.0947, -20.0679, -8.658199, 55.0947, -26.7572, -9.1788, 58.067497, -23.412498, -8.658199, 55.0947, -23.412498, -9.1788, 58.067497, -23.412498, -9.1788, 58.067497, -20.0679, -8.658199, 55.0947, -20.0679, -9.1788, 58.067497, -26.7572, -9.8364, 61.822598, -26.7572, -9.1788, 58.067497, -23.412498, -9.1788, 58.067497, -23.412498, -9.8364, 61.822598, -23.412498, -9.1788, 58.067497, -20.0679, -9.1788, 58.067497, -26.7572, -9.8364, 61.822598, -23.412498, -9.1788, 58.067497, -23.412498, -9.8364, 61.822598, -23.412498, -10.4337, 65.2334, -26.7572, -9.8364, 61.822598, -23.412498, -9.8364, 61.822598, -23.412498, -10.4337, 65.2334, -26.7572, -10.4337, 65.2334, -26.7572, -9.8364, 61.822598, -20.0679, -10.4337, 65.2334, -23.412498, -10.4337, 65.2334, -23.412498, -9.8364, 61.822598, -23.412498, -9.8364, 61.822598, -20.0679, -9.1788, 58.067497, -20.0679, -9.8364, 61.822598, -20.0679, -10.4337, 65.2334, -23.412498, -9.8364, 61.822598, -20.0679, -9.8364, 61.822598, -20.0679, -9.8364, 61.822598, -20.0679, -9.1788, 58.067497, -16.7232, -9.1788, 58.067497, -16.7232, -10.4337, 65.2334, -20.0679, -10.4337, 65.2334, -20.0679, -9.8364, 61.822598, -20.0679, -9.8364, 61.822598, -16.7232, -9.1788, 58.067497, -16.7232, -9.8364, 61.822598, -16.7232, -10.4337, 65.2334, -20.0679, -9.8364, 61.822598, -16.7232, -9.8364, 61.822598, -16.7232, -9.8364, 61.822598, -16.7232, -9.1788, 58.067497, -13.378601, -9.1788, 58.067497, -13.378601, -10.4337, 65.2334, -16.7232, -10.4337, 65.2334, -16.7232, -9.8364, 61.822598, -16.7232, -9.8364, 61.822598, -13.378601, -9.1788, 58.067497, -13.378601, -9.8364, 61.822598, -13.378601, -10.4337, 65.2334, -16.7232, -9.8364, 61.822598, -13.378601, -9.8364, 61.822598, -13.378601, -9.8364, 61.822598, -13.378601, -9.1788, 58.067497, -10.033899, -9.1788, 58.067497, -10.033899, -10.4337, 65.2334, -13.378601, -10.4337, 65.2334, -13.378601, -9.8364, 61.822598, -13.378601, -9.8364, 61.822598, -10.033899, -9.1788, 58.067497, -10.033899, -9.8364, 61.822598, -10.033899, -10.4337, 65.2334, -13.378601, -9.8364, 61.822598, -10.033899, -9.8364, 61.822598, -10.033899, -9.8364, 61.822598, -10.033899, -9.1788, 58.067497, -6.6893, -9.1788, 58.067497, -6.6893, -10.4337, 65.2334, -10.033899, -10.4337, 65.2334, -10.033899, -9.8364, 61.822598, -10.033899, -9.8364, 61.822598, -6.6893, -9.1788, 58.067497, -6.6893, -9.8364, 61.822598, -6.6893, -10.4337, 65.2334, -10.033899, -9.8364, 61.822598, -6.6893, -9.8364, 61.822598, -6.6893, -9.8364, 61.822598, -6.6893, -9.1788, 58.067497, -3.3446002, -9.1788, 58.067497, -3.3446002, -10.4337, 65.2334, -6.6893, -10.4337, 65.2334, -6.6893, -9.8364, 61.822598, -6.6893, -9.8364, 61.822598, -3.3446002, -9.1788, 58.067497, -3.3446002, -9.8364, 61.822598, -3.3446002, -10.4337, 65.2334, -6.6893, -9.8364, 61.822598, -3.3446002, -9.8364, 61.822598, -3.3446002, -9.8364, 61.822598, -3.3446002, -9.1788, 58.067497, 0, -9.1788, 58.067497, 0, -10.4337, 65.2334, -3.3446002, -10.4337, 65.2334, -3.3446002, -9.8364, 61.822598, -3.3446002, -9.8364, 61.822598, 0, -9.1788, 58.067497, 0, -9.8364, 61.822598, 0, -10.4337, 65.2334, -3.3446002, -9.8364, 61.822598, 0, -9.8364, 61.822598, 0, -9.8364, 61.822598, 0, -9.1788, 58.067497, 3.3446, -9.1788, 58.067497, 3.3446, -10.4337, 65.2334, 0, -10.4337, 65.2334, 0, -9.8364, 61.822598, 0, -9.8364, 61.822598, 3.3446, -9.1788, 58.067497, 3.3446, -9.8364, 61.822598, 3.3446, -10.4337, 65.2334, 0, -9.8364, 61.822598, 3.3446, -9.8364, 61.822598, 3.3446, -9.8364, 61.822598, 3.3446, -9.1788, 58.067497, 6.6893, -9.1788, 58.067497, 6.6893, -10.4337, 65.2334, 3.3446, -10.4337, 65.2334, 3.3446, -9.8364, 61.822598, 3.3446, -9.8364, 61.822598, 6.6893, -9.1788, 58.067497, 6.6893, -9.8364, 61.822598, 6.6893, -10.4337, 65.2334, 3.3446, -9.8364, 61.822598, 6.6893, -9.8364, 61.822598, 6.6893, -9.8364, 61.822598, 6.6893, -9.1788, 58.067497, 10.033899, -9.1788, 58.067497, 10.033899, -10.4337, 65.2334, 6.6893, -10.4337, 65.2334, 6.6893, -9.8364, 61.822598, 6.6893, -9.8364, 61.822598, 10.033899, -9.1788, 58.067497, 10.033899, -9.8364, 61.822598, 10.033899, -10.4337, 65.2334, 6.6893, -9.8364, 61.822598, 10.033899, -9.8364, 61.822598, 10.033899, -9.8364, 61.822598, 10.033899, -9.1788, 58.067497, 13.378599, -9.1788, 58.067497, 13.378599, -10.4337, 65.2334, 10.033899, -10.4337, 65.2334, 10.033899, -9.8364, 61.822598, 10.033899, -9.8364, 61.822598, 13.378599, -9.1788, 58.067497, 13.378599, -9.8364, 61.822598, 13.378599, -10.4337, 65.2334, 10.033899, -9.8364, 61.822598, 13.378599, -9.8364, 61.822598, 13.378599, -9.8364, 61.822598, 13.378599, -9.1788, 58.067497, 16.7232, -9.1788, 58.067497, 16.7232, -10.4337, 65.2334, 13.378599, -10.4337, 65.2334, 13.378599, -9.8364, 61.822598, 13.378599, -9.8364, 61.822598, 16.7232, -9.1788, 58.067497, 16.7232, -9.8364, 61.822598, 16.7232, -10.4337, 65.2334, 13.378599, -9.8364, 61.822598, 16.7232, -9.8364, 61.822598, 20.0679, -9.1788, 58.067497, 16.7232, -9.8364, 61.822598, 16.7232, -9.1788, 58.067497, 20.0679, -9.8364, 61.822598, 16.7232, -10.4337, 65.2334, 16.7232, -9.8364, 61.822598, 20.0679, -9.1788, 58.067497, 20.0679, -9.8364, 61.822598, 16.7232, -9.8364, 61.822598, 20.0679, -9.8364, 61.822598, 20.0679, -10.4337, 65.2334, 16.7232, -10.4337, 65.2334, 20.0679, -8.658199, 55.0947, 20.0679, -9.1788, 58.067497, 16.7232, -9.1788, 58.067497, 20.0679, -10.4337, 65.2334, 20.0679, -9.8364, 61.822598, 23.412498, -9.8364, 61.822598, 20.0679, -10.4337, 65.2334, 23.412498, -9.8364, 61.822598, 23.412498, -10.4337, 65.2334, 26.7582, -9.8364, 61.822598, 23.412498, -10.4337, 65.2334, 23.412498, -9.8364, 61.822598, 26.7582, -9.8364, 61.822598, 26.7582, -10.4337, 65.2334, 23.412498, -10.4337, 65.2334, 26.7582, -9.1788, 58.067497, 26.7582, -9.8364, 61.822598, 23.412498, -9.8364, 61.822598, 20.0679, -9.8364, 61.822598, 23.412498, -9.1788, 58.067497, 23.412498, -9.8364, 61.822598, 26.7582, -9.1788, 58.067497, 23.412498, -9.8364, 61.822598, 23.412498, -9.1788, 58.067497, 20.0679, -9.8364, 61.822598, 20.0679, -9.1788, 58.067497, 23.412498, -9.1788, 58.067497, 26.7582, -8.658199, 55.0947, 26.7582, -9.1788, 58.067497, 23.412498, -9.1788, 58.067497, 20.0679, -9.1788, 58.067497, 20.0679, -8.658199, 55.0947, 23.412498, -8.658199, 55.0947, 20.0679, -9.1788, 58.067497, 23.412498, -8.658199, 55.0947, 23.412498, -9.1788, 58.067497, 26.7582, -8.658199, 55.0947, 23.412498, -9.1788, 58.067497, 23.412498, -8.658199, 55.0947, 20.0679, -8.658199, 55.0947, 23.412498, -8.1437, 52.156597, 23.412498, -8.658199, 55.0947, 23.412498, -8.1437, 52.156597, 26.7582, -8.658199, 55.0947, 23.412498, -8.658199, 55.0947, 23.412498, -8.1437, 52.156597, 26.7582, -8.1437, 52.156597, 26.7582, -8.658199, 55.0947, 20.0679, -3.8518999, 39.456898, 16.7232, -4.9595, 42.7343, 16.7232, -3.8518999, 39.456898, 13.378599, -4.9595, 42.7343, 16.7232, -3.8518999, 39.456898, 16.7232, -4.9595, 42.7343, 13.378599, -4.9595, 42.7343, 13.378599, -3.8518999, 39.456898, 16.7232, -3.8518999, 39.456898, 20.0679, -3.8518999, 39.456898, 20.0679, -4.9595, 42.7343, 16.7232, -4.9595, 42.7343, 20.0679, -4.9595, 42.7343, 20.0679, -3.8518999, 39.456898, 23.412498, -3.8518999, 39.456898, 13.378599, -6.2019997, 46.4111, 13.378599, -4.9595, 42.7343, 16.7232, -4.9595, 42.7343, 20.0679, -4.9595, 42.7343, 16.7232, -6.2019997, 46.4111, 16.7232, -4.9595, 42.7343, 13.378599, -6.2019997, 46.4111, 16.7232, -4.9595, 42.7343, 16.7232, -6.2019997, 46.4111, 10.033899, -4.9595, 42.7343, 13.378599, -3.8518999, 39.456898, 13.378599, -4.9595, 42.7343, 10.033899, -4.9595, 42.7343, 10.033899, -3.8518999, 39.456898, 13.378599, -3.8518999, 39.456898, 10.033899, -6.2019997, 46.4111, 13.378599, -4.9595, 42.7343, 13.378599, -6.2019997, 46.4111, 10.033899, -6.2019997, 46.4111, 10.033899, -4.9595, 42.7343, 13.378599, -4.9595, 42.7343, 6.6893, -4.9595, 42.7343, 10.033899, -3.8518999, 39.456898, 10.033899, -4.9595, 42.7343, 6.6893, -4.9595, 42.7343, 6.6893, -3.8518999, 39.456898, 10.033899, -3.8518999, 39.456898, 6.6893, -6.2019997, 46.4111, 10.033899, -4.9595, 42.7343, 10.033899, -6.2019997, 46.4111, 6.6893, -6.2019997, 46.4111, 6.6893, -4.9595, 42.7343, 10.033899, -4.9595, 42.7343, 3.3446, -4.9595, 42.7343, 6.6893, -3.8518999, 39.456898, 6.6893, -4.9595, 42.7343, 3.3446, -4.9595, 42.7343, 3.3446, -3.8518999, 39.456898, 6.6893, -3.8518999, 39.456898, 3.3446, -6.2019997, 46.4111, 6.6893, -4.9595, 42.7343, 6.6893, -6.2019997, 46.4111, 3.3446, -6.2019997, 46.4111, 3.3446, -4.9595, 42.7343, 6.6893, -4.9595, 42.7343, 0, -4.9595, 42.7343, 3.3446, -3.8518999, 39.456898, 3.3446, -4.9595, 42.7343, 0, -4.9595, 42.7343, 0, -3.8518999, 39.456898, 3.3446, -3.8518999, 39.456898, 0, -6.2019997, 46.4111, 3.3446, -4.9595, 42.7343, 3.3446, -6.2019997, 46.4111, 0, -6.2019997, 46.4111, 0, -4.9595, 42.7343, 3.3446, -4.9595, 42.7343, -3.3446002, -4.9595, 42.7343, 0, -3.8518999, 39.456898, 0, -4.9595, 42.7343, -3.3446002, -4.9595, 42.7343, -3.3446002, -3.8518999, 39.456898, 0, -3.8518999, 39.456898, -3.3446002, -6.2019997, 46.4111, 0, -4.9595, 42.7343, 0, -6.2019997, 46.4111, -3.3446002, -6.2019997, 46.4111, -3.3446002, -4.9595, 42.7343, 0, -4.9595, 42.7343, -6.6893, -4.9595, 42.7343, -3.3446002, -3.8518999, 39.456898, -3.3446002, -4.9595, 42.7343, -6.6893, -4.9595, 42.7343, -6.6893, -3.8518999, 39.456898, -3.3446002, -3.8518999, 39.456898, -6.6893, -6.2019997, 46.4111, -3.3446002, -4.9595, 42.7343, -3.3446002, -6.2019997, 46.4111, -6.6893, -6.2019997, 46.4111, -6.6893, -4.9595, 42.7343, -3.3446002, -4.9595, 42.7343, -10.033899, -4.9595, 42.7343, -6.6893, -3.8518999, 39.456898, -6.6893, -4.9595, 42.7343, -10.033899, -4.9595, 42.7343, -10.033899, -3.8518999, 39.456898, -6.6893, -3.8518999, 39.456898, -10.033899, -6.2019997, 46.4111, -6.6893, -4.9595, 42.7343, -6.6893, -6.2019997, 46.4111, -10.033899, -6.2019997, 46.4111, -10.033899, -4.9595, 42.7343, -6.6893, -4.9595, 42.7343, -13.378601, -4.9595, 42.7343, -10.033899, -3.8518999, 39.456898, -10.033899, -4.9595, 42.7343, -13.378601, -4.9595, 42.7343, -13.378601, -3.8518999, 39.456898, -10.033899, -3.8518999, 39.456898, -13.378601, -6.2019997, 46.4111, -10.033899, -4.9595, 42.7343, -10.033899, -6.2019997, 46.4111, -13.378601, -6.2019997, 46.4111, -13.378601, -4.9595, 42.7343, -10.033899, -4.9595, 42.7343, -16.7232, -4.9595, 42.7343, -13.378601, -3.8518999, 39.456898, -13.378601, -4.9595, 42.7343, -16.7232, -4.9595, 42.7343, -16.7232, -3.8518999, 39.456898, -13.378601, -3.8518999, 39.456898, -16.7232, -6.2019997, 46.4111, -13.378601, -4.9595, 42.7343, -13.378601, -6.2019997, 46.4111, -16.7232, -6.2019997, 46.4111, -16.7232, -4.9595, 42.7343, -13.378601, -4.9595, 42.7343, -20.0679, -4.9595, 42.7343, -16.7232, -3.8518999, 39.456898, -16.7232, -4.9595, 42.7343, -20.0679, -4.9595, 42.7343, -20.0679, -3.8518999, 39.456898, -16.7232, -3.8518999, 39.456898, -20.0679, -6.2019997, 46.4111, -16.7232, -4.9595, 42.7343, -16.7232, -6.2019997, 46.4111, -20.0679, -6.2019997, 46.4111, -20.0679, -4.9595, 42.7343, -16.7232, -4.9595, 42.7343, -23.412498, -4.9595, 42.7343, -20.0679, -3.8518999, 39.456898, -20.0679, -4.9595, 42.7343, -23.412498, -4.9595, 42.7343, -23.412498, -3.8518999, 39.456898, -20.0679, -3.8518999, 39.456898, -26.7572, -4.9595, 42.7343, -23.412498, -3.8518999, 39.456898, -23.412498, -4.9595, 42.7343, -26.7572, -4.9595, 42.7343, -26.7572, -3.8518999, 39.456898, -23.412498, -3.8518999, 39.456898, -26.7572, -6.2019997, 46.4111, -26.7572, -4.9595, 42.7343, -23.412498, -4.9595, 42.7343, -23.412498, -6.2019997, 46.4111, -23.412498, -4.9595, 42.7343, -20.0679, -4.9595, 42.7343, -26.7572, -6.2019997, 46.4111, -23.412498, -4.9595, 42.7343, -23.412498, -6.2019997, 46.4111, -23.412498, -6.2019997, 46.4111, -20.0679, -4.9595, 42.7343, -20.0679, -6.2019997, 46.4111, -26.7572, -7.286, 49.6186, -26.7572, -6.2019997, 46.4111, -23.412498, -6.2019997, 46.4111, -23.412498, -7.286, 49.6186, -23.412498, -6.2019997, 46.4111, -20.0679, -6.2019997, 46.4111, -26.7572, -7.286, 49.6186, -23.412498, -6.2019997, 46.4111, -23.412498, -7.286, 49.6186, -23.412498, -8.1437, 52.156597, -26.7572, -7.286, 49.6186, -23.412498, -7.286, 49.6186, -23.412498, -8.1437, 52.156597, -26.7572, -8.1437, 52.156597, -26.7572, -7.286, 49.6186, -20.0679, -8.1437, 52.156597, -23.412498, -8.1437, 52.156597, -23.412498, -7.286, 49.6186, -23.412498, -7.286, 49.6186, -20.0679, -6.2019997, 46.4111, -20.0679, -7.286, 49.6186, -20.0679, -8.1437, 52.156597, -23.412498, -7.286, 49.6186, -20.0679, -7.286, 49.6186, -20.0679, -7.286, 49.6186, -20.0679, -6.2019997, 46.4111, -16.7232, -6.2019997, 46.4111, -16.7232, -8.1437, 52.156597, -20.0679, -8.1437, 52.156597, -20.0679, -7.286, 49.6186, -20.0679, -7.286, 49.6186, -16.7232, -6.2019997, 46.4111, -16.7232, -7.286, 49.6186, -16.7232, -8.1437, 52.156597, -20.0679, -7.286, 49.6186, -16.7232, -7.286, 49.6186, -16.7232, -7.286, 49.6186, -16.7232, -6.2019997, 46.4111, -13.378601, -6.2019997, 46.4111, -13.378601, -8.1437, 52.156597, -16.7232, -8.1437, 52.156597, -16.7232, -7.286, 49.6186, -16.7232, -7.286, 49.6186, -13.378601, -6.2019997, 46.4111, -13.378601, -7.286, 49.6186, -13.378601, -8.1437, 52.156597, -16.7232, -7.286, 49.6186, -13.378601, -7.286, 49.6186, -13.378601, -7.286, 49.6186, -13.378601, -6.2019997, 46.4111, -10.033899, -6.2019997, 46.4111, -10.033899, -8.1437, 52.156597, -13.378601, -8.1437, 52.156597, -13.378601, -7.286, 49.6186, -13.378601, -7.286, 49.6186, -10.033899, -6.2019997, 46.4111, -10.033899, -7.286, 49.6186, -10.033899, -8.1437, 52.156597, -13.378601, -7.286, 49.6186, -10.033899, -7.286, 49.6186, -10.033899, -7.286, 49.6186, -10.033899, -6.2019997, 46.4111, -6.6893, -6.2019997, 46.4111, -6.6893, -8.1437, 52.156597, -10.033899, -8.1437, 52.156597, -10.033899, -7.286, 49.6186, -10.033899, -7.286, 49.6186, -6.6893, -6.2019997, 46.4111, -6.6893, -7.286, 49.6186, -6.6893, -8.1437, 52.156597, -10.033899, -7.286, 49.6186, -6.6893, -7.286, 49.6186, -6.6893, -7.286, 49.6186, -6.6893, -6.2019997, 46.4111, -3.3446002, -6.2019997, 46.4111, -3.3446002, -8.1437, 52.156597, -6.6893, -8.1437, 52.156597, -6.6893, -7.286, 49.6186, -6.6893, -7.286, 49.6186, -3.3446002, -6.2019997, 46.4111, -3.3446002, -7.286, 49.6186, -3.3446002, -8.1437, 52.156597, -6.6893, -7.286, 49.6186, -3.3446002, -7.286, 49.6186, -3.3446002, -7.286, 49.6186, -3.3446002, -6.2019997, 46.4111, 0, -6.2019997, 46.4111, 0, -8.1437, 52.156597, -3.3446002, -8.1437, 52.156597, -3.3446002, -7.286, 49.6186, -3.3446002, -7.286, 49.6186, 0, -6.2019997, 46.4111, 0, -7.286, 49.6186, 0, -8.1437, 52.156597, -3.3446002, -7.286, 49.6186, 0, -7.286, 49.6186, 0, -7.286, 49.6186, 0, -6.2019997, 46.4111, 3.3446, -6.2019997, 46.4111, 3.3446, -8.1437, 52.156597, 0, -8.1437, 52.156597, 0, -7.286, 49.6186, 0, -7.286, 49.6186, 3.3446, -6.2019997, 46.4111, 3.3446, -7.286, 49.6186, 3.3446, -8.1437, 52.156597, 0, -7.286, 49.6186, 3.3446, -7.286, 49.6186, 3.3446, -7.286, 49.6186, 3.3446, -6.2019997, 46.4111, 6.6893, -6.2019997, 46.4111, 6.6893, -8.1437, 52.156597, 3.3446, -8.1437, 52.156597, 3.3446, -7.286, 49.6186, 3.3446, -7.286, 49.6186, 6.6893, -6.2019997, 46.4111, 6.6893, -7.286, 49.6186, 6.6893, -8.1437, 52.156597, 3.3446, -7.286, 49.6186, 6.6893, -7.286, 49.6186, 6.6893, -7.286, 49.6186, 6.6893, -6.2019997, 46.4111, 10.033899, -6.2019997, 46.4111, 10.033899, -8.1437, 52.156597, 6.6893, -8.1437, 52.156597, 6.6893, -7.286, 49.6186, 6.6893, -7.286, 49.6186, 10.033899, -6.2019997, 46.4111, 10.033899, -7.286, 49.6186, 10.033899, -8.1437, 52.156597, 6.6893, -7.286, 49.6186, 10.033899, -7.286, 49.6186, 10.033899, -7.286, 49.6186, 10.033899, -6.2019997, 46.4111, 13.378599, -6.2019997, 46.4111, 13.378599, -8.1437, 52.156597, 10.033899, -8.1437, 52.156597, 10.033899, -7.286, 49.6186, 10.033899, -7.286, 49.6186, 13.378599, -6.2019997, 46.4111, 13.378599, -7.286, 49.6186, 13.378599, -8.1437, 52.156597, 10.033899, -7.286, 49.6186, 13.378599, -7.286, 49.6186, 13.378599, -7.286, 49.6186, 13.378599, -6.2019997, 46.4111, 16.7232, -6.2019997, 46.4111, 16.7232, -8.1437, 52.156597, 13.378599, -8.1437, 52.156597, 13.378599, -7.286, 49.6186, 13.378599, -7.286, 49.6186, 16.7232, -6.2019997, 46.4111, 16.7232, -7.286, 49.6186, 16.7232, -8.1437, 52.156597, 13.378599, -7.286, 49.6186, 16.7232, -7.286, 49.6186, 20.0679, -6.2019997, 46.4111, 16.7232, -7.286, 49.6186, 16.7232, -6.2019997, 46.4111, 20.0679, -7.286, 49.6186, 16.7232, -8.1437, 52.156597, 16.7232, -7.286, 49.6186, 20.0679, -6.2019997, 46.4111, 20.0679, -7.286, 49.6186, 16.7232, -7.286, 49.6186, 20.0679, -7.286, 49.6186, 20.0679, -8.1437, 52.156597, 16.7232, -8.1437, 52.156597, 20.0679, -4.9595, 42.7343, 20.0679, -6.2019997, 46.4111, 16.7232, -6.2019997, 46.4111, 20.0679, -8.1437, 52.156597, 20.0679, -7.286, 49.6186, 23.412498, -7.286, 49.6186, 20.0679, -8.1437, 52.156597, 23.412498, -7.286, 49.6186, 23.412498, -8.1437, 52.156597, 26.7582, -7.286, 49.6186, 23.412498, -8.1437, 52.156597, 23.412498, -7.286, 49.6186, 26.7582, -7.286, 49.6186, 26.7582, -8.1437, 52.156597, 23.412498, -8.1437, 52.156597, 26.7582, -6.2019997, 46.4111, 26.7582, -7.286, 49.6186, 23.412498, -7.286, 49.6186, 20.0679, -7.286, 49.6186, 23.412498, -6.2019997, 46.4111, 23.412498, -7.286, 49.6186, 26.7582, -6.2019997, 46.4111, 23.412498, -7.286, 49.6186, 23.412498, -6.2019997, 46.4111, 20.0679, -7.286, 49.6186, 20.0679, -6.2019997, 46.4111, 23.412498, -6.2019997, 46.4111, 26.7582, -4.9595, 42.7343, 26.7582, -6.2019997, 46.4111, 23.412498, -6.2019997, 46.4111, 20.0679, -6.2019997, 46.4111, 20.0679, -4.9595, 42.7343, 23.412498, -4.9595, 42.7343, 20.0679, -6.2019997, 46.4111, 23.412498, -4.9595, 42.7343, 23.412498, -6.2019997, 46.4111, 26.7582, -4.9595, 42.7343, 23.412498, -6.2019997, 46.4111, 23.412498, -4.9595, 42.7343, 20.0679, -4.9595, 42.7343, 23.412498, -3.8518999, 39.456898, 23.412498, -4.9595, 42.7343, 23.412498, -3.8518999, 39.456898, 26.7582, -4.9595, 42.7343, 23.412498, -4.9595, 42.7343, 23.412498, -3.8518999, 39.456898, 26.7582, -3.8518999, 39.456898, 26.7582, -4.9595, 42.7343, -26.7572, -1.2701, 26.7572, -26.7572, -2.0054, 30.3737, -28.645699, -2.01, 28.5121, -28.645699, -2.01, 28.5121, -30.426, -2.0144, 26.7572, -26.7572, -1.2701, 26.7572, -28.447899, -2.6727, 31.969099, -28.645699, -2.01, 28.5121, -26.7572, -2.0054, 30.3737, -26.7572, -2.0054, 30.3737, -26.7572, -2.6892, 33.737698, -28.447899, -2.6727, 31.969099, -30.291101, -2.6547, 30.040998, -30.426, -2.0144, 26.7572, -28.645699, -2.01, 28.5121, -28.447899, -2.6727, 31.969099, -30.291101, -2.6547, 30.040998, -28.645699, -2.01, 28.5121, -30.291101, -2.6547, 30.040998, -32.027798, -2.6376998, 28.2244, -30.426, -2.0144, 26.7572, -32.027798, -2.6376998, 28.2244, -33.4304, -2.6239998, 26.7572, -30.426, -2.0144, 26.7572, -29.8963, -3.2444, 33.336, -30.291101, -2.6547, 30.040998, -28.447899, -2.6727, 31.969099, -26.7572, -2.6892, 33.737698, -29.8963, -3.2444, 33.336, -28.447899, -2.6727, 31.969099, -31.7465, -3.2247999, 31.3933, -32.027798, -2.6376998, 28.2244, -30.291101, -2.6547, 30.040998, -29.8963, -3.2444, 33.336, -31.7465, -3.2247999, 31.3933, -30.291101, -2.6547, 30.040998, -26.7572, -2.6892, 33.737698, -28.188799, -3.2624998, 35.128998, -29.8963, -3.2444, 33.336, -28.188799, -3.2624998, 35.128998, -26.7572, -2.6892, 33.737698, -26.7572, -3.2777, 36.6322, -29.5455, -3.8058, 36.4474, -29.8963, -3.2444, 33.336, -28.188799, -3.2624998, 35.128998, -26.7572, -3.2777, 36.6322, -29.5455, -3.8058, 36.4474, -28.188799, -3.2624998, 35.128998, -26.7572, -3.2777, 36.6322, -28.1288, -3.8291998, 37.976498, -29.5455, -3.8058, 36.4474, -28.1288, -3.8291998, 37.976498, -26.7572, -3.2777, 36.6322, -26.7572, -3.8518999, 39.456898, -29.5455, -3.8058, 36.4474, -31.247301, -3.7775998, 34.6107, -29.8963, -3.2444, 33.336, -31.247301, -3.7775998, 34.6107, -31.7465, -3.2247999, 31.3933, -29.8963, -3.2444, 33.336, -31.247301, -3.7775998, 34.6107, -33.0801, -3.7473, 32.6325, -31.7465, -3.2247999, 31.3933, -33.0801, -3.7473, 32.6325, -33.4894, -3.2064, 29.5632, -31.7465, -3.2247999, 31.3933, -31.7465, -3.2247999, 31.3933, -33.4894, -3.2064, 29.5632, -32.027798, -2.6376998, 28.2244, -33.4894, -3.2064, 29.5632, -33.4304, -2.6239998, 26.7572, -32.027798, -2.6376998, 28.2244, -33.0801, -3.7473, 32.6325, -34.806297, -3.7187998, 30.769499, -33.4894, -3.2064, 29.5632, -33.4894, -3.2064, 29.5632, -34.9099, -3.1913, 28.0716, -33.4304, -2.6239998, 26.7572, -34.806297, -3.7187998, 30.769499, -34.9099, -3.1913, 28.0716, -33.4894, -3.2064, 29.5632, -34.9099, -3.1913, 28.0716, -36.161697, -3.1780999, 26.7572, -33.4304, -2.6239998, 26.7572, -34.806297, -3.7187998, 30.769499, -36.2241, -3.6952999, 29.2393, -34.9099, -3.1913, 28.0716, -36.2241, -3.6952999, 29.2393, -36.161697, -3.1780999, 26.7572, -34.9099, -3.1913, 28.0716, -36.2241, -3.6952999, 29.2393, -37.4711, -3.6747, 27.8934, -36.161697, -3.1780999, 26.7572, -37.4711, -3.6747, 27.8934, -38.5239, -3.6573, 26.7572, -36.161697, -3.1780999, 26.7572, -50.290497, -8.1437, 26.7572, -47.4473, -7.0596, 26.7572, -49.074398, -8.1437, 28.0697, -62.057194, -10.4337, 26.7572, -58.463497, -9.7343, 26.7572, -60.611, -10.4337, 28.3335, 26.7582, -9.8364, 61.822598, 66.1166, -10.4337, 26.7572, 26.7582, -10.4337, 65.2334, 26.7582, -9.8364, 61.822598, 62.4636, -9.7961, 26.7572, 66.1166, -10.4337, 26.7572, 26.7582, -9.1788, 58.067497, 62.4636, -9.7961, 26.7572, 26.7582, -9.8364, 61.822598, 26.7582, -9.1788, 58.067497, 58.918198, -9.177199, 26.7572, 62.4636, -9.7961, 26.7572, 26.7582, -9.1788, 58.067497, 55.849197, -8.6414995, 26.7572, 58.918198, -9.177199, 26.7572, 55.849197, -8.6414995, 26.7572, 26.7582, -9.1788, 58.067497, 52.997097, -8.1437, 26.7572, 52.997097, -8.1437, 26.7572, 26.7582, -9.1788, 58.067497, 26.7582, -8.1437, 52.156597, 26.7582, -8.1437, 52.156597, 26.7582, -9.1788, 58.067497, 26.7582, -8.658199, 55.0947, 43.234898, -4.8054, 26.7572, 26.7582, -3.8518999, 39.456898, 39.877598, -3.6573, 26.7572, 46.577698, -5.9484997, 26.7572, 26.7582, -3.8518999, 39.456898, 43.234898, -4.8054, 26.7572, 46.577698, -5.9484997, 26.7572, 26.7582, -6.2019997, 46.4111, 26.7582, -3.8518999, 39.456898, 26.7582, -3.8518999, 39.456898, 26.7582, -6.2019997, 46.4111, 26.7582, -4.9595, 42.7343, 26.7582, -6.2019997, 46.4111, 46.577698, -5.9484997, 26.7572, 26.7582, -8.1437, 52.156597, 26.7582, -6.2019997, 46.4111, 26.7582, -8.1437, 52.156597, 26.7582, -7.286, 49.6186, 26.7582, -8.1437, 52.156597, 46.577698, -5.9484997, 26.7572, 49.5743, -6.9732, 26.7572, 26.7582, -8.1437, 52.156597, 49.5743, -6.9732, 26.7572, 52.997097, -8.1437, 26.7572, 29.1145, -2.5956, 26.7572, 26.7582, -3.0839999, 30.2834, 26.7582, -2.7837, 26.6955, 29.1145, -2.5956, 26.7572, 26.7582, -3.3683999, 33.680798, 26.7582, -3.0839999, 30.2834, 33.146698, -3.214, 26.7572, 26.7582, -3.3683999, 33.680798, 29.1145, -2.5956, 26.7572, 33.146698, -3.214, 26.7572, 26.7582, -3.6130998, 36.6041, 26.7582, -3.3683999, 33.680798, 26.7582, -3.6130998, 36.6041, 33.146698, -3.214, 26.7572, 39.877598, -3.6573, 26.7572, 39.877598, -3.6573, 26.7572, 33.146698, -3.214, 26.7572, 36.078598, -3.0746999, 26.7572, 26.7582, -3.6130998, 36.6041, 39.877598, -3.6573, 26.7572, 26.7582, -3.8518999, 39.456898, 26.7573, -1.7447, -30.111198, 29.3928, -1.796, -26.7572, 26.7573, -1.3222, -26.7572, 26.7572, -2.5274, -33.9013, 29.3928, -1.796, -26.7572, 26.7573, -1.7447, -30.111198, 29.3928, -1.796, -26.7572, 26.7572, -2.5274, -33.9013, 26.7572, -3.9166, -40.6287, 26.7572, -3.9166, -40.6287, 26.7572, -2.5274, -33.9013, 26.7572, -3.2101, -37.2077, 29.3928, -1.796, -26.7572, 26.7572, -3.9166, -40.6287, 36.0678, -2.9959998, -26.7572, 36.0678, -2.9959998, -26.7572, 26.7572, -3.9166, -40.6287, 39.877598, -3.6808999, -26.7572, 29.3928, -1.796, -26.7572, 36.0678, -2.9959998, -26.7572, 32.9589, -2.4371, -26.7572, 49.4036, -6.9384, -26.7572, 26.7572, -8.403, -54.500298, 52.997097, -8.1672, -26.7572, 46.2278, -5.8524, -26.7572, 26.7572, -8.403, -54.500298, 49.4036, -6.9384, -26.7572, 26.7572, -8.403, -54.500298, 46.2278, -5.8524, -26.7572, 26.7572, -3.9166, -40.6287, 26.7572, -3.9166, -40.6287, 46.2278, -5.8524, -26.7572, 42.8416, -4.6944, -26.7572, 26.7572, -3.9166, -40.6287, 42.8416, -4.6944, -26.7572, 39.877598, -3.6808999, -26.7572, 26.7572, -8.403, -54.500298, 26.7572, -3.9166, -40.6287, 26.7572, -6.2504, -47.8447, 26.7572, -6.2504, -47.8447, 26.7572, -3.9166, -40.6287, 26.7572, -5.1514, -44.446598, 26.7572, -8.403, -54.500298, 26.7572, -6.2504, -47.8447, 26.7572, -7.3550997, -51.2602, 62.356697, -9.8008995, -26.7572, 26.7572, -10.693, -68.371895, 66.1166, -10.4572, -26.7572, 59.4335, -9.2907, -26.7572, 26.7572, -10.693, -68.371895, 62.356697, -9.8008995, -26.7572, 26.7572, -10.693, -68.371895, 59.4335, -9.2907, -26.7572, 26.7572, -9.312699, -60.011097, 26.7572, -10.693, -68.371895, 26.7572, -9.312699, -60.011097, 26.7572, -9.876599, -63.426796, 26.7572, -9.312699, -60.011097, 59.4335, -9.2907, -26.7572, 56.4827, -8.7755995, -26.7572, 26.7572, -9.312699, -60.011097, 56.4827, -8.7755995, -26.7572, 26.7572, -8.881, -57.3956, 26.7572, -8.881, -57.3956, 56.4827, -8.7755995, -26.7572, 52.997097, -8.1672, -26.7572, 26.7572, -8.881, -57.3956, 52.997097, -8.1672, -26.7572, 26.7572, -8.403, -54.500298, 26.7572, -10.693, -70.330795, 66.1166, -10.4572, -26.7572, 26.7572, -10.693, -68.371895, 26.7572, -10.693, -70.330795, 68.9481, -10.4572, -26.7572, 66.1166, -10.4572, -26.7572, 26.7572, -10.693, -73.1232, 68.9481, -10.4572, -26.7572, 26.7572, -10.693, -70.330795, 26.7572, -10.693, -73.1232, 72.2092, -10.4572, -26.7572, 68.9481, -10.4572, -26.7572, 26.7572, -10.693, -73.1232, 75.6957, -10.4572, -26.7572, 72.2092, -10.4572, -26.7572, 75.6957, -10.4572, -26.7572, 26.7572, -10.693, -73.1232, 26.7572, -10.693, -82.2434, 75.6957, -10.4572, -26.7572, 26.7572, -10.693, -82.2434, 79.236, -10.4572, -26.7572, 26.7572, -10.693, -82.2434, 26.7572, -10.693, -73.1232, 26.7572, -10.693, -79.5579, 26.7572, -10.693, -79.5579, 26.7572, -10.693, -73.1232, 26.7572, -10.693, -76.193, -30.173899, -3.2568, -26.7572, -26.7572, -3.2445998, -30.4851, -26.7572, -3.0929, -26.7572, -30.173899, -3.2568, -26.7572, -26.7572, -3.3862, -33.9664, -26.7572, -3.2445998, -30.4851, -30.173899, -3.2568, -26.7572, -26.7572, -3.5244, -37.3623, -26.7572, -3.3862, -33.9664, -26.7572, -3.5244, -37.3623, -30.173899, -3.2568, -26.7572, -36.0882, -3.5405, -26.7572, -36.0882, -3.5405, -26.7572, -30.173899, -3.2568, -26.7572, -33.2719, -3.4053998, -26.7572, -26.7572, -3.5244, -37.3623, -36.0882, -3.5405, -26.7572, -26.7572, -3.6573, -40.6287, -26.7572, -3.6573, -40.6287, -36.0882, -3.5405, -26.7572, -38.5239, -3.6573, -26.7572, -41.627, -4.8403997, -26.7572, -26.7572, -3.6573, -40.6287, -38.5239, -3.6573, -26.7572, -41.627, -4.8403997, -26.7572, -26.7572, -5.9270997, -47.646698, -26.7572, -3.6573, -40.6287, -26.7572, -3.6573, -40.6287, -26.7572, -5.9270997, -47.646698, -26.7572, -4.8125997, -44.200798, -44.537098, -5.95, -26.7572, -26.7572, -5.9270997, -47.646698, -41.627, -4.8403997, -26.7572, -26.7572, -5.9270997, -47.646698, -44.537098, -5.95, -26.7572, -50.290497, -8.1437, -26.7572, -50.290497, -8.1437, -26.7572, -44.537098, -5.95, -26.7572, -47.4473, -7.0596, -26.7572, -26.7572, -5.9270997, -47.646698, -50.290497, -8.1437, -26.7572, -26.7572, -7.1011, -51.2768, -26.7572, -7.1011, -51.2768, -50.290497, -8.1437, -26.7572, -26.7572, -8.1437, -54.500298, -53.117798, -8.6939, -26.7572, -26.7572, -8.1437, -54.500298, -50.290497, -8.1437, -26.7572, -55.5951, -9.176, -26.7572, -26.7572, -8.1437, -54.500298, -53.117798, -8.6939, -26.7572, -58.463497, -9.7343, -26.7572, -26.7572, -8.1437, -54.500298, -55.5951, -9.176, -26.7572, -26.7572, -8.1437, -54.500298, -58.463497, -9.7343, -26.7572, -26.7572, -9.7125, -64.0031, -26.7572, -9.7125, -64.0031, -58.463497, -9.7343, -26.7572, -62.057194, -10.4337, -26.7572, -26.7572, -9.7125, -64.0031, -62.057194, -10.4337, -26.7572, -26.7572, -10.4337, -68.371895, -26.7572, -8.1437, -54.500298, -26.7572, -9.7125, -64.0031, -26.7572, -9.111199, -60.361, -26.7572, -8.1437, -54.500298, -26.7572, -9.111199, -60.361, -26.7572, -8.6276, -57.4314, -64.963196, -10.4337, -26.7572, -26.7572, -10.4337, -68.371895, -62.057194, -10.4337, -26.7572, -64.963196, -10.4337, -26.7572, -26.7572, -10.4337, -70.9319, -26.7572, -10.4337, -68.371895, -64.963196, -10.4337, -26.7572, -62.057194, -10.4337, -26.7572, -62.057194, -10.4337, -23.412498, -68.2489, -10.4337, -26.7572, -26.7572, -10.4337, -70.9319, -64.963196, -10.4337, -26.7572, -68.2489, -10.4337, -26.7572, -26.7572, -10.4337, -73.7615, -26.7572, -10.4337, -70.9319, -64.963196, -10.4337, -26.7572, -62.057194, -10.4337, -23.412498, -64.963196, -10.4337, -23.412498, -68.2489, -10.4337, -26.7572, -64.963196, -10.4337, -26.7572, -64.963196, -10.4337, -23.412498, -64.963196, -10.4337, -23.412498, -62.057194, -10.4337, -23.412498, -62.057194, -10.4337, -20.174799, -71.2999, -10.4337, -26.7572, -26.7572, -10.4337, -73.7615, -68.2489, -10.4337, -26.7572, -71.2999, -10.4337, -26.7572, -26.7572, -10.4337, -76.6203, -26.7572, -10.4337, -73.7615, -26.7572, -10.4337, -76.6203, -71.2999, -10.4337, -26.7572, -26.7572, -10.4337, -79.0804, -26.7572, -10.4337, -79.0804, -71.2999, -10.4337, -26.7572, -26.7572, -10.4337, -82.2434, -26.7572, -10.4337, -82.2434, -71.2999, -10.4337, -26.7572, -73.8239, -10.4337, -26.7572, -73.8239, -10.4337, -23.412498, -73.8239, -10.4337, -26.7572, -71.2999, -10.4337, -26.7572, -68.2489, -10.4337, -26.7572, -64.963196, -10.4337, -23.412498, -68.2489, -10.4337, -23.412498, -71.2999, -10.4337, -26.7572, -68.2489, -10.4337, -26.7572, -68.2489, -10.4337, -23.412498, -73.8239, -10.4337, -23.412498, -71.2999, -10.4337, -26.7572, -71.2999, -10.4337, -23.412498, -71.2999, -10.4337, -26.7572, -68.2489, -10.4337, -23.412498, -71.2999, -10.4337, -23.412498, -73.8239, -10.4337, -20.0679, -73.8239, -10.4337, -23.412498, -71.2999, -10.4337, -23.412498, -73.8239, -10.4337, -20.0679, -71.2999, -10.4337, -23.412498, -71.2999, -10.4337, -20.0908, -71.2999, -10.4337, -23.412498, -68.2489, -10.4337, -20.118599, -71.2999, -10.4337, -20.0908, -71.2999, -10.4337, -23.412498, -68.2489, -10.4337, -23.412498, -68.2489, -10.4337, -20.118599, -71.2999, -10.4337, -20.0908, -68.2489, -10.4337, -20.118599, -68.2489, -10.4337, -16.7739, -68.2489, -10.4337, -20.118599, -64.963196, -10.4337, -16.803799, -68.2489, -10.4337, -16.7739, -68.2489, -10.4337, -23.412498, -64.963196, -10.4337, -20.1484, -68.2489, -10.4337, -20.118599, -68.2489, -10.4337, -20.118599, -64.963196, -10.4337, -20.1484, -64.963196, -10.4337, -16.803799, -68.2489, -10.4337, -23.412498, -64.963196, -10.4337, -23.412498, -64.963196, -10.4337, -20.1484, -62.057194, -10.4337, -20.174799, -64.963196, -10.4337, -16.803799, -64.963196, -10.4337, -20.1484, -64.963196, -10.4337, -23.412498, -62.057194, -10.4337, -20.174799, -64.963196, -10.4337, -20.1484, -3.3446002, -1.458, -26.7572, -6.6709, -1.7958, -30.683697, -3.3313003, -1.7853999, -30.628498, -3.3446002, -1.458, -26.7572, -6.6893, -1.458, -26.7572, -6.6709, -1.7958, -30.683697, -6.6893, -3.6573, -40.6287, -6.6689, -4.8491, -44.313698, -6.6243997, -3.6573, -40.6287, -6.6689, -4.8491, -44.313698, -3.3446002, -3.6573, -40.6287, -6.6243997, -3.6573, -40.6287, -6.6893, -3.6573, -40.6287, -9.908199, -3.6573, -40.6287, -6.6689, -4.8491, -44.313698, -6.6689, -4.8491, -44.313698, -3.297, -3.6573, -40.6287, -3.3446002, -3.6573, -40.6287, -6.6689, -4.8491, -44.313698, -9.908199, -3.6573, -40.6287, -9.969999, -4.8431, -44.295097, -10.033899, -3.6573, -40.6287, -9.969999, -4.8431, -44.295097, -9.908199, -3.6573, -40.6287, -9.969999, -4.8431, -44.295097, -10.033899, -3.6573, -40.6287, -13.0655, -3.6573, -40.6287, -9.969999, -4.8431, -44.295097, -13.0655, -3.6573, -40.6287, -13.160999, -4.8373, -44.2772, -13.378601, -3.6573, -40.6287, -13.160999, -4.8373, -44.2772, -13.0655, -3.6573, -40.6287, -13.160999, -4.8373, -44.2772, -13.378601, -3.6573, -40.6287, -16.3079, -3.6573, -40.6287, -13.160999, -4.8373, -44.2772, -16.3079, -3.6573, -40.6287, -16.4166, -4.8314, -44.2589, -16.7232, -3.6573, -40.6287, -16.4166, -4.8314, -44.2589, -16.3079, -3.6573, -40.6287, -16.4166, -4.8314, -44.2589, -16.7232, -3.6573, -40.6287, -19.5913, -3.6573, -40.6287, -16.4166, -4.8314, -44.2589, -19.5913, -3.6573, -40.6287, -19.7153, -4.8254, -44.2403, -20.0679, -3.6573, -40.6287, -19.7153, -4.8254, -44.2403, -19.5913, -3.6573, -40.6287, -19.7153, -4.8254, -44.2403, -20.0679, -3.6573, -40.6287, -22.8746, -3.6573, -40.6287, -19.7153, -4.8254, -44.2403, -22.8746, -3.6573, -40.6287, -23.013899, -4.8194, -44.221798, -23.412498, -3.6573, -40.6287, -23.013899, -4.8194, -44.221798, -22.8746, -3.6573, -40.6287, -23.013899, -4.8194, -44.221798, -23.412498, -3.6573, -40.6287, -26.7572, -3.6573, -40.6287, -23.013899, -4.8194, -44.221798, -26.7572, -3.6573, -40.6287, -26.7572, -4.8125997, -44.200798, -26.7572, -4.8125997, -44.200798, -23.1474, -5.9323997, -47.6632, -23.013899, -4.8194, -44.221798, -26.7572, -4.8125997, -44.200798, -26.7572, -5.9270997, -47.646698, -23.1474, -5.9323997, -47.6632, -23.013899, -4.8194, -44.221798, -19.8335, -5.9372997, -47.6783, -19.7153, -4.8254, -44.2403, -23.013899, -4.8194, -44.221798, -23.1474, -5.9323997, -47.6632, -19.8335, -5.9372997, -47.6783, -19.7153, -4.8254, -44.2403, -19.8335, -5.9372997, -47.6783, -16.5194, -5.9421997, -47.6935, -19.7153, -4.8254, -44.2403, -16.5194, -5.9421997, -47.6935, -16.4166, -4.8314, -44.2589, -26.7572, -5.9270997, -47.646698, -23.2876, -7.1015997, -51.2782, -23.1474, -5.9323997, -47.6632, -26.7572, -5.9270997, -47.646698, -26.7572, -7.1011, -51.2768, -23.2876, -7.1015997, -51.2782, -26.7572, -7.1011, -51.2768, -23.412498, -8.1437, -54.500298, -23.2876, -7.1015997, -51.2782, -26.7572, -7.1011, -51.2768, -26.7572, -8.1437, -54.500298, -23.412498, -8.1437, -54.500298, -23.2876, -7.1015997, -51.2782, -23.412498, -8.1437, -54.500298, -20.0679, -8.1437, -54.500298, -23.1474, -5.9323997, -47.6632, -23.2876, -7.1015997, -51.2782, -19.9572, -7.1019998, -51.2795, -23.2876, -7.1015997, -51.2782, -20.0679, -8.1437, -54.500298, -19.9572, -7.1019998, -51.2795, -23.1474, -5.9323997, -47.6632, -19.9572, -7.1019998, -51.2795, -19.8335, -5.9372997, -47.6783, -19.9572, -7.1019998, -51.2795, -20.0679, -8.1437, -54.500298, -16.7232, -8.1437, -54.500298, -19.8335, -5.9372997, -47.6783, -19.9572, -7.1019998, -51.2795, -16.626799, -7.1024, -51.2808, -19.9572, -7.1019998, -51.2795, -16.7232, -8.1437, -54.500298, -16.626799, -7.1024, -51.2808, -19.8335, -5.9372997, -47.6783, -16.626799, -7.1024, -51.2808, -16.5194, -5.9421997, -47.6935, -16.626799, -7.1024, -51.2808, -16.7232, -8.1437, -54.500298, -13.4286, -8.1437, -54.500298, -16.5194, -5.9421997, -47.6935, -16.626799, -7.1024, -51.2808, -13.344299, -7.1029, -51.282097, -16.626799, -7.1024, -51.2808, -13.4286, -8.1437, -54.500298, -13.344299, -7.1029, -51.282097, -16.5194, -5.9421997, -47.6935, -13.344299, -7.1029, -51.282097, -13.250801, -5.947, -47.708397, -16.4166, -4.8314, -44.2589, -16.5194, -5.9421997, -47.6935, -13.250801, -5.947, -47.708397, -16.4166, -4.8314, -44.2589, -13.250801, -5.947, -47.708397, -13.160999, -4.8373, -44.2772, -13.4286, -8.1437, -54.500298, -10.0877, -7.1032996, -51.283398, -13.344299, -7.1029, -51.282097, -13.250801, -5.947, -47.708397, -13.344299, -7.1029, -51.282097, -10.0877, -7.1032996, -51.283398, -10.0877, -7.1032996, -51.283398, -13.4286, -8.1437, -54.500298, -13.378601, -8.1437, -54.500298, -10.0877, -7.1032996, -51.283398, -13.378601, -8.1437, -54.500298, -10.141901, -8.1437, -54.500298, -13.160999, -4.8373, -44.2772, -13.250801, -5.947, -47.708397, -10.0277, -5.9518, -47.723198, -13.250801, -5.947, -47.708397, -10.0877, -7.1032996, -51.283398, -10.0277, -5.9518, -47.723198, -13.160999, -4.8373, -44.2772, -10.0277, -5.9518, -47.723198, -9.969999, -4.8431, -44.295097, -10.141901, -8.1437, -54.500298, -6.7531, -7.1036997, -51.284798, -10.0877, -7.1032996, -51.283398, -10.0277, -5.9518, -47.723198, -10.0877, -7.1032996, -51.283398, -6.7531, -7.1036997, -51.284798, -6.7531, -7.1036997, -51.284798, -10.141901, -8.1437, -54.500298, -10.033899, -8.1437, -54.500298, -6.7531, -7.1036997, -51.284798, -10.033899, -8.1437, -54.500298, -6.7919, -8.1437, -54.500298, -6.6893, -8.1437, -54.500298, -6.7531, -7.1036997, -51.284798, -6.7919, -8.1437, -54.500298, -6.7531, -7.1036997, -51.284798, -6.6893, -8.1437, -54.500298, -3.4199996, -8.1437, -54.500298, -9.969999, -4.8431, -44.295097, -10.0277, -5.9518, -47.723198, -6.7102, -5.9567, -47.7383, -10.0277, -5.9518, -47.723198, -6.7531, -7.1036997, -51.284798, -6.7102, -5.9567, -47.7383, -9.969999, -4.8431, -44.295097, -6.7102, -5.9567, -47.7383, -6.6689, -4.8491, -44.313698, -6.7531, -7.1036997, -51.284798, -3.4199996, -8.1437, -54.500298, -3.3915, -7.1040998, -51.2861, -3.3915, -7.1040998, -51.2861, -6.7102, -5.9567, -47.7383, -6.7531, -7.1036997, -51.284798, -3.3446002, -8.1437, -54.500298, -3.3915, -7.1040998, -51.2861, -3.4199996, -8.1437, -54.500298, -3.3915, -7.1040998, -51.2861, -3.3446002, -8.1437, -54.500298, -0.048199892, -8.1437, -54.500298, -3.3915, -7.1040998, -51.2861, -3.3602, -5.9617, -47.753597, -6.7102, -5.9567, -47.7383, -3.3602, -5.9617, -47.753597, -6.6689, -4.8491, -44.313698, -6.7102, -5.9567, -47.7383, -3.3915, -7.1040998, -51.2861, -0.048199892, -8.1437, -54.500298, -0.03699994, -7.1046, -51.2874, -0.03699994, -7.1046, -51.2874, -3.3602, -5.9617, -47.753597, -3.3915, -7.1040998, -51.2861, 0, -8.1437, -54.500298, -0.03699994, -7.1046, -51.2874, -0.048199892, -8.1437, -54.500298, -0.03699994, -7.1046, -51.2874, 0, -8.1437, -54.500298, 3.3446, -8.1437, -54.500298, -3.3602, -5.9617, -47.753597, -3.3298001, -4.8552, -44.3325, -6.6689, -4.8491, -44.313698, -3.297, -3.6573, -40.6287, -6.6689, -4.8491, -44.313698, -3.3298001, -4.8552, -44.3325, -0.03699994, -7.1046, -51.2874, -0.024800062, -5.9666, -47.768898, -3.3602, -5.9617, -47.753597, -0.024800062, -5.9666, -47.768898, -3.3298001, -4.8552, -44.3325, -3.3602, -5.9617, -47.753597, -0.012900114, -4.8612, -44.351097, -3.297, -3.6573, -40.6287, -3.3298001, -4.8552, -44.3325, -0.024800062, -5.9666, -47.768898, -0.012900114, -4.8612, -44.351097, -3.3298001, -4.8552, -44.3325, -0.012900114, -4.8612, -44.351097, 0, -3.6573, -40.6287, -3.297, -3.6573, -40.6287, 3.3446, -7.105, -51.2888, -0.024800062, -5.9666, -47.768898, -0.03699994, -7.1046, -51.2874, -0.03699994, -7.1046, -51.2874, 3.3446, -8.1437, -54.500298, 3.3446, -7.105, -51.2888, 3.3446, -5.9716, -47.7842, -0.012900114, -4.8612, -44.351097, -0.024800062, -5.9666, -47.768898, 3.3446, -7.105, -51.2888, 3.3446, -5.9716, -47.7842, -0.024800062, -5.9666, -47.768898, 3.3446, -4.8673, -44.37, 0, -3.6573, -40.6287, -0.012900114, -4.8612, -44.351097, 3.3446, -5.9716, -47.7842, 3.3446, -4.8673, -44.37, -0.012900114, -4.8612, -44.351097, 3.3446, -4.8673, -44.37, 3.3446, -3.6573, -40.6287, 0, -3.6573, -40.6287, 3.3446, -8.1437, -54.500298, 6.6893, -7.1053996, -51.2901, 3.3446, -7.105, -51.2888, 6.6893, -7.1053996, -51.2901, 3.3446, -5.9716, -47.7842, 3.3446, -7.105, -51.2888, 3.3446, -8.1437, -54.500298, 6.6893, -8.1437, -54.500298, 6.6893, -7.1053996, -51.2901, 6.6893, -5.9765, -47.7995, 3.3446, -4.8673, -44.37, 3.3446, -5.9716, -47.7842, 6.6893, -7.1053996, -51.2901, 6.6893, -5.9765, -47.7995, 3.3446, -5.9716, -47.7842, 6.6893, -4.8733997, -44.388798, 3.3446, -3.6573, -40.6287, 3.3446, -4.8673, -44.37, 6.6893, -5.9765, -47.7995, 6.6893, -4.8733997, -44.388798, 3.3446, -4.8673, -44.37, 6.6893, -4.8733997, -44.388798, 6.6893, -3.6573, -40.6287, 3.3446, -3.6573, -40.6287, 6.6893, -8.1437, -54.500298, 10.033899, -7.1059, -51.291397, 6.6893, -7.1053996, -51.2901, 10.033899, -7.1059, -51.291397, 6.6893, -5.9765, -47.7995, 6.6893, -7.1053996, -51.2901, 6.6893, -8.1437, -54.500298, 10.033899, -8.1437, -54.500298, 10.033899, -7.1059, -51.291397, 13.378599, -8.1437, -54.500298, 10.033899, -7.1059, -51.291397, 10.033899, -8.1437, -54.500298, 10.033899, -7.1059, -51.291397, 13.378599, -8.1437, -54.500298, 13.444599, -8.1437, -54.500298, 10.033899, -4.8795, -44.4076, 6.6893, -3.6573, -40.6287, 6.6893, -4.8733997, -44.388798, 10.033899, -5.9814, -47.8148, 6.6893, -4.8733997, -44.388798, 6.6893, -5.9765, -47.7995, 10.033899, -7.1059, -51.291397, 10.033899, -5.9814, -47.8148, 6.6893, -5.9765, -47.7995, 10.033899, -5.9814, -47.8148, 10.033899, -4.8795, -44.4076, 6.6893, -4.8733997, -44.388798, 10.033899, -7.1059, -51.291397, 13.444599, -8.1437, -54.500298, 13.429298, -7.1063, -51.292797, 13.429298, -7.1063, -51.292797, 10.033899, -5.9814, -47.8148, 10.033899, -7.1059, -51.291397, 10.033899, -4.8795, -44.4076, 10.033899, -3.6573, -40.6287, 6.6893, -3.6573, -40.6287, 16.7232, -8.1437, -54.500298, 13.429298, -7.1063, -51.292797, 13.444599, -8.1437, -54.500298, 13.429298, -7.1063, -51.292797, 16.7232, -8.1437, -54.500298, 16.8188, -8.1437, -54.500298, 13.412899, -5.9863997, -47.8302, 10.033899, -4.8795, -44.4076, 10.033899, -5.9814, -47.8148, 13.429298, -7.1063, -51.292797, 13.412899, -5.9863997, -47.8302, 10.033899, -5.9814, -47.8148, 13.3967, -4.8856, -44.4265, 10.033899, -3.6573, -40.6287, 10.033899, -4.8795, -44.4076, 13.412899, -5.9863997, -47.8302, 13.3967, -4.8856, -44.4265, 10.033899, -4.8795, -44.4076, 13.3967, -4.8856, -44.4265, 13.378599, -3.6573, -40.6287, 10.033899, -3.6573, -40.6287, 13.429298, -7.1063, -51.292797, 16.8188, -8.1437, -54.500298, 16.7967, -7.1067, -51.294098, 16.7967, -7.1067, -51.294098, 13.412899, -5.9863997, -47.8302, 13.429298, -7.1063, -51.292797, 20.0679, -8.1437, -54.500298, 16.7967, -7.1067, -51.294098, 16.8188, -8.1437, -54.500298, 16.7967, -7.1067, -51.294098, 20.0679, -8.1437, -54.500298, 20.1975, -8.1437, -54.500298, 16.7967, -7.1067, -51.294098, 20.1975, -8.1437, -54.500298, 20.1675, -7.1071997, -51.295498, 16.7967, -7.1067, -51.294098, 16.772999, -5.9914, -47.8456, 13.412899, -5.9863997, -47.8302, 20.1675, -7.1071997, -51.295498, 16.772999, -5.9914, -47.8456, 16.7967, -7.1067, -51.294098, 16.772999, -5.9914, -47.8456, 13.3967, -4.8856, -44.4265, 13.412899, -5.9863997, -47.8302, 20.1675, -7.1071997, -51.295498, 20.135399, -5.9964, -47.861, 16.772999, -5.9914, -47.8456, 16.7495, -4.8917, -44.445297, 13.378599, -3.6573, -40.6287, 13.3967, -4.8856, -44.4265, 16.772999, -5.9914, -47.8456, 16.7495, -4.8917, -44.445297, 13.3967, -4.8856, -44.4265, 20.135399, -5.9964, -47.861, 16.7495, -4.8917, -44.445297, 16.772999, -5.9914, -47.8456, 16.7495, -4.8917, -44.445297, 16.7232, -3.6573, -40.6287, 13.378599, -3.6573, -40.6287, 20.135399, -5.9964, -47.861, 20.103699, -4.8978, -44.4642, 16.7495, -4.8917, -44.445297, 20.103699, -4.8978, -44.4642, 16.7232, -3.6573, -40.6287, 16.7495, -4.8917, -44.445297, 20.103699, -4.8978, -44.4642, 20.0679, -3.6573, -40.6287, 16.7232, -3.6573, -40.6287, 16.7232, 0, 10.033899, 17.4912, 0.0034999999, 7.4080997, 20.0679, 0, 10.033899, 12.824999, 2.8544, 7.9234, 11.2783985, 2.1174998, 3.3446, 12.705599, 1.0321, 4.0927, 12.824999, 2.8544, 7.9234, 11.173899, 3.3332, 6.6554, 11.2783985, 2.1174998, 3.3446, 10.033899, 3.1929998, 0, 13.249599, 2.9931998, -3.4737, 11.278099, 2.3706, 0, 10.033899, 3.1929998, 0, 10.033899, 3.2212, -2.2307, 13.249599, 2.9931998, -3.4737, 20.613499, 1.0832, -3.1922002, 22.5396, -0.014599999, -3.3446002, 20.5402, -0.0411, -1.7683, 20.613499, 1.0832, -3.1922002, 21.09, -0.0218, -4.9179, 22.5396, -0.014599999, -3.3446002, 15.485399, 1.0792, -1.6708999, 14.217899, 2.8779, -3.3838, 15.6619, 2.2603, -2.7998996, 20.613499, 1.0832, -3.1922002, 15.485399, 1.0792, -1.6708999, 15.6619, 2.2603, -2.7998996, 20.613499, 1.0832, -3.1922002, 20.5402, -0.0411, -1.7683, 15.485399, 1.0792, -1.6708999, 20.5402, -0.0411, -1.7683, 23.412498, 0, 0, 20.2292, 0.030499998, -0.05219996, 23.412498, 0, 0, 20.5402, -0.0411, -1.7683, 22.5396, -0.014599999, -3.3446002, 23.412498, 0, 0, 22.5396, -0.014599999, -3.3446002, 23.412498, 0, -3.3446002, 21.09, -0.0218, -4.9179, 23.412498, 0, -6.6893, 22.5396, -0.014599999, -3.3446002, 21.09, -0.0218, -4.9179, 21.2349, 0, -6.6893, 23.412498, 0, -6.6893, 15.485399, 1.0792, -1.6708999, 20.2292, 0.030499998, -0.05219996, 14.261799, 0.9069, 1.1807, 14.217899, 2.8779, -3.3838, 11.278099, 2.3706, 0, 13.249599, 2.9931998, -3.4737, 11.278099, 2.3706, 0, 14.217899, 2.8779, -3.3838, 15.485399, 1.0792, -1.6708999, 11.278099, 2.3706, 0, 15.485399, 1.0792, -1.6708999, 14.261799, 0.9069, 1.1807, 15.485399, 1.0792, -1.6708999, 20.5402, -0.0411, -1.7683, 20.2292, 0.030499998, -0.05219996, 10.033899, 3.3913999, 4.2546997, 11.2783985, 2.1174998, 3.3446, 11.173899, 3.3332, 6.6554, 10.033899, 3.3913999, 4.2546997, 10.033899, 3.1929998, 3.3446, 11.2783985, 2.1174998, 3.3446, 6.6893, 3.3913999, 3.3446, 10.033899, 3.1929998, 3.3446, 10.033899, 3.3913999, 4.2546997, 10.033899, 3.2212, -2.2307, 10.033899, 3.1929998, 0, 6.6893, 3.3913999, 0, 14.0257, 0.0442, 7.3252997, 12.824999, 2.8544, 7.9234, 12.705599, 1.0321, 4.0927, 14.0257, 0.0442, 7.3252997, 13.175798, 2.4743, 8.042, 12.824999, 2.8544, 7.9234, 26.7582, -0.83089995, 6.6893, 26.7582, -1.7623999, 10.033899, 23.412498, -0.35529998, 10.033899, 23.826399, -8.175799, -54.500298, 26.7572, -7.3550997, -51.2602, 23.7466, -7.1323, -51.2931, 23.826399, -8.175799, -54.500298, 26.7572, -8.403, -54.500298, 26.7572, -7.3550997, -51.2602, 26.7572, -7.3550997, -51.2602, 23.6614, -6.0197997, -47.8736, 23.7466, -7.1323, -51.2931, 26.7572, -7.3550997, -51.2602, 26.7572, -6.2504, -47.8447, 23.6614, -6.0197997, -47.8736, 26.7572, -6.2504, -47.8447, 23.577, -4.916, -44.481, 23.6614, -6.0197997, -47.8736, 26.7572, -6.2504, -47.8447, 26.7572, -5.1514, -44.446598, 23.577, -4.916, -44.481, 26.7572, -5.1514, -44.446598, 23.480999, -3.6625998, -40.6287, 23.577, -4.916, -44.481, 26.7572, -5.1514, -44.446598, 26.7572, -3.9166, -40.6287, 23.480999, -3.6625998, -40.6287, 23.808699, -10.464399, -68.371895, 26.7572, -9.876599, -63.426796, 23.8148, -9.6761, -63.593796, 23.808699, -10.464399, -68.371895, 26.7572, -10.693, -68.371895, 26.7572, -9.876599, -63.426796, 26.7572, -9.876599, -63.426796, 23.819199, -9.111899, -60.1742, 23.8148, -9.6761, -63.593796, 26.7572, -9.876599, -63.426796, 26.7572, -9.312699, -60.011097, 23.819199, -9.111899, -60.1742, 26.7572, -9.312699, -60.011097, 23.8225, -8.6786995, -57.548298, 23.819199, -9.111899, -60.1742, 26.7572, -9.312699, -60.011097, 26.7572, -8.881, -57.3956, 23.8225, -8.6786995, -57.548298, 26.7572, -8.881, -57.3956, 23.826399, -8.175799, -54.500298, 23.8225, -8.6786995, -57.548298, 26.7572, -8.881, -57.3956, 26.7572, -8.403, -54.500298, 23.826399, -8.175799, -54.500298, 23.480999, -3.6625998, -40.6287, 26.7572, -3.2101, -37.2077, 23.4646, -2.985, -37.2919, 23.480999, -3.6625998, -40.6287, 26.7572, -3.9166, -40.6287, 26.7572, -3.2101, -37.2077, 23.412498, -8.1437, -54.500298, 20.1675, -7.1071997, -51.295498, 20.1975, -8.1437, -54.500298, 20.1675, -7.1071997, -51.295498, 23.412498, -8.1437, -54.500298, 23.826399, -8.175799, -54.500298, 20.1675, -7.1071997, -51.295498, 23.826399, -8.175799, -54.500298, 23.7466, -7.1323, -51.2931, 23.412498, -10.4337, -68.371895, 20.112099, -9.6529, -63.642, 20.0679, -10.4337, -68.371895, 20.112099, -9.6529, -63.642, 23.412498, -10.4337, -68.371895, 23.808699, -10.464399, -68.371895, 20.112099, -9.6529, -63.642, 23.808699, -10.464399, -68.371895, 23.8148, -9.6761, -63.593796, 23.412498, -3.6573, -40.6287, 20.0679, -2.9615998, -37.315, 20.0679, -3.6573, -40.6287, 20.0679, -2.9615998, -37.315, 23.412498, -3.6573, -40.6287, 23.480999, -3.6625998, -40.6287, 20.0679, -2.9615998, -37.315, 23.480999, -3.6625998, -40.6287, 23.4646, -2.985, -37.2919, 16.7232, -3.6573, -40.6287, 20.0679, -2.9615998, -37.315, 16.7232, -2.9859998, -37.3232, 16.7232, -3.6573, -40.6287, 20.0679, -3.6573, -40.6287, 20.0679, -2.9615998, -37.315, 13.378599, -3.6573, -40.6287, 16.7232, -2.9859998, -37.3232, 13.378599, -2.9889998, -37.3379, 13.378599, -3.6573, -40.6287, 16.7232, -3.6573, -40.6287, 16.7232, -2.9859998, -37.3232, 16.7232, -2.9859998, -37.3232, 13.378599, -2.3234, -34.0607, 13.378599, -2.9889998, -37.3379, 16.7232, -2.9859998, -37.3232, 16.7232, -2.3191, -34.0398, 13.378599, -2.3234, -34.0607, 16.7232, -2.3191, -34.0398, 13.378599, -1.582, -30.410397, 13.378599, -2.3234, -34.0607, 16.7232, -2.3191, -34.0398, 16.7232, -1.5709, -30.3554, 13.378599, -1.582, -30.410397, 10.033899, -3.6573, -40.6287, 13.378599, -2.9889998, -37.3379, 10.033899, -2.9973998, -37.350998, 10.033899, -3.6573, -40.6287, 13.378599, -3.6573, -40.6287, 13.378599, -2.9889998, -37.3379, 6.6893, -3.6573, -40.6287, 10.033899, -2.9973998, -37.350998, 6.6893, -3.0333998, -37.355698, 6.6893, -3.6573, -40.6287, 10.033899, -3.6573, -40.6287, 10.033899, -2.9973998, -37.350998, 3.3446, -3.6573, -40.6287, 6.6893, -3.0333998, -37.355698, 3.3446, -3.0307, -37.3722, 3.3446, -3.6573, -40.6287, 6.6893, -3.6573, -40.6287, 6.6893, -3.0333998, -37.355698, 0, -3.6573, -40.6287, 3.3446, -3.0307, -37.3722, 0, -3.0496, -37.3821, 0, -3.6573, -40.6287, 3.3446, -3.6573, -40.6287, 3.3446, -3.0307, -37.3722, -3.297, -3.6573, -40.6287, 0, -3.0496, -37.3821, -3.3081002, -3.0523, -37.396698, -3.297, -3.6573, -40.6287, 0, -3.6573, -40.6287, 0, -3.0496, -37.3821, -3.3081002, -3.0523, -37.396698, -3.3446002, -3.6573, -40.6287, -3.297, -3.6573, -40.6287, -3.3081002, -3.0523, -37.396698, -6.6243997, -3.6573, -40.6287, -3.3446002, -3.6573, -40.6287, -6.6243997, -3.6573, -40.6287, -3.3081002, -3.0523, -37.396698, -6.6394, -3.0551, -37.4115, -6.6547, -2.4452, -34.1534, -6.6394, -3.0551, -37.4115, -3.3081002, -3.0523, -37.396698, 0, -3.0496, -37.3821, -3.3193002, -2.4413, -34.1324, -3.3081002, -3.0523, -37.396698, -6.6547, -2.4452, -34.1534, -3.3081002, -3.0523, -37.396698, -3.3193002, -2.4413, -34.1324, -6.6709, -1.7958, -30.683697, -6.6547, -2.4452, -34.1534, -3.3193002, -2.4413, -34.1324, 0, -3.0496, -37.3821, 0, -2.4373999, -34.1115, -3.3193002, -2.4413, -34.1324, -6.6709, -1.7958, -30.683697, -3.3193002, -2.4413, -34.1324, -3.3313003, -1.7853999, -30.628498, 0, -2.4373999, -34.1115, -3.3313003, -1.7853999, -30.628498, -3.3193002, -2.4413, -34.1324, 0, -2.4373999, -34.1115, 0, -1.7751, -30.573498, -3.3313003, -1.7853999, -30.628498, -6.6893, -1.458, -26.7572, -9.9988, -2.089, -30.630497, -6.6709, -1.7958, -30.683697, -6.6893, -1.458, -26.7572, -10.033899, -1.8786, -26.7572, -9.9988, -2.089, -30.630497, -10.033899, -1.8786, -26.7572, -13.297701, -2.9966, -30.341599, -9.9988, -2.089, -30.630497, -10.033899, -1.8786, -26.7572, -13.378601, -2.7663999, -26.7572, -13.297701, -2.9966, -30.341599, -13.378601, -2.7663999, -26.7572, -16.6169, -3.2373998, -30.3083, -13.297701, -2.9966, -30.341599, -13.378601, -2.7663999, -26.7572, -16.7232, -3.0929, -26.7572, -16.6169, -3.2373998, -30.3083, -16.7232, -3.0929, -26.7572, -19.9439, -3.2396998, -30.366299, -16.6169, -3.2373998, -30.3083, -19.9439, -3.2396998, -30.366299, -16.5094, -3.3834999, -33.898598, -16.6169, -3.2373998, -30.3083, -16.7232, -3.0929, -26.7572, -20.0679, -3.0929, -26.7572, -19.9439, -3.2396998, -30.366299, -19.9439, -3.2396998, -30.366299, -19.8217, -3.3844, -33.920498, -16.5094, -3.3834999, -33.898598, -20.0679, -3.0929, -26.7572, -23.2703, -3.2421, -30.424297, -19.9439, -3.2396998, -30.366299, -23.2703, -3.2421, -30.424297, -19.8217, -3.3844, -33.920498, -19.9439, -3.2396998, -30.366299, -20.0679, -3.0929, -26.7572, -23.412498, -3.0929, -26.7572, -23.2703, -3.2421, -30.424297, -19.8217, -3.3844, -33.920498, -16.4072, -3.5224, -37.3144, -16.5094, -3.3834999, -33.898598, -23.412498, -3.0929, -26.7572, -26.7572, -3.2445998, -30.4851, -23.2703, -3.2421, -30.424297, -23.412498, -3.0929, -26.7572, -26.7572, -3.0929, -26.7572, -26.7572, -3.2445998, -30.4851, -26.7572, -3.2445998, -30.4851, -23.1339, -3.3852, -33.942398, -23.2703, -3.2421, -30.424297, -23.2703, -3.2421, -30.424297, -23.1339, -3.3852, -33.942398, -19.8217, -3.3844, -33.920498, -26.7572, -3.2445998, -30.4851, -26.7572, -3.3862, -33.9664, -23.1339, -3.3852, -33.942398, -16.4072, -3.5224, -37.3144, -16.7232, -3.6573, -40.6287, -16.3079, -3.6573, -40.6287, -16.4072, -3.5224, -37.3144, -19.5913, -3.6573, -40.6287, -16.7232, -3.6573, -40.6287, -19.8217, -3.3844, -33.920498, -19.7046, -3.5231, -37.3297, -16.4072, -3.5224, -37.3144, -19.5913, -3.6573, -40.6287, -16.4072, -3.5224, -37.3144, -19.7046, -3.5231, -37.3297, -23.1339, -3.3852, -33.942398, -19.7046, -3.5231, -37.3297, -19.8217, -3.3844, -33.920498, -19.7046, -3.5231, -37.3297, -20.0679, -3.6573, -40.6287, -19.5913, -3.6573, -40.6287, -19.7046, -3.5231, -37.3297, -22.8746, -3.6573, -40.6287, -20.0679, -3.6573, -40.6287, -23.1339, -3.3852, -33.942398, -23.0019, -3.5237, -37.3449, -19.7046, -3.5231, -37.3297, -22.8746, -3.6573, -40.6287, -19.7046, -3.5231, -37.3297, -23.0019, -3.5237, -37.3449, -26.7572, -3.3862, -33.9664, -23.0019, -3.5237, -37.3449, -23.1339, -3.3852, -33.942398, -23.0019, -3.5237, -37.3449, -23.412498, -3.6573, -40.6287, -22.8746, -3.6573, -40.6287, -26.7572, -3.3862, -33.9664, -26.7572, -3.5244, -37.3623, -23.0019, -3.5237, -37.3449, -23.0019, -3.5237, -37.3449, -26.7572, -3.6573, -40.6287, -23.412498, -3.6573, -40.6287, -26.7572, -3.6573, -40.6287, -23.0019, -3.5237, -37.3449, -26.7572, -3.5244, -37.3623, -13.297701, -2.9966, -30.341599, -9.9673, -2.6348999, -34.110798, -9.9988, -2.089, -30.630497, -13.297701, -2.9966, -30.341599, -13.2167, -3.2271, -33.93, -9.9673, -2.6348999, -34.110798, 6.6893, -1.7218, -30.475399, 3.3446, -0.98829997, -26.7572, 3.3446, -1.7156, -30.536999, 6.6893, -1.7218, -30.475399, 6.6893, -1.0129, -26.7572, 3.3446, -0.98829997, -26.7572, 26.7573, -1.7447, -30.111198, 23.412498, -1.1158999, -26.7572, 23.429699, -1.5537, -30.243198, 26.7573, -1.7447, -30.111198, 26.7573, -1.3222, -26.7572, 23.412498, -1.1158999, -26.7572, -9.9988, -2.089, -30.630497, -6.6547, -2.4452, -34.1534, -6.6709, -1.7958, -30.683697, -9.9988, -2.089, -30.630497, -9.9673, -2.6348999, -34.110798, -6.6547, -2.4452, -34.1534, 10.033899, -1.61, -30.458897, 6.6893, -1.0129, -26.7572, 6.6893, -1.7218, -30.475399, 10.033899, -1.61, -30.458897, 10.033899, -0.8648, -26.7572, 6.6893, -1.0129, -26.7572, 3.3446, -1.7156, -30.536999, 0, -1.0608, -26.7572, 0, -1.7751, -30.573498, 3.3446, -1.7156, -30.536999, 3.3446, -0.98829997, -26.7572, 0, -1.0608, -26.7572, 23.429699, -1.5537, -30.243198, 20.0679, -1.1922, -26.7572, 20.0679, -1.4942, -30.325497, 23.429699, -1.5537, -30.243198, 23.412498, -1.1158999, -26.7572, 20.0679, -1.1922, -26.7572, 16.7232, -1.5709, -30.3554, 16.7232, -1.3382, -26.7572, 13.378599, -1.582, -30.410397, 13.378599, -1.582, -30.410397, 10.033899, -0.8648, -26.7572, 10.033899, -1.61, -30.458897, 13.378599, -1.582, -30.410397, 13.378599, -0.8401, -26.7572, 10.033899, -0.8648, -26.7572, 0, -1.7751, -30.573498, -3.3446002, -1.458, -26.7572, -3.3313003, -1.7853999, -30.628498, 0, -1.7751, -30.573498, 0, -1.0608, -26.7572, -3.3446002, -1.458, -26.7572, 20.0679, -1.4942, -30.325497, 16.7232, -1.3382, -26.7572, 16.7232, -1.5709, -30.3554, 20.0679, -1.4942, -30.325497, 20.0679, -1.1922, -26.7572, 16.7232, -1.3382, -26.7572, -16.6169, -3.2373998, -30.3083, -13.2167, -3.2271, -33.93, -13.297701, -2.9966, -30.341599, -16.6169, -3.2373998, -30.3083, -16.5094, -3.3834999, -33.898598, -13.2167, -3.2271, -33.93, 6.6893, -2.4083, -34.0766, 3.3446, -1.7156, -30.536999, 3.3446, -2.4013999, -34.1014, 6.6893, -2.4083, -34.0766, 6.6893, -1.7218, -30.475399, 3.3446, -1.7156, -30.536999, 26.7572, -2.5274, -33.9013, 23.429699, -1.5537, -30.243198, 23.4483, -2.3157, -33.9959, 26.7572, -2.5274, -33.9013, 26.7573, -1.7447, -30.111198, 23.429699, -1.5537, -30.243198, 23.4483, -2.3157, -33.9959, 20.0679, -1.4942, -30.325497, 20.0679, -2.2726, -34.0333, 23.4483, -2.3157, -33.9959, 23.429699, -1.5537, -30.243198, 20.0679, -1.4942, -30.325497, -9.9673, -2.6348999, -34.110798, -6.6394, -3.0551, -37.4115, -6.6547, -2.4452, -34.1534, -9.9673, -2.6348999, -34.110798, -9.9375, -3.1504998, -37.3978, -6.6394, -3.0551, -37.4115, 3.3446, -2.4013999, -34.1014, 0, -1.7751, -30.573498, 0, -2.4373999, -34.1115, 3.3446, -2.4013999, -34.1014, 3.3446, -1.7156, -30.536999, 0, -1.7751, -30.573498, -16.5094, -3.3834999, -33.898598, -13.1401005, -3.445, -37.3228, -13.2167, -3.2271, -33.93, -16.5094, -3.3834999, -33.898598, -16.4072, -3.5224, -37.3144, -13.1401005, -3.445, -37.3228, 10.033899, -2.3385, -34.078, 6.6893, -1.7218, -30.475399, 6.6893, -2.4083, -34.0766, 10.033899, -2.3385, -34.078, 10.033899, -1.61, -30.458897, 6.6893, -1.7218, -30.475399, 13.378599, -2.3234, -34.0607, 10.033899, -1.61, -30.458897, 10.033899, -2.3385, -34.078, 13.378599, -2.3234, -34.0607, 13.378599, -1.582, -30.410397, 10.033899, -1.61, -30.458897, 20.0679, -2.2726, -34.0333, 16.7232, -1.5709, -30.3554, 16.7232, -2.3191, -34.0398, 20.0679, -2.2726, -34.0333, 20.0679, -1.4942, -30.325497, 16.7232, -1.5709, -30.3554, -13.2167, -3.2271, -33.93, -9.9375, -3.1504998, -37.3978, -9.9673, -2.6348999, -34.110798, -13.2167, -3.2271, -33.93, -13.1401005, -3.445, -37.3228, -9.9375, -3.1504998, -37.3978, -16.3079, -3.6573, -40.6287, -13.1401005, -3.445, -37.3228, -16.4072, -3.5224, -37.3144, -13.1401005, -3.445, -37.3228, -16.3079, -3.6573, -40.6287, -13.378601, -3.6573, -40.6287, -13.1401005, -3.445, -37.3228, -13.378601, -3.6573, -40.6287, -13.0655, -3.6573, -40.6287, -13.0655, -3.6573, -40.6287, -9.9375, -3.1504998, -37.3978, -13.1401005, -3.445, -37.3228, -9.9375, -3.1504998, -37.3978, -13.0655, -3.6573, -40.6287, -10.033899, -3.6573, -40.6287, -9.9375, -3.1504998, -37.3978, -10.033899, -3.6573, -40.6287, -9.908199, -3.6573, -40.6287, 6.6893, -3.0333998, -37.355698, 3.3446, -2.4013999, -34.1014, 3.3446, -3.0307, -37.3722, 6.6893, -3.0333998, -37.355698, 6.6893, -2.4083, -34.0766, 3.3446, -2.4013999, -34.1014, 10.033899, -2.9973998, -37.350998, 6.6893, -2.4083, -34.0766, 6.6893, -3.0333998, -37.355698, 10.033899, -2.9973998, -37.350998, 10.033899, -2.3385, -34.078, 6.6893, -2.4083, -34.0766, 3.3446, -3.0307, -37.3722, 0, -2.4373999, -34.1115, 0, -3.0496, -37.3821, 3.3446, -3.0307, -37.3722, 3.3446, -2.4013999, -34.1014, 0, -2.4373999, -34.1115, 23.4646, -2.985, -37.2919, 20.0679, -2.2726, -34.0333, 20.0679, -2.9615998, -37.315, 23.4646, -2.985, -37.2919, 23.4483, -2.3157, -33.9959, 20.0679, -2.2726, -34.0333, 26.7572, -3.2101, -37.2077, 23.4483, -2.3157, -33.9959, 23.4646, -2.985, -37.2919, 26.7572, -3.2101, -37.2077, 26.7572, -2.5274, -33.9013, 23.4483, -2.3157, -33.9959, 13.378599, -2.9889998, -37.3379, 10.033899, -2.3385, -34.078, 10.033899, -2.9973998, -37.350998, 13.378599, -2.9889998, -37.3379, 13.378599, -2.3234, -34.0607, 10.033899, -2.3385, -34.078, 20.0679, -2.9615998, -37.315, 16.7232, -2.3191, -34.0398, 16.7232, -2.9859998, -37.3232, 20.0679, -2.9615998, -37.315, 20.0679, -2.2726, -34.0333, 16.7232, -2.3191, -34.0398, -9.908199, -3.6573, -40.6287, -6.6394, -3.0551, -37.4115, -9.9375, -3.1504998, -37.3978, -6.6394, -3.0551, -37.4115, -9.908199, -3.6573, -40.6287, -6.6893, -3.6573, -40.6287, -6.6394, -3.0551, -37.4115, -6.6893, -3.6573, -40.6287, -6.6243997, -3.6573, -40.6287, 23.480999, -3.6625998, -40.6287, 20.103699, -4.8978, -44.4642, 23.577, -4.916, -44.481, 20.103699, -4.8978, -44.4642, 23.480999, -3.6625998, -40.6287, 23.412498, -3.6573, -40.6287, 20.103699, -4.8978, -44.4642, 23.412498, -3.6573, -40.6287, 20.0679, -3.6573, -40.6287, 23.6614, -6.0197997, -47.8736, 20.103699, -4.8978, -44.4642, 20.135399, -5.9964, -47.861, 23.6614, -6.0197997, -47.8736, 23.577, -4.916, -44.481, 20.103699, -4.8978, -44.4642, 23.7466, -7.1323, -51.2931, 20.135399, -5.9964, -47.861, 20.1675, -7.1071997, -51.295498, 23.7466, -7.1323, -51.2931, 23.6614, -6.0197997, -47.8736, 20.135399, -5.9964, -47.861, 23.826399, -8.175799, -54.500298, 20.1689, -8.6489, -57.5607, 23.8225, -8.6786995, -57.548298, 20.1689, -8.6489, -57.5607, 23.826399, -8.175799, -54.500298, 23.412498, -8.1437, -54.500298, 20.1689, -8.6489, -57.5607, 23.412498, -8.1437, -54.500298, 20.1975, -8.1437, -54.500298, 23.819199, -9.111899, -60.1742, 20.1689, -8.6489, -57.5607, 20.1442, -9.085899, -60.207497, 23.819199, -9.111899, -60.1742, 23.8225, -8.6786995, -57.548298, 20.1689, -8.6489, -57.5607, 23.8148, -9.6761, -63.593796, 20.1442, -9.085899, -60.207497, 20.112099, -9.6529, -63.642, 23.8148, -9.6761, -63.593796, 23.819199, -9.111899, -60.1742, 20.1442, -9.085899, -60.207497, -26.7572, -10.4337, -70.9319, -23.4177, -10.4499, -73.721596, -23.4177, -10.4499, -70.8944, -23.4177, -10.4499, -73.721596, -20.0768, -10.466, -70.856895, -23.4177, -10.4499, -70.8944, -26.7572, -10.4337, -70.9319, -26.7572, -10.4337, -73.7615, -23.4177, -10.4499, -73.721596, -23.4177, -10.4499, -73.721596, -20.0768, -10.466, -73.6818, -20.0768, -10.466, -70.856895, -20.0768, -10.466, -73.6818, -16.734499, -10.4822, -70.8193, -20.0768, -10.466, -70.856895, -26.7572, -10.4337, -73.7615, -23.4177, -10.4499, -76.5937, -23.4177, -10.4499, -73.721596, -23.4177, -10.4499, -76.5937, -20.0768, -10.466, -73.6818, -23.4177, -10.4499, -73.721596, -26.7572, -10.4337, -73.7615, -26.7572, -10.4337, -76.6203, -23.4177, -10.4499, -76.5937, -26.7572, -10.4337, -76.6203, -23.4177, -10.4499, -79.1102, -23.4177, -10.4499, -76.5937, -26.7572, -10.4337, -76.6203, -26.7572, -10.4337, -79.0804, -23.4177, -10.4499, -79.1102, -23.4177, -10.4499, -79.1102, -20.0768, -10.466, -76.567, -23.4177, -10.4499, -76.5937, -23.4177, -10.4499, -76.5937, -20.0768, -10.466, -76.567, -20.0768, -10.466, -73.6818, -23.4177, -10.4499, -79.1102, -20.0768, -10.466, -79.14, -20.0768, -10.466, -76.567, -20.0768, -10.466, -73.6818, -16.734499, -10.4822, -73.6419, -16.734499, -10.4822, -70.8193, -20.0768, -10.466, -76.567, -16.734499, -10.4822, -73.6419, -20.0768, -10.466, -73.6818, -16.734499, -10.4822, -73.6419, -13.3908, -10.4984, -70.7818, -16.734499, -10.4822, -70.8193, -20.0768, -10.466, -79.14, -16.734499, -10.4822, -76.5403, -20.0768, -10.466, -76.567, -20.0768, -10.466, -76.567, -16.734499, -10.4822, -76.5403, -16.734499, -10.4822, -73.6419, -20.0768, -10.466, -79.14, -16.734499, -10.4822, -79.1699, -16.734499, -10.4822, -76.5403, -16.734499, -10.4822, -73.6419, -13.3909, -10.4984, -73.602, -13.3908, -10.4984, -70.7818, -16.734499, -10.4822, -76.5403, -13.3909, -10.4984, -73.602, -16.734499, -10.4822, -73.6419, -13.3909, -10.4984, -73.602, -10.045801, -10.5146, -70.7442, -13.3908, -10.4984, -70.7818, -16.734499, -10.4822, -79.1699, -13.3909, -10.4984, -76.513596, -16.734499, -10.4822, -76.5403, -16.734499, -10.4822, -76.5403, -13.3909, -10.4984, -76.513596, -13.3909, -10.4984, -73.602, -16.734499, -10.4822, -79.1699, -13.3909, -10.4984, -79.1997, -13.3909, -10.4984, -76.513596, -13.3909, -10.4984, -73.602, -10.045801, -10.5146, -73.562195, -10.045801, -10.5146, -70.7442, -13.3909, -10.4984, -76.513596, -10.045801, -10.5146, -73.562195, -13.3909, -10.4984, -73.602, -10.045801, -10.5146, -73.562195, -6.6993003, -10.5309, -70.7066, -10.045801, -10.5146, -70.7442, -13.3909, -10.4984, -79.1997, -10.045801, -10.5146, -76.4869, -13.3909, -10.4984, -76.513596, -13.3909, -10.4984, -76.513596, -10.045801, -10.5146, -76.4869, -10.045801, -10.5146, -73.562195, -13.3909, -10.4984, -79.1997, -10.045801, -10.5146, -79.2295, -10.045801, -10.5146, -76.4869, -10.045801, -10.5146, -73.562195, -6.6993003, -10.5309, -73.5222, -6.6993003, -10.5309, -70.7066, -10.045801, -10.5146, -76.4869, -6.6993003, -10.5309, -73.5222, -10.045801, -10.5146, -73.562195, -6.6993003, -10.5309, -73.5222, -3.3514, -10.5471, -70.669, -6.6993003, -10.5309, -70.7066, -10.045801, -10.5146, -79.2295, -6.6993003, -10.5309, -76.4602, -10.045801, -10.5146, -76.4869, -10.045801, -10.5146, -76.4869, -6.6993003, -10.5309, -76.4602, -6.6993003, -10.5309, -73.5222, -10.045801, -10.5146, -79.2295, -6.6993003, -10.5309, -79.2594, -6.6993003, -10.5309, -76.4602, -6.6993003, -10.5309, -73.5222, -3.3514, -10.5471, -73.4823, -3.3514, -10.5471, -70.669, -6.6993003, -10.5309, -76.4602, -3.3514, -10.5471, -73.4823, -6.6993003, -10.5309, -73.5222, -3.3514, -10.5471, -73.4823, -0.0020999908, -10.5633, -70.6314, -3.3514, -10.5471, -70.669, -6.6993003, -10.5309, -79.2594, -3.3514, -10.5471, -76.433395, -6.6993003, -10.5309, -76.4602, -6.6993003, -10.5309, -76.4602, -3.3514, -10.5471, -76.433395, -3.3514, -10.5471, -73.4823, -6.6993003, -10.5309, -79.2594, -3.3514, -10.5471, -79.2893, -3.3514, -10.5471, -76.433395, -3.3514, -10.5471, -73.4823, -0.0020999908, -10.5633, -73.4424, -0.0020999908, -10.5633, -70.6314, -3.3514, -10.5471, -76.433395, -0.0020999908, -10.5633, -73.4424, -3.3514, -10.5471, -73.4823, -0.0020999908, -10.5633, -73.4424, 3.3486998, -10.579599, -70.5937, -0.0020999908, -10.5633, -70.6314, -3.3514, -10.5471, -79.2893, -0.0020999908, -10.5633, -76.4067, -3.3514, -10.5471, -76.433395, -3.3514, -10.5471, -76.433395, -0.0020999908, -10.5633, -76.4067, -0.0020999908, -10.5633, -73.4424, -3.3514, -10.5471, -79.2893, -0.0020999908, -10.5633, -79.3192, -0.0020999908, -10.5633, -76.4067, -0.0020999908, -10.5633, -73.4424, 3.3486998, -10.579599, -73.4024, 3.3486998, -10.579599, -70.5937, -0.0020999908, -10.5633, -76.4067, 3.3486998, -10.579599, -73.4024, -0.0020999908, -10.5633, -73.4424, 3.3486998, -10.579599, -73.4024, 6.7008, -10.595799, -70.5561, 3.3486998, -10.579599, -70.5937, -0.0020999908, -10.5633, -79.3192, 3.3486998, -10.579599, -76.3799, -0.0020999908, -10.5633, -76.4067, -0.0020999908, -10.5633, -76.4067, 3.3486998, -10.579599, -76.3799, 3.3486998, -10.579599, -73.4024, -0.0020999908, -10.5633, -79.3192, 3.3486998, -10.579599, -79.3491, 3.3486998, -10.579599, -76.3799, 3.3486998, -10.579599, -73.4024, 6.7008, -10.595799, -73.3624, 6.7008, -10.595799, -70.5561, 3.3486998, -10.579599, -76.3799, 6.7008, -10.595799, -73.3624, 3.3486998, -10.579599, -73.4024, 6.7008, -10.595799, -73.3624, 10.054299, -10.6119995, -70.518394, 6.7008, -10.595799, -70.5561, 3.3486998, -10.579599, -79.3491, 6.7008, -10.595799, -76.353195, 3.3486998, -10.579599, -76.3799, 3.3486998, -10.579599, -76.3799, 6.7008, -10.595799, -76.353195, 6.7008, -10.595799, -73.3624, 3.3486998, -10.579599, -79.3491, 6.7008, -10.595799, -79.379, 6.7008, -10.595799, -76.353195, 6.7008, -10.595799, -73.3624, 10.054299, -10.6119995, -73.322395, 10.054299, -10.6119995, -70.518394, 6.7008, -10.595799, -76.353195, 10.054299, -10.6119995, -73.322395, 6.7008, -10.595799, -73.3624, 10.054299, -10.6119995, -73.322395, 13.4093, -10.6283, -70.4807, 10.054299, -10.6119995, -70.518394, 6.7008, -10.595799, -79.379, 10.054299, -10.6119995, -76.3264, 6.7008, -10.595799, -76.353195, 6.7008, -10.595799, -76.353195, 10.054299, -10.6119995, -76.3264, 10.054299, -10.6119995, -73.322395, 6.7008, -10.595799, -79.379, 10.054299, -10.6119995, -79.4089, 10.054299, -10.6119995, -76.3264, 10.054299, -10.6119995, -73.322395, 13.4093, -10.6283, -73.282394, 13.4093, -10.6283, -70.4807, 10.054299, -10.6119995, -76.3264, 13.4093, -10.6283, -73.282394, 10.054299, -10.6119995, -73.322395, 13.4093, -10.6283, -73.282394, 16.7656, -10.6446, -70.443, 13.4093, -10.6283, -70.4807, 10.054299, -10.6119995, -79.4089, 13.4093, -10.6283, -76.2996, 10.054299, -10.6119995, -76.3264, 10.054299, -10.6119995, -76.3264, 13.4093, -10.6283, -76.2996, 13.4093, -10.6283, -73.282394, 10.054299, -10.6119995, -79.4089, 13.4093, -10.6283, -79.4388, 13.4093, -10.6283, -76.2996, 13.4093, -10.6283, -73.282394, 16.7656, -10.6446, -73.2424, 16.7656, -10.6446, -70.443, 13.4093, -10.6283, -76.2996, 16.7656, -10.6446, -73.2424, 13.4093, -10.6283, -73.282394, 16.7656, -10.6446, -73.2424, 20.1234, -10.6608, -70.4053, 16.7656, -10.6446, -70.443, 13.4093, -10.6283, -79.4388, 16.7656, -10.6446, -76.2728, 13.4093, -10.6283, -76.2996, 13.4093, -10.6283, -76.2996, 16.7656, -10.6446, -76.2728, 16.7656, -10.6446, -73.2424, 13.4093, -10.6283, -79.4388, 16.7656, -10.6446, -79.468796, 16.7656, -10.6446, -76.2728, 16.7656, -10.6446, -73.2424, 20.1234, -10.6608, -73.2023, 20.1234, -10.6608, -70.4053, 16.7656, -10.6446, -76.2728, 20.1234, -10.6608, -73.2023, 16.7656, -10.6446, -73.2424, 20.1234, -10.6608, -73.2023, 23.812, -10.6786995, -70.3639, 20.1234, -10.6608, -70.4053, 16.7656, -10.6446, -79.468796, 20.1234, -10.6608, -76.245995, 16.7656, -10.6446, -76.2728, 16.7656, -10.6446, -76.2728, 20.1234, -10.6608, -76.245995, 20.1234, -10.6608, -73.2023, 16.7656, -10.6446, -79.468796, 20.1234, -10.6608, -79.498695, 20.1234, -10.6608, -76.245995, 20.1234, -10.6608, -73.2023, 23.7337, -10.6783, -73.1593, 23.812, -10.6786995, -70.3639, 20.1234, -10.6608, -76.245995, 23.7337, -10.6783, -73.1593, 20.1234, -10.6608, -73.2023, 23.7337, -10.6783, -73.1593, 26.7572, -10.693, -70.330795, 23.812, -10.6786995, -70.3639, 23.7337, -10.6783, -73.1593, 26.7572, -10.693, -73.1232, 26.7572, -10.693, -70.330795, 20.1234, -10.6608, -76.245995, 23.6481, -10.677899, -76.2178, 23.7337, -10.6783, -73.1593, 23.6481, -10.677899, -76.2178, 26.7572, -10.693, -73.1232, 23.7337, -10.6783, -73.1593, 20.1234, -10.6608, -79.498695, 23.6481, -10.677899, -76.2178, 20.1234, -10.6608, -76.245995, 23.6481, -10.677899, -76.2178, 26.7572, -10.693, -76.193, 26.7572, -10.693, -73.1232, 20.1234, -10.6608, -79.498695, 23.555399, -10.6775, -79.529396, 23.6481, -10.677899, -76.2178, 23.555399, -10.6775, -79.529396, 26.7572, -10.693, -76.193, 23.6481, -10.677899, -76.2178, 23.555399, -10.6775, -79.529396, 26.7572, -10.693, -79.5579, 26.7572, -10.693, -76.193, -26.7572, -10.4337, -79.0804, -23.412498, -10.4337, -82.2434, -23.4177, -10.4499, -79.1102, -26.7572, -10.4337, -79.0804, -26.7572, -10.4337, -82.2434, -23.412498, -10.4337, -82.2434, 23.412498, -10.4337, -82.2434, 26.7572, -10.693, -79.5579, 23.555399, -10.6775, -79.529396, 23.412498, -10.4337, -82.2434, 26.7572, -10.693, -82.2434, 26.7572, -10.693, -79.5579, 23.812, -10.6786995, -70.3639, 26.7572, -10.693, -68.371895, 23.808699, -10.464399, -68.371895, 23.812, -10.6786995, -70.3639, 26.7572, -10.693, -70.330795, 26.7572, -10.693, -68.371895, 20.0679, -10.4337, -82.2434, 23.555399, -10.6775, -79.529396, 20.1234, -10.6608, -79.498695, 20.0679, -10.4337, -82.2434, 23.412498, -10.4337, -82.2434, 23.555399, -10.6775, -79.529396, 23.812, -10.6786995, -70.3639, 20.0679, -10.4337, -68.371895, 20.1234, -10.6608, -70.4053, 20.0679, -10.4337, -68.371895, 23.812, -10.6786995, -70.3639, 23.808699, -10.464399, -68.371895, 20.0679, -10.4337, -68.371895, 23.808699, -10.464399, -68.371895, 23.412498, -10.4337, -68.371895, 16.7232, -10.4337, -82.2434, 20.1234, -10.6608, -79.498695, 16.7656, -10.6446, -79.468796, 16.7232, -10.4337, -82.2434, 20.0679, -10.4337, -82.2434, 20.1234, -10.6608, -79.498695, 16.7656, -10.6446, -70.443, 20.0679, -10.4337, -68.371895, 16.7232, -10.4337, -68.371895, 16.7656, -10.6446, -70.443, 20.1234, -10.6608, -70.4053, 20.0679, -10.4337, -68.371895, 13.378599, -10.4337, -82.2434, 16.7656, -10.6446, -79.468796, 13.4093, -10.6283, -79.4388, 13.378599, -10.4337, -82.2434, 16.7232, -10.4337, -82.2434, 16.7656, -10.6446, -79.468796, 13.4093, -10.6283, -70.4807, 16.7232, -10.4337, -68.371895, 13.378599, -10.4337, -68.371895, 13.4093, -10.6283, -70.4807, 16.7656, -10.6446, -70.443, 16.7232, -10.4337, -68.371895, 10.033899, -10.4337, -82.2434, 13.4093, -10.6283, -79.4388, 10.054299, -10.6119995, -79.4089, 10.033899, -10.4337, -82.2434, 13.378599, -10.4337, -82.2434, 13.4093, -10.6283, -79.4388, 10.054299, -10.6119995, -70.518394, 13.378599, -10.4337, -68.371895, 10.033899, -10.4337, -68.371895, 10.054299, -10.6119995, -70.518394, 13.4093, -10.6283, -70.4807, 13.378599, -10.4337, -68.371895, 6.6893, -10.4337, -82.2434, 10.054299, -10.6119995, -79.4089, 6.7008, -10.595799, -79.379, 6.6893, -10.4337, -82.2434, 10.033899, -10.4337, -82.2434, 10.054299, -10.6119995, -79.4089, 6.7008, -10.595799, -70.5561, 10.033899, -10.4337, -68.371895, 6.6893, -10.4337, -68.371895, 6.7008, -10.595799, -70.5561, 10.054299, -10.6119995, -70.518394, 10.033899, -10.4337, -68.371895, 3.3446, -10.4337, -82.2434, 6.7008, -10.595799, -79.379, 3.3486998, -10.579599, -79.3491, 3.3446, -10.4337, -82.2434, 6.6893, -10.4337, -82.2434, 6.7008, -10.595799, -79.379, 3.3486998, -10.579599, -70.5937, 6.6893, -10.4337, -68.371895, 3.3446, -10.4337, -68.371895, 3.3486998, -10.579599, -70.5937, 6.7008, -10.595799, -70.5561, 6.6893, -10.4337, -68.371895, 0, -10.4337, -82.2434, 3.3486998, -10.579599, -79.3491, -0.0020999908, -10.5633, -79.3192, 0, -10.4337, -82.2434, 3.3446, -10.4337, -82.2434, 3.3486998, -10.579599, -79.3491, -0.0020999908, -10.5633, -70.6314, 3.3446, -10.4337, -68.371895, 0, -10.4337, -68.371895, -0.0020999908, -10.5633, -70.6314, 3.3486998, -10.579599, -70.5937, 3.3446, -10.4337, -68.371895, -3.3446002, -10.4337, -82.2434, -0.0020999908, -10.5633, -79.3192, -3.3514, -10.5471, -79.2893, -3.3446002, -10.4337, -82.2434, 0, -10.4337, -82.2434, -0.0020999908, -10.5633, -79.3192, -3.3514, -10.5471, -70.669, -6.6893, -10.4337, -68.371895, -6.6993003, -10.5309, -70.7066, -3.3514, -10.5471, -70.669, -3.3446002, -10.4337, -68.371895, -6.6893, -10.4337, -68.371895, -6.6893, -10.4337, -68.371895, -10.045801, -10.5146, -70.7442, -6.6993003, -10.5309, -70.7066, -6.6893, -10.4337, -68.371895, -10.033899, -10.4337, -68.371895, -10.045801, -10.5146, -70.7442, -6.6893, -10.4337, -82.2434, -3.3514, -10.5471, -79.2893, -6.6993003, -10.5309, -79.2594, -6.6893, -10.4337, -82.2434, -3.3446002, -10.4337, -82.2434, -3.3514, -10.5471, -79.2893, -10.033899, -10.4337, -82.2434, -6.6993003, -10.5309, -79.2594, -10.045801, -10.5146, -79.2295, -10.033899, -10.4337, -82.2434, -6.6893, -10.4337, -82.2434, -6.6993003, -10.5309, -79.2594, -10.033899, -10.4337, -68.371895, -13.3908, -10.4984, -70.7818, -10.045801, -10.5146, -70.7442, -10.033899, -10.4337, -68.371895, -13.378601, -10.4337, -68.371895, -13.3908, -10.4984, -70.7818, -13.378601, -10.4337, -82.2434, -10.045801, -10.5146, -79.2295, -13.3909, -10.4984, -79.1997, -13.378601, -10.4337, -82.2434, -10.033899, -10.4337, -82.2434, -10.045801, -10.5146, -79.2295, -13.378601, -10.4337, -68.371895, -16.734499, -10.4822, -70.8193, -13.3908, -10.4984, -70.7818, -13.378601, -10.4337, -68.371895, -16.7232, -10.4337, -68.371895, -16.734499, -10.4822, -70.8193, -16.7232, -10.4337, -68.371895, -20.0768, -10.466, -70.856895, -16.734499, -10.4822, -70.8193, -16.7232, -10.4337, -68.371895, -20.0679, -10.4337, -68.371895, -20.0768, -10.466, -70.856895, -16.7232, -10.4337, -82.2434, -13.3909, -10.4984, -79.1997, -16.734499, -10.4822, -79.1699, -16.7232, -10.4337, -82.2434, -13.378601, -10.4337, -82.2434, -13.3909, -10.4984, -79.1997, -20.0679, -10.4337, -68.371895, -23.4177, -10.4499, -70.8944, -20.0768, -10.466, -70.856895, -20.0679, -10.4337, -68.371895, -23.412498, -10.4337, -68.371895, -23.4177, -10.4499, -70.8944, -20.0679, -10.4337, -82.2434, -16.734499, -10.4822, -79.1699, -20.0768, -10.466, -79.14, -20.0679, -10.4337, -82.2434, -16.7232, -10.4337, -82.2434, -16.734499, -10.4822, -79.1699, -23.412498, -10.4337, -68.371895, -26.7572, -10.4337, -70.9319, -23.4177, -10.4499, -70.8944, -23.412498, -10.4337, -68.371895, -26.7572, -10.4337, -68.371895, -26.7572, -10.4337, -70.9319, -23.412498, -10.4337, -82.2434, -20.0768, -10.466, -79.14, -23.4177, -10.4499, -79.1102, -23.412498, -10.4337, -82.2434, -20.0679, -10.4337, -82.2434, -20.0768, -10.466, -79.14, 13.378599, -1.0746, 20.0679, 13.378599, -2.2719998, 23.3902, 10.033899, -0.501, 23.412498, 20.0679, -1.0746, 13.378601, 16.7232, -1.0746, 16.7232, 16.7232, -1.0746, 13.378601, 16.7232, -1.0746, 16.7232, 15.0508995, -1.0746, 20.0679, 13.378599, -1.0746, 20.0679, 16.7232, -1.0746, 16.7232, 16.7232, -1.8294, 20.0679, 15.0508995, -1.0746, 20.0679, 23.412498, -0.35529998, 10.033899, 23.412498, -1.8294, 13.378601, 20.0679, -1.0746, 13.378601, 10.501598, 0, 20.5087, 6.6893, 0, 20.0679, 10.080299, 0, 20.0679, 10.501598, 0, 20.5087, 6.6893, -0.2814, 23.412498, 6.6893, 0, 20.0679, -20.0679, 0, 13.378601, -16.7232, 0, 13.868401, -16.7232, -0.26209998, 16.7232, -20.0679, 0, 13.378601, -17.213, 0, 13.378601, -16.7232, 0, 13.868401, -16.7232, -0.26209998, 16.7232, -13.378601, 0, 16.7232, -13.378601, 0, 20.0679, -24.056599, 0, 10.033899, -25.9681, -0.2875, 13.378601, -26.0077, -0.2465, 10.033899, -25.9681, -0.2875, 13.378601, -24.056599, 0, 10.033899, -23.412498, 0, 10.6779995, -25.9681, -0.2875, 13.378601, -23.412498, 0, 10.6779995, -23.412498, 0, 13.378601, -23.8078, 0, 6.6893, -26.0077, -0.2465, 10.033899, -25.945799, -0.25779998, 6.6893, -26.0077, -0.2465, 10.033899, -23.8078, 0, 6.6893, -23.489, 0, 9.386499, -26.0077, -0.2465, 10.033899, -23.489, 0, 9.386499, -24.056599, 0, 10.033899, -25.4212, -0.5907, 16.7232, -23.412498, -0.692, 20.0679, -25.174, -0.7391, 20.0679, -25.479, -0.4588, 0.143, -23.412498, 0, 3.3446, -25.869999, -0.2764, 3.3446, -25.479, -0.4588, 0.143, -23.412498, -0.2913, 0.26119995, -23.412498, 0, 3.3446, -25.869999, -0.2764, 3.3446, -23.8078, 0, 6.6893, -25.945799, -0.25779998, 6.6893, -25.869999, -0.2764, 3.3446, -23.412498, 0, 3.3446, -23.8078, 0, 6.6893, -17.2383, -0.66719997, -19.6364, -20.0679, -0.43429998, -16.7232, -20.0679, -1.7572, -20.0679, -20.0679, -0.43429998, -16.7232, -20.0679, 0, -13.378599, -23.412498, 0, -13.378599, -9.318001, -0.3969, -19.413, -6.6893, -0.9271, -20.0679, -6.6893, 0, -16.7232, -6.6893, 0, -16.7232, -3.3446002, -0.9271, -16.7232, -3.3446002, 0, -13.7127, 16.7232, 0, -20.0679, 20.0679, -0.2837, -20.0679, 20.0679, 0, -16.7232, 13.378599, 0, -23.412498, 16.7232, -0.3105, -23.412498, 16.7232, 0, -20.0679, 20.0679, 0, -16.7232, 23.412498, -0.456, -16.7232, 23.412498, 0, -13.378599, 23.412498, 0, -6.6893, 23.412498, 0, -3.3446002, 22.5396, -0.014599999, -3.3446002, 15.0508995, -0.40039998, 16.7232, 13.378599, 0, 16.7232, 15.0508995, -0.6288, 13.378601, 14.439199, 0, 13.6437, 15.0508995, 0, 10.033899, 15.0508995, -0.6288, 13.378601, 15.0508995, 0, 10.033899, 14.439199, 0, 13.6437, 13.8046, 0, 13.2202, 15.0508995, 0, 10.033899, 13.8046, 0, 13.2202, 13.8046, 0, 10.033899, 3.0140998, 0, -16.7232, 3.3446, -0.3989, -12.9443, 0, 0, -13.378599, -23.412498, -0.692, 20.0679, -25.4212, -0.5907, 16.7232, -23.412498, -0.36479998, 16.7232, -23.412498, -0.692, 20.0679, -20.0679, -0.36479998, 20.0679, -20.0679, -0.5589, 23.412498, -16.7232, -0.36479998, 23.412498, -13.378601, -0.2814, 23.412498, -13.378601, -1.1209999, 26.7572, -20.0679, -0.36479998, 16.7232, -16.7232, -0.26209998, 16.7232, -16.7232, -0.36479998, 20.0679, -16.7232, -0.36479998, 20.0679, -13.378601, 0, 20.0679, -13.378601, -0.2814, 23.412498, -23.412498, -0.7373, 23.412498, -20.0679, -0.5589, 23.412498, -20.0679, -1.1374999, 26.7572, -25.174, -0.7391, 20.0679, -23.412498, -0.692, 20.0679, -23.412498, -0.7373, 23.412498, -20.0679, -0.5589, 23.412498, -16.7232, -0.36479998, 23.412498, -16.7232, -1.1374999, 26.7572, 26.7582, -1.7623999, 10.033899, 26.7582, -1.8294, 13.378601, 23.412498, -1.8294, 13.378601, 13.378599, 0, 16.7232, 14.439199, 0, 13.6437, 15.0508995, -0.6288, 13.378601, 13.378599, 0, 16.7232, 13.700699, 0, 14.602699, 14.439199, 0, 13.6437, 15.0508995, -0.6288, 13.378601, 15.0508995, 0, 10.033899, 16.7232, 0, 10.033899, 15.0508995, -0.40039998, 16.7232, 16.7232, -1.0746, 13.378601, 16.7232, -1.0746, 16.7232, 13.378599, 0, 16.7232, 13.378599, -1.0746, 20.0679, 11.0191, 0, 20.0679, 15.0508995, -0.40039998, 16.7232, 13.378599, -1.0746, 20.0679, 13.378599, 0, 16.7232, 20.0679, -0.2837, -20.0679, 23.412498, -1.0924, -20.0679, 23.412498, -0.456, -16.7232, 26.572199, -0.95549995, -13.659599, 23.412498, 0, -13.378599, 23.412498, -0.456, -16.7232, 13.378599, -0.8401, -26.7572, 16.7232, -1.3382, -26.7572, 16.7232, -0.3105, -23.412498, 16.7232, -0.3105, -23.412498, 20.0679, -0.8222, -23.412498, 20.0679, -0.2837, -20.0679, 16.7232, -1.3382, -26.7572, 13.378599, -0.8401, -26.7572, 13.378599, -1.582, -30.410397, 16.7232, -1.3382, -26.7572, 20.0679, -1.1922, -26.7572, 20.0679, -0.8222, -23.412498, 26.7577, -0.8804, -16.7232, 23.412498, -0.456, -16.7232, 23.412498, -1.0924, -20.0679, 20.0679, -0.8222, -23.412498, 23.412498, -1.0924, -23.412498, 23.412498, -1.0924, -20.0679, 3.3446, -0.3434, -23.412498, 0, -0.9271, -20.0679, 0, -0.9271, -23.412498, -10.033899, -0.9271, -23.412498, -6.6893, -1.3241999, -23.412498, -6.6893, -0.9271, -20.0679, 2.3184, 0, -20.4362, 0, -0.9271, -16.7232, 0, -0.9271, -20.0679, 39.877598, -3.6573, 23.412498, 36.078598, -3.0746999, 26.7572, 36.2121, -2.9357, 23.412498, 39.877598, -3.6573, 23.412498, 39.877598, -3.6573, 26.7572, 36.078598, -3.0746999, 26.7572, 52.997097, -8.1437, 23.412498, 49.5743, -6.9732, 26.7572, 49.5645, -6.9698997, 23.412498, 52.997097, -8.1437, 23.412498, 52.997097, -8.1437, 26.7572, 49.5743, -6.9732, 26.7572, 52.997097, -8.1437, 20.0679, 52.997097, -8.1437, 23.412498, 49.5645, -6.9698997, 23.412498, 49.5743, -6.9732, 26.7572, 46.556797, -5.9413, 23.412498, 49.5645, -6.9698997, 23.412498, 49.5743, -6.9732, 26.7572, 46.577698, -5.9484997, 26.7572, 46.556797, -5.9413, 23.412498, 52.997097, -8.1437, 20.0679, 49.5645, -6.9698997, 23.412498, 49.5547, -6.9665, 20.0679, 52.997097, -8.1437, 16.7232, 52.997097, -8.1437, 20.0679, 49.5547, -6.9665, 20.0679, 52.997097, -8.1437, 16.7232, 49.5547, -6.9665, 20.0679, 49.5449, -6.9631996, 16.7232, 49.5547, -6.9665, 20.0679, 46.5149, -5.927, 16.7232, 49.5449, -6.9631996, 16.7232, 49.5645, -6.9698997, 23.412498, 46.535797, -5.9342, 20.0679, 49.5547, -6.9665, 20.0679, 49.5547, -6.9665, 20.0679, 46.535797, -5.9342, 20.0679, 46.5149, -5.927, 16.7232, 49.5645, -6.9698997, 23.412498, 46.556797, -5.9413, 23.412498, 46.535797, -5.9342, 20.0679, 46.535797, -5.9342, 20.0679, 43.1641, -4.7812, 16.7232, 46.5149, -5.927, 16.7232, 46.535797, -5.9342, 20.0679, 43.1877, -4.7892, 20.0679, 43.1641, -4.7812, 16.7232, 46.556797, -5.9413, 23.412498, 43.1877, -4.7892, 20.0679, 46.535797, -5.9342, 20.0679, 43.1877, -4.7892, 20.0679, 39.877598, -3.6573, 16.7232, 43.1641, -4.7812, 16.7232, 43.1877, -4.7892, 20.0679, 39.877598, -3.6573, 20.0679, 39.877598, -3.6573, 16.7232, 46.556797, -5.9413, 23.412498, 43.2113, -4.7973, 23.412498, 43.1877, -4.7892, 20.0679, 43.2113, -4.7973, 23.412498, 39.877598, -3.6573, 20.0679, 43.1877, -4.7892, 20.0679, 46.577698, -5.9484997, 26.7572, 43.2113, -4.7973, 23.412498, 46.556797, -5.9413, 23.412498, 43.2113, -4.7973, 23.412498, 39.877598, -3.6573, 23.412498, 39.877598, -3.6573, 20.0679, 46.577698, -5.9484997, 26.7572, 43.234898, -4.8054, 26.7572, 43.2113, -4.7973, 23.412498, 43.234898, -4.8054, 26.7572, 39.877598, -3.6573, 23.412498, 43.2113, -4.7973, 23.412498, 43.234898, -4.8054, 26.7572, 39.877598, -3.6573, 26.7572, 39.877598, -3.6573, 23.412498, 39.877598, -3.6573, 20.0679, 36.2121, -2.9357, 23.412498, 36.2075, -2.9348, 20.0679, 39.877598, -3.6573, 20.0679, 39.877598, -3.6573, 23.412498, 36.2121, -2.9357, 23.412498, 39.877598, -3.6573, 16.7232, 39.877598, -3.6573, 20.0679, 36.2075, -2.9348, 20.0679, 39.877598, -3.6573, 16.7232, 36.2075, -2.9348, 20.0679, 36.2028, -2.9338999, 16.7232, 39.877598, -3.7507, 13.378601, 36.2028, -2.9338999, 16.7232, 36.1422, -2.9887998, 13.378601, 39.877598, -3.7507, 13.378601, 39.877598, -3.6573, 16.7232, 36.2028, -2.9338999, 16.7232, 52.997097, -8.1437, 13.378601, 49.5449, -6.9631996, 16.7232, 49.520397, -6.9795, 13.378601, 52.997097, -8.1437, 13.378601, 52.997097, -8.1437, 16.7232, 49.5449, -6.9631996, 16.7232, 39.877598, -3.7507, 10.033899, 36.1422, -2.9887998, 13.378601, 36.153297, -2.972, 10.033899, 39.877598, -3.7507, 10.033899, 39.877598, -3.7507, 13.378601, 36.1422, -2.9887998, 13.378601, 52.997097, -8.1437, 10.033899, 49.520397, -6.9795, 13.378601, 49.510597, -6.9763, 10.033899, 52.997097, -8.1437, 10.033899, 52.997097, -8.1437, 13.378601, 49.520397, -6.9795, 13.378601, 52.997097, -8.1437, 6.6893, 52.997097, -8.1437, 10.033899, 49.510597, -6.9763, 10.033899, 49.520397, -6.9795, 13.378601, 46.4435, -5.9493, 10.033899, 49.510597, -6.9763, 10.033899, 49.520397, -6.9795, 13.378601, 46.4646, -5.9563, 13.378601, 46.4435, -5.9493, 10.033899, 52.997097, -8.1437, 6.6893, 49.510597, -6.9763, 10.033899, 49.500698, -6.973, 6.6893, 52.997097, -8.1437, 3.3446, 52.997097, -8.1437, 6.6893, 49.500698, -6.973, 6.6893, 49.510597, -6.9763, 10.033899, 46.4435, -5.9493, 10.033899, 46.4225, -5.9421997, 6.6893, 49.510597, -6.9763, 10.033899, 46.4225, -5.9421997, 6.6893, 49.500698, -6.973, 6.6893, 46.4646, -5.9563, 13.378601, 43.0695, -4.8195, 10.033899, 46.4435, -5.9493, 10.033899, 46.4646, -5.9563, 13.378601, 43.0932, -4.8275, 13.378601, 43.0695, -4.8195, 10.033899, 43.0932, -4.8275, 13.378601, 39.877598, -3.7507, 10.033899, 43.0695, -4.8195, 10.033899, 43.0932, -4.8275, 13.378601, 39.877598, -3.7507, 13.378601, 39.877598, -3.7507, 10.033899, 43.0695, -4.8195, 10.033899, 39.877598, -3.7507, 10.033899, 39.877598, -3.7507, 6.6893, 46.4435, -5.9493, 10.033899, 43.0695, -4.8195, 10.033899, 43.0458, -4.8115997, 6.6893, 43.0695, -4.8195, 10.033899, 39.877598, -3.7507, 6.6893, 43.0458, -4.8115997, 6.6893, 46.4435, -5.9493, 10.033899, 43.0458, -4.8115997, 6.6893, 46.4225, -5.9421997, 6.6893, 43.0458, -4.8115997, 6.6893, 39.877598, -3.7507, 6.6893, 39.877598, -3.7507, 3.3446, 43.0458, -4.8115997, 6.6893, 39.877598, -3.7507, 3.3446, 43.0221, -4.8036, 3.3446, 46.4225, -5.9421997, 6.6893, 43.0458, -4.8115997, 6.6893, 43.0221, -4.8036, 3.3446, 43.0221, -4.8036, 3.3446, 39.877598, -3.7507, 3.3446, 39.877598, -3.7507, 0, 46.4225, -5.9421997, 6.6893, 43.0221, -4.8036, 3.3446, 46.4015, -5.9351997, 3.3446, 49.500698, -6.973, 6.6893, 46.4225, -5.9421997, 6.6893, 46.4015, -5.9351997, 3.3446, 43.0221, -4.8036, 3.3446, 39.877598, -3.7507, 0, 42.998398, -4.7957, 0, 46.4015, -5.9351997, 3.3446, 43.0221, -4.8036, 3.3446, 42.998398, -4.7957, 0, 42.998398, -4.7957, 0, 39.877598, -3.7507, 0, 39.877598, -3.7507, -3.3446002, 49.500698, -6.973, 6.6893, 46.4015, -5.9351997, 3.3446, 49.4909, -6.9697, 3.3446, 52.997097, -8.1437, 3.3446, 49.500698, -6.973, 6.6893, 49.4909, -6.9697, 3.3446, 52.997097, -8.1437, 0, 52.997097, -8.1437, 3.3446, 49.4909, -6.9697, 3.3446, 49.4909, -6.9697, 3.3446, 46.4015, -5.9351997, 3.3446, 46.380398, -5.9282, 0, 46.4015, -5.9351997, 3.3446, 42.998398, -4.7957, 0, 46.380398, -5.9282, 0, 52.997097, -8.1437, 0, 49.4909, -6.9697, 3.3446, 49.4811, -6.9663997, 0, 49.4909, -6.9697, 3.3446, 46.380398, -5.9282, 0, 49.4811, -6.9663997, 0, 52.997097, -8.1437, -3.3446002, 52.997097, -8.1437, 0, 49.4811, -6.9663997, 0, 42.998398, -4.7957, 0, 39.877598, -3.7507, -3.3446002, 42.974697, -4.7878, -3.3446002, 46.380398, -5.9282, 0, 42.998398, -4.7957, 0, 42.974697, -4.7878, -3.3446002, 42.974697, -4.7878, -3.3446002, 39.877598, -3.7507, -3.3446002, 39.877598, -3.7507, -6.6893, 42.974697, -4.7878, -3.3446002, 39.877598, -3.7507, -6.6893, 42.951, -4.7798, -6.6893, 46.380398, -5.9282, 0, 42.974697, -4.7878, -3.3446002, 46.359398, -5.9210997, -3.3446002, 46.359398, -5.9210997, -3.3446002, 42.974697, -4.7878, -3.3446002, 42.951, -4.7798, -6.6893, 49.4811, -6.9663997, 0, 46.380398, -5.9282, 0, 46.359398, -5.9210997, -3.3446002, 46.359398, -5.9210997, -3.3446002, 42.951, -4.7798, -6.6893, 46.338398, -5.9140997, -6.6893, 52.997097, -8.1437, -3.3446002, 49.4811, -6.9663997, 0, 49.4712, -6.9631, -3.3446002, 49.4811, -6.9663997, 0, 46.359398, -5.9210997, -3.3446002, 49.4712, -6.9631, -3.3446002, 49.4712, -6.9631, -3.3446002, 46.359398, -5.9210997, -3.3446002, 46.338398, -5.9140997, -6.6893, 52.997097, -8.1437, -6.6893, 52.997097, -8.1437, -3.3446002, 49.4712, -6.9631, -3.3446002, 49.4712, -6.9631, -3.3446002, 46.338398, -5.9140997, -6.6893, 49.4614, -6.9598, -6.6893, 52.997097, -8.1437, -6.6893, 49.4712, -6.9631, -3.3446002, 49.4614, -6.9598, -6.6893, 39.877598, -3.7507, 6.6893, 36.153297, -2.972, 10.033899, 36.1898, -2.9299998, 6.6893, 39.877598, -3.7507, 6.6893, 39.877598, -3.7507, 10.033899, 36.153297, -2.972, 10.033899, 39.877598, -3.7507, 3.3446, 36.1898, -2.9299998, 6.6893, 36.2081, -2.9062, 3.3446, 39.877598, -3.7507, 3.3446, 39.877598, -3.7507, 6.6893, 36.1898, -2.9299998, 6.6893, 39.877598, -3.7507, 0, 36.2081, -2.9062, 3.3446, 36.211597, -2.8971, 0, 39.877598, -3.7507, 0, 39.877598, -3.7507, 3.3446, 36.2081, -2.9062, 3.3446, 39.877598, -3.7507, -3.3446002, 36.211597, -2.8971, 0, 36.2533, -2.8497999, -3.3446002, 39.877598, -3.7507, -3.3446002, 39.877598, -3.7507, 0, 36.211597, -2.8971, 0, 39.877598, -3.7507, -6.6893, 36.2533, -2.8497999, -3.3446002, 36.2803, -2.8172998, -6.6893, 39.877598, -3.7507, -6.6893, 39.877598, -3.7507, -3.3446002, 36.2533, -2.8497999, -3.3446002, 52.997097, -8.1437, -10.033899, 49.4614, -6.9598, -6.6893, 49.4666, -6.9364, -10.033899, 52.997097, -8.1437, -10.033899, 52.997097, -8.1437, -6.6893, 49.4614, -6.9598, -6.6893, 39.877598, -3.6573, -10.033899, 36.2803, -2.8172998, -6.6893, 36.3083, -2.7835999, -10.109299, 39.877598, -3.6573, -10.033899, 39.877598, -3.7507, -6.6893, 36.2803, -2.8172998, -6.6893, 39.877598, -3.6573, -13.378599, 36.3083, -2.7835999, -10.109299, 36.1797, -2.9064, -13.456699, 39.877598, -3.6573, -13.378599, 39.877598, -3.6573, -10.033899, 36.3083, -2.7835999, -10.109299, 39.877598, -3.6573, -16.7232, 36.1797, -2.9064, -13.456699, 36.2015, -2.8792, -16.7232, 39.877598, -3.6573, -16.7232, 39.877598, -3.6573, -13.378599, 36.1797, -2.9064, -13.456699, 39.877598, -3.6573, -20.0679, 36.2015, -2.8792, -16.7232, 36.1471, -2.928, -20.0679, 39.877598, -3.6573, -20.0679, 39.877598, -3.6573, -16.7232, 36.2015, -2.8792, -16.7232, 39.877598, -3.6573, -23.412498, 42.8416, -4.6944, -26.7572, 42.881, -4.6844, -23.412498, 39.877598, -3.6573, -23.412498, 39.877598, -3.6808999, -26.7572, 42.8416, -4.6944, -26.7572, 42.8416, -4.6944, -26.7572, 46.2636, -5.8410997, -23.412498, 42.881, -4.6844, -23.412498, 42.8416, -4.6944, -26.7572, 46.2278, -5.8524, -26.7572, 46.2636, -5.8410997, -23.412498, 46.2278, -5.8524, -26.7572, 49.427498, -6.923, -23.412498, 46.2636, -5.8410997, -23.412498, 46.2278, -5.8524, -26.7572, 49.4036, -6.9384, -26.7572, 49.427498, -6.923, -23.412498, 49.4036, -6.9384, -26.7572, 52.997097, -8.1437, -23.412498, 49.427498, -6.923, -23.412498, 49.4036, -6.9384, -26.7572, 52.997097, -8.1672, -26.7572, 52.997097, -8.1437, -23.412498, 52.997097, -8.1437, -23.412498, 56.4827, -8.7755995, -26.7572, 56.4571, -8.7476, -23.412498, 52.997097, -8.1437, -23.412498, 52.997097, -8.1672, -26.7572, 56.4827, -8.7755995, -26.7572, 56.4827, -8.7755995, -26.7572, 59.414497, -9.2638, -23.412498, 56.4571, -8.7476, -23.412498, 56.4827, -8.7755995, -26.7572, 59.4335, -9.2907, -26.7572, 59.414497, -9.2638, -23.412498, 59.4335, -9.2907, -26.7572, 62.3756, -9.7807, -23.412498, 59.414497, -9.2638, -23.412498, 59.4335, -9.2907, -26.7572, 62.356697, -9.8008995, -26.7572, 62.3756, -9.7807, -23.412498, 62.356697, -9.8008995, -26.7572, 66.1166, -10.4337, -23.412498, 62.3756, -9.7807, -23.412498, 62.356697, -9.8008995, -26.7572, 66.1166, -10.4572, -26.7572, 66.1166, -10.4337, -23.412498, 39.877598, -3.6573, -23.412498, 36.1471, -2.928, -20.0679, 36.1424, -2.9269998, -23.412498, 39.877598, -3.6573, -23.412498, 39.877598, -3.6573, -20.0679, 36.1471, -2.928, -20.0679, 36.1471, -2.928, -20.0679, 33.0948, -2.3312, -23.412498, 36.1424, -2.9269998, -23.412498, 36.1471, -2.928, -20.0679, 33.1148, -2.3351, -20.0679, 33.0948, -2.3312, -23.412498, 33.1148, -2.3351, -20.0679, 29.564798, -1.6410999, -23.412498, 33.0948, -2.3312, -23.412498, 33.1148, -2.3351, -20.0679, 29.562698, -1.6407, -20.0679, 29.564798, -1.6410999, -23.412498, 29.562698, -1.6407, -20.0679, 26.7582, -1.0924, -23.412498, 29.564798, -1.6410999, -23.412498, 29.562698, -1.6407, -20.0679, 26.7582, -1.0924, -20.0679, 26.7582, -1.0924, -23.412498, 29.6302, -1.5764999, -13.594999, 26.7577, -0.8804, -16.7232, 29.7123, -1.5057, -16.7232, 29.6302, -1.5764999, -13.594999, 26.572199, -0.95549995, -13.659599, 26.7577, -0.8804, -16.7232, 30.0488, -1.2003, -6.6893, 26.572199, -0.4003, -10.314899, 30.0059, -1.2408999, -10.242399, 30.0488, -1.2003, -6.6893, 26.7577, -0.3464, -6.6893, 26.572199, -0.4003, -10.314899, 29.536499, -2.2303, 13.378601, 26.7582, -1.7623999, 10.033899, 29.586699, -2.188, 10.033899, 29.536499, -2.2303, 13.378601, 26.7582, -1.8294, 13.378601, 26.7582, -1.7623999, 10.033899, 29.8112, -1.4067, 0, 26.7577, -0.4894, -3.3446002, 29.9525, -1.2836, -3.3446002, 29.8112, -1.4067, 0, 26.7577, -0.6956, 0, 26.7577, -0.4894, -3.3446002, 29.550499, -2.2133, 20.0679, 26.7582, -1.8294, 16.7232, 29.552599, -2.2137, 16.7232, 29.550499, -2.2133, 20.0679, 26.7582, -1.8294, 20.0679, 26.7582, -1.8294, 16.7232, 29.3928, -1.796, -26.7572, 33.0948, -2.3312, -23.412498, 29.564798, -1.6410999, -23.412498, 29.3928, -1.796, -26.7572, 32.9589, -2.4371, -26.7572, 33.0948, -2.3312, -23.412498, 29.7136, -1.4886999, 6.6893, 26.7577, -0.73109996, 3.3446, 29.7848, -1.4277999, 3.3446, 29.7136, -1.4886999, 6.6893, 26.7582, -0.83089995, 6.6893, 26.7577, -0.73109996, 3.3446, 29.1145, -2.5956, 26.7572, 26.7582, -2.2719998, 23.3902, 29.548399, -2.2129, 23.412498, 29.1145, -2.5956, 26.7572, 26.7582, -2.7837, 26.6955, 26.7582, -2.2719998, 23.3902, 29.7123, -1.5057, -16.7232, 26.7582, -1.0924, -20.0679, 29.562698, -1.6407, -20.0679, 29.7123, -1.5057, -16.7232, 26.7577, -0.8804, -16.7232, 26.7582, -1.0924, -20.0679, 30.0059, -1.2408999, -10.242399, 26.572199, -0.95549995, -13.659599, 29.6302, -1.5764999, -13.594999, 30.0059, -1.2408999, -10.242399, 26.572199, -0.4003, -10.314899, 26.572199, -0.95549995, -13.659599, 29.586699, -2.188, 10.033899, 26.7582, -0.83089995, 6.6893, 29.7136, -1.4886999, 6.6893, 29.586699, -2.188, 10.033899, 26.7582, -1.7623999, 10.033899, 26.7582, -0.83089995, 6.6893, 29.9525, -1.2836, -3.3446002, 26.7577, -0.3464, -6.6893, 30.0488, -1.2003, -6.6893, 29.9525, -1.2836, -3.3446002, 26.7577, -0.4894, -3.3446002, 26.7577, -0.3464, -6.6893, 29.552599, -2.2137, 16.7232, 26.7582, -1.8294, 13.378601, 29.536499, -2.2303, 13.378601, 29.552599, -2.2137, 16.7232, 26.7582, -1.8294, 16.7232, 26.7582, -1.8294, 13.378601, 29.7848, -1.4277999, 3.3446, 26.7577, -0.6956, 0, 29.8112, -1.4067, 0, 29.7848, -1.4277999, 3.3446, 26.7577, -0.73109996, 3.3446, 26.7577, -0.6956, 0, 29.548399, -2.2129, 23.412498, 26.7582, -1.8294, 20.0679, 29.550499, -2.2133, 20.0679, 29.548399, -2.2129, 23.412498, 26.7582, -2.2719998, 23.3902, 26.7582, -1.8294, 20.0679, 33.1982, -2.3009999, -13.5196, 29.7123, -1.5057, -16.7232, 33.2289, -2.25, -16.7232, 33.1982, -2.3009999, -13.5196, 29.6302, -1.5764999, -13.594999, 29.7123, -1.5057, -16.7232, 33.4719, -2.0886, -6.6893, 30.0059, -1.2408999, -10.242399, 33.4514, -2.0843, -10.169699, 33.4719, -2.0886, -6.6893, 30.0488, -1.2003, -6.6893, 30.0059, -1.2408999, -10.242399, 33.2822, -2.9943, 13.378601, 29.586699, -2.188, 10.033899, 33.2915, -2.9626, 10.033899, 33.2822, -2.9943, 13.378601, 29.536499, -2.2303, 13.378601, 29.586699, -2.188, 10.033899, 33.3661, -2.2345, 0, 29.9525, -1.2836, -3.3446002, 33.4326, -2.1487, -3.3446002, 33.3661, -2.2345, 0, 29.8112, -1.4067, 0, 29.9525, -1.2836, -3.3446002, 33.3628, -2.9636998, 20.0679, 29.552599, -2.2137, 16.7232, 33.3428, -2.9598, 16.7232, 33.3628, -2.9636998, 20.0679, 29.550499, -2.2133, 20.0679, 29.552599, -2.2137, 16.7232, 33.3828, -2.9677, 23.412498, 29.550499, -2.2133, 20.0679, 33.3628, -2.9636998, 20.0679, 33.3828, -2.9677, 23.412498, 29.548399, -2.2129, 23.412498, 29.550499, -2.2133, 20.0679, 33.347797, -2.2975, 6.6893, 29.7848, -1.4277999, 3.3446, 33.370598, -2.2531, 3.3446, 33.347797, -2.2975, 6.6893, 29.7136, -1.4886999, 6.6893, 29.7848, -1.4277999, 3.3446, 33.146698, -3.214, 26.7572, 29.548399, -2.2129, 23.412498, 33.3828, -2.9677, 23.412498, 33.146698, -3.214, 26.7572, 29.1145, -2.5956, 26.7572, 29.548399, -2.2129, 23.412498, 33.2289, -2.25, -16.7232, 29.562698, -1.6407, -20.0679, 33.1148, -2.3351, -20.0679, 33.2289, -2.25, -16.7232, 29.7123, -1.5057, -16.7232, 29.562698, -1.6407, -20.0679, 32.9589, -2.4371, -26.7572, 36.1424, -2.9269998, -23.412498, 33.0948, -2.3312, -23.412498, 32.9589, -2.4371, -26.7572, 36.0678, -2.9959998, -26.7572, 36.1424, -2.9269998, -23.412498, 33.4514, -2.0843, -10.169699, 29.6302, -1.5764999, -13.594999, 33.1982, -2.3009999, -13.5196, 33.4514, -2.0843, -10.169699, 30.0059, -1.2408999, -10.242399, 29.6302, -1.5764999, -13.594999, 33.2915, -2.9626, 10.033899, 29.7136, -1.4886999, 6.6893, 33.347797, -2.2975, 6.6893, 33.2915, -2.9626, 10.033899, 29.586699, -2.188, 10.033899, 29.7136, -1.4886999, 6.6893, 33.4326, -2.1487, -3.3446002, 30.0488, -1.2003, -6.6893, 33.4719, -2.0886, -6.6893, 33.4326, -2.1487, -3.3446002, 29.9525, -1.2836, -3.3446002, 30.0488, -1.2003, -6.6893, 33.3428, -2.9598, 16.7232, 29.536499, -2.2303, 13.378601, 33.2822, -2.9943, 13.378601, 33.3428, -2.9598, 16.7232, 29.552599, -2.2137, 16.7232, 29.536499, -2.2303, 13.378601, 33.370598, -2.2531, 3.3446, 29.8112, -1.4067, 0, 33.3661, -2.2345, 0, 33.370598, -2.2531, 3.3446, 29.7848, -1.4277999, 3.3446, 29.8112, -1.4067, 0, 36.1797, -2.9064, -13.456699, 33.2289, -2.25, -16.7232, 36.2015, -2.8792, -16.7232, 36.1797, -2.9064, -13.456699, 33.1982, -2.3009999, -13.5196, 33.2289, -2.25, -16.7232, 36.2803, -2.8172998, -6.6893, 33.4514, -2.0843, -10.169699, 36.3083, -2.7835999, -10.109299, 36.2803, -2.8172998, -6.6893, 33.4719, -2.0886, -6.6893, 33.4514, -2.0843, -10.169699, 36.1422, -2.9887998, 13.378601, 33.2915, -2.9626, 10.033899, 36.153297, -2.972, 10.033899, 36.1422, -2.9887998, 13.378601, 33.2822, -2.9943, 13.378601, 33.2915, -2.9626, 10.033899, 36.211597, -2.8971, 0, 33.4326, -2.1487, -3.3446002, 36.2533, -2.8497999, -3.3446002, 36.211597, -2.8971, 0, 33.3661, -2.2345, 0, 33.4326, -2.1487, -3.3446002, 36.2075, -2.9348, 20.0679, 33.3428, -2.9598, 16.7232, 36.2028, -2.9338999, 16.7232, 36.2075, -2.9348, 20.0679, 33.3628, -2.9636998, 20.0679, 33.3428, -2.9598, 16.7232, 36.153297, -2.972, 10.033899, 33.347797, -2.2975, 6.6893, 36.1898, -2.9299998, 6.6893, 36.153297, -2.972, 10.033899, 33.2915, -2.9626, 10.033899, 33.347797, -2.2975, 6.6893, 36.1898, -2.9299998, 6.6893, 33.370598, -2.2531, 3.3446, 36.2081, -2.9062, 3.3446, 36.1898, -2.9299998, 6.6893, 33.347797, -2.2975, 6.6893, 33.370598, -2.2531, 3.3446, 36.078598, -3.0746999, 26.7572, 33.3828, -2.9677, 23.412498, 36.2121, -2.9357, 23.412498, 36.078598, -3.0746999, 26.7572, 33.146698, -3.214, 26.7572, 33.3828, -2.9677, 23.412498, 36.2015, -2.8792, -16.7232, 33.1148, -2.3351, -20.0679, 36.1471, -2.928, -20.0679, 36.2015, -2.8792, -16.7232, 33.2289, -2.25, -16.7232, 33.1148, -2.3351, -20.0679, 36.3083, -2.7835999, -10.109299, 33.1982, -2.3009999, -13.5196, 36.1797, -2.9064, -13.456699, 36.3083, -2.7835999, -10.109299, 33.4514, -2.0843, -10.169699, 33.1982, -2.3009999, -13.5196, 36.0678, -2.9959998, -26.7572, 39.877598, -3.6573, -23.412498, 36.1424, -2.9269998, -23.412498, 36.0678, -2.9959998, -26.7572, 39.877598, -3.6808999, -26.7572, 39.877598, -3.6573, -23.412498, 36.2533, -2.8497999, -3.3446002, 33.4719, -2.0886, -6.6893, 36.2803, -2.8172998, -6.6893, 36.2533, -2.8497999, -3.3446002, 33.4326, -2.1487, -3.3446002, 33.4719, -2.0886, -6.6893, 36.2028, -2.9338999, 16.7232, 33.2822, -2.9943, 13.378601, 36.1422, -2.9887998, 13.378601, 36.2028, -2.9338999, 16.7232, 33.3428, -2.9598, 16.7232, 33.2822, -2.9943, 13.378601, 36.2081, -2.9062, 3.3446, 33.3661, -2.2345, 0, 36.211597, -2.8971, 0, 36.2081, -2.9062, 3.3446, 33.370598, -2.2531, 3.3446, 33.3661, -2.2345, 0, 36.2121, -2.9357, 23.412498, 33.3628, -2.9636998, 20.0679, 36.2075, -2.9348, 20.0679, 36.2121, -2.9357, 23.412498, 33.3828, -2.9677, 23.412498, 33.3628, -2.9636998, 20.0679, 42.951, -4.7798, -6.6893, 39.877598, -3.6573, -10.033899, 42.9754, -4.7166, -10.033899, 42.951, -4.7798, -6.6893, 39.877598, -3.7507, -6.6893, 39.877598, -3.6573, -10.033899, 43.1641, -4.7812, 16.7232, 39.877598, -3.7507, 13.378601, 43.0932, -4.8275, 13.378601, 43.1641, -4.7812, 16.7232, 39.877598, -3.6573, 16.7232, 39.877598, -3.7507, 13.378601, 46.338398, -5.9140997, -6.6893, 42.9754, -4.7166, -10.033899, 46.3474, -5.8697, -10.033899, 46.338398, -5.9140997, -6.6893, 42.951, -4.7798, -6.6893, 42.9754, -4.7166, -10.033899, 46.5149, -5.927, 16.7232, 43.0932, -4.8275, 13.378601, 46.4646, -5.9563, 13.378601, 46.5149, -5.927, 16.7232, 43.1641, -4.7812, 16.7232, 43.0932, -4.8275, 13.378601, 49.4614, -6.9598, -6.6893, 46.3474, -5.8697, -10.033899, 49.4666, -6.9364, -10.033899, 49.4614, -6.9598, -6.6893, 46.338398, -5.9140997, -6.6893, 46.3474, -5.8697, -10.033899, 49.5449, -6.9631996, 16.7232, 46.4646, -5.9563, 13.378601, 49.520397, -6.9795, 13.378601, 49.5449, -6.9631996, 16.7232, 46.5149, -5.927, 16.7232, 46.4646, -5.9563, 13.378601, -23.412498, -3.8518999, 39.456898, -26.7572, -3.2777, 36.6322, -23.412498, -3.2477999, 36.6305, -23.412498, -3.8518999, 39.456898, -26.7572, -3.8518999, 39.456898, -26.7572, -3.2777, 36.6322, -16.7232, -3.8518999, 39.456898, -13.378601, -3.243, 36.6252, -13.378601, -3.8518999, 39.456898, -16.7232, -3.8518999, 39.456898, -16.7232, -3.247, 36.6269, -13.378601, -3.243, 36.6252, -13.378601, -1.1209999, 26.7572, -10.033899, -1.8925999, 30.345499, -13.378601, -1.8938999, 30.3512, -10.033899, -1.8925999, 30.345499, -13.378601, -2.619, 33.7235, -13.378601, -1.8938999, 30.3512, -13.378601, -1.1209999, 26.7572, -10.033899, -1.1209999, 26.7572, -10.033899, -1.8925999, 30.345499, -10.033899, -1.8925999, 30.345499, -10.033899, -2.6181998, 33.7199, -13.378601, -2.619, 33.7235, -10.033899, -2.6181998, 33.7199, -13.378601, -3.243, 36.6252, -13.378601, -2.619, 33.7235, -10.033899, -1.1209999, 26.7572, -6.6893, -1.8914, 30.3399, -10.033899, -1.8925999, 30.345499, -6.6893, -1.8914, 30.3399, -10.033899, -2.6181998, 33.7199, -10.033899, -1.8925999, 30.345499, -10.033899, -1.1209999, 26.7572, -6.6893, -1.1209999, 26.7572, -6.6893, -1.8914, 30.3399, -10.033899, -2.6181998, 33.7199, -10.033899, -3.2426, 36.623398, -13.378601, -3.243, 36.6252, -10.033899, -3.2426, 36.623398, -13.378601, -3.8518999, 39.456898, -13.378601, -3.243, 36.6252, -10.033899, -3.2426, 36.623398, -10.033899, -3.8518999, 39.456898, -13.378601, -3.8518999, 39.456898, -6.6893, -2.6174998, 33.7163, -10.033899, -3.2426, 36.623398, -10.033899, -2.6181998, 33.7199, -6.6893, -1.8914, 30.3399, -6.6893, -2.6174998, 33.7163, -10.033899, -2.6181998, 33.7199, -6.6893, -3.2422, 36.6217, -10.033899, -3.8518999, 39.456898, -10.033899, -3.2426, 36.623398, -6.6893, -2.6174998, 33.7163, -6.6893, -3.2422, 36.6217, -10.033899, -3.2426, 36.623398, -6.6893, -3.2422, 36.6217, -6.6893, -3.8518999, 39.456898, -10.033899, -3.8518999, 39.456898, -3.3446002, -1.8901999, 30.3342, -6.6893, -2.6174998, 33.7163, -6.6893, -1.8914, 30.3399, -6.6893, -1.1209999, 26.7572, -3.3446002, -1.8901999, 30.3342, -6.6893, -1.8914, 30.3399, -6.6893, -1.1209999, 26.7572, -3.3446002, -1.1209999, 26.7572, -3.3446002, -1.8901999, 30.3342, -3.3446002, -1.8901999, 30.3342, -3.3446002, -2.6167, 33.7128, -6.6893, -2.6174998, 33.7163, -3.3446002, -2.6167, 33.7128, -6.6893, -3.2422, 36.6217, -6.6893, -2.6174998, 33.7163, -3.3446002, -1.1209999, 26.7572, 0, -1.8889999, 30.3286, -3.3446002, -1.8901999, 30.3342, 0, -1.8889999, 30.3286, -3.3446002, -2.6167, 33.7128, -3.3446002, -1.8901999, 30.3342, -3.3446002, -1.1209999, 26.7572, 0, -1.1209999, 26.7572, 0, -1.8889999, 30.3286, -3.3446002, -3.2417998, 36.6199, -6.6893, -3.8518999, 39.456898, -6.6893, -3.2422, 36.6217, -3.3446002, -2.6167, 33.7128, -3.3446002, -3.2417998, 36.6199, -6.6893, -3.2422, 36.6217, -3.3446002, -3.2417998, 36.6199, -3.3446002, -3.8518999, 39.456898, -6.6893, -3.8518999, 39.456898, 0, -1.8889999, 30.3286, 0, -2.616, 33.709198, -3.3446002, -2.6167, 33.7128, 0, -2.616, 33.709198, -3.3446002, -3.2417998, 36.6199, -3.3446002, -2.6167, 33.7128, 0, -1.1209999, 26.7572, 3.3446, -1.8878, 30.323, 0, -1.8889999, 30.3286, 3.3446, -1.8878, 30.323, 0, -2.616, 33.709198, 0, -1.8889999, 30.3286, 0, -1.1209999, 26.7572, 3.3446, -1.1209999, 26.7572, 3.3446, -1.8878, 30.323, 0, -3.2415, 36.6182, -3.3446002, -3.8518999, 39.456898, -3.3446002, -3.2417998, 36.6199, 0, -2.616, 33.709198, 0, -3.2415, 36.6182, -3.3446002, -3.2417998, 36.6199, 0, -3.2415, 36.6182, 0, -3.8518999, 39.456898, -3.3446002, -3.8518999, 39.456898, 3.3446, -1.8878, 30.323, 3.3446, -2.6152, 33.7057, 0, -2.616, 33.709198, 3.3446, -2.6152, 33.7057, 0, -3.2415, 36.6182, 0, -2.616, 33.709198, 3.3446, -1.1209999, 26.7572, 6.6893, -1.8865999, 30.317299, 3.3446, -1.8878, 30.323, 6.6893, -1.8865999, 30.317299, 3.3446, -2.6152, 33.7057, 3.3446, -1.8878, 30.323, 3.3446, -1.1209999, 26.7572, 6.6893, -1.1209999, 26.7572, 6.6893, -1.8865999, 30.317299, 6.6893, -1.1209999, 26.7572, 10.033899, -1.8853999, 30.311699, 6.6893, -1.8865999, 30.317299, 6.6893, -1.1209999, 26.7572, 10.033899, -1.1209999, 26.7572, 10.033899, -1.8853999, 30.311699, 10.033899, -1.8853999, 30.311699, 6.6893, -2.6144, 33.7021, 6.6893, -1.8865999, 30.317299, 6.6893, -1.8865999, 30.317299, 6.6893, -2.6144, 33.7021, 3.3446, -2.6152, 33.7057, 10.033899, -1.8853999, 30.311699, 10.033899, -2.6137, 33.6986, 6.6893, -2.6144, 33.7021, 3.3446, -2.6152, 33.7057, 3.3446, -3.2410998, 36.616398, 0, -3.2415, 36.6182, 3.3446, -3.2410998, 36.616398, 0, -3.8518999, 39.456898, 0, -3.2415, 36.6182, 6.6893, -2.6144, 33.7021, 3.3446, -3.2410998, 36.616398, 3.3446, -2.6152, 33.7057, 3.3446, -3.2410998, 36.616398, 3.3446, -3.8518999, 39.456898, 0, -3.8518999, 39.456898, 10.033899, -2.6137, 33.6986, 6.6893, -3.2407, 36.6147, 6.6893, -2.6144, 33.7021, 6.6893, -2.6144, 33.7021, 6.6893, -3.2407, 36.6147, 3.3446, -3.2410998, 36.616398, 6.6893, -3.2407, 36.6147, 3.3446, -3.8518999, 39.456898, 3.3446, -3.2410998, 36.616398, 10.033899, -2.6137, 33.6986, 10.033899, -3.2403, 36.6129, 6.6893, -3.2407, 36.6147, 6.6893, -3.2407, 36.6147, 6.6893, -3.8518999, 39.456898, 3.3446, -3.8518999, 39.456898, 10.033899, -3.2403, 36.6129, 6.6893, -3.8518999, 39.456898, 6.6893, -3.2407, 36.6147, 10.033899, -3.2403, 36.6129, 10.033899, -3.8518999, 39.456898, 6.6893, -3.8518999, 39.456898, 10.033899, -1.1209999, 26.7572, 13.378599, -2.6734, 30.306, 10.033899, -1.8853999, 30.311699, 10.033899, -1.1209999, 26.7572, 13.378599, -2.2194998, 26.781, 13.378599, -2.6734, 30.306, 13.378599, -2.2194998, 26.781, 16.7232, -2.6727, 30.3004, 13.378599, -2.6734, 30.306, 16.7232, -2.6727, 30.3004, 13.378599, -3.1099, 33.695, 13.378599, -2.6734, 30.306, 13.378599, -2.2194998, 26.781, 16.7232, -2.2194998, 26.781, 16.7232, -2.6727, 30.3004, 16.7232, -2.6727, 30.3004, 16.7232, -3.1094, 33.691498, 13.378599, -3.1099, 33.695, 16.7232, -3.1094, 33.691498, 13.378599, -3.4854, 36.6112, 13.378599, -3.1099, 33.695, 16.7232, -2.2194998, 26.781, 20.0679, -2.672, 30.294699, 16.7232, -2.6727, 30.3004, 20.0679, -2.672, 30.294699, 16.7232, -3.1094, 33.691498, 16.7232, -2.6727, 30.3004, 16.7232, -2.2194998, 26.781, 20.0679, -2.2194998, 26.781, 20.0679, -2.672, 30.294699, 20.0679, -2.2194998, 26.781, 23.412498, -2.6713, 30.289099, 20.0679, -2.672, 30.294699, 20.0679, -2.2194998, 26.781, 23.412498, -2.2194998, 26.781, 23.412498, -2.6713, 30.289099, 23.412498, -2.6713, 30.289099, 20.0679, -3.109, 33.6879, 20.0679, -2.672, 30.294699, 20.0679, -2.672, 30.294699, 20.0679, -3.109, 33.6879, 16.7232, -3.1094, 33.691498, 23.412498, -2.6713, 30.289099, 23.412498, -3.1085, 33.6844, 20.0679, -3.109, 33.6879, 16.7232, -3.1094, 33.691498, 16.7232, -3.4852, 36.609398, 13.378599, -3.4854, 36.6112, 20.0679, -3.109, 33.6879, 16.7232, -3.4852, 36.609398, 16.7232, -3.1094, 33.691498, 16.7232, -3.4852, 36.609398, 13.378599, -3.8518999, 39.456898, 13.378599, -3.4854, 36.6112, 16.7232, -3.4852, 36.609398, 16.7232, -3.8518999, 39.456898, 13.378599, -3.8518999, 39.456898, 20.0679, -3.109, 33.6879, 20.0679, -3.485, 36.607597, 16.7232, -3.4852, 36.609398, 20.0679, -3.485, 36.607597, 16.7232, -3.8518999, 39.456898, 16.7232, -3.4852, 36.609398, 23.412498, -3.1085, 33.6844, 20.0679, -3.485, 36.607597, 20.0679, -3.109, 33.6879, 20.0679, -3.485, 36.607597, 20.0679, -3.8518999, 39.456898, 16.7232, -3.8518999, 39.456898, 23.412498, -3.1085, 33.6844, 23.412498, -3.4847, 36.6059, 20.0679, -3.485, 36.607597, 23.412498, -3.4847, 36.6059, 20.0679, -3.8518999, 39.456898, 20.0679, -3.485, 36.607597, 23.412498, -3.4847, 36.6059, 23.412498, -3.8518999, 39.456898, 20.0679, -3.8518999, 39.456898, 23.412498, -2.2194998, 26.781, 26.7582, -3.0839999, 30.2834, 23.412498, -2.6713, 30.289099, 23.412498, -2.2194998, 26.781, 26.7582, -2.7837, 26.6955, 26.7582, -3.0839999, 30.2834, -26.7572, -3.2777, 36.6322, -23.412498, -2.6287, 33.7341, -23.412498, -3.2477999, 36.6305, -26.7572, -3.2777, 36.6322, -26.7572, -2.6892, 33.737698, -23.412498, -2.6287, 33.7341, -16.7232, -3.247, 36.6269, -13.378601, -2.619, 33.7235, -13.378601, -3.243, 36.6252, -16.7232, -3.247, 36.6269, -16.7232, -2.6272, 33.726997, -13.378601, -2.619, 33.7235, 26.7582, -3.6130998, 36.6041, 23.412498, -3.8518999, 39.456898, 23.412498, -3.4847, 36.6059, 26.7582, -3.6130998, 36.6041, 26.7582, -3.8518999, 39.456898, 23.412498, -3.8518999, 39.456898, 13.378599, -3.4854, 36.6112, 10.033899, -3.8518999, 39.456898, 10.033899, -3.2403, 36.6129, 13.378599, -3.4854, 36.6112, 13.378599, -3.8518999, 39.456898, 10.033899, -3.8518999, 39.456898, 26.7582, -3.3683999, 33.680798, 23.412498, -3.4847, 36.6059, 23.412498, -3.1085, 33.6844, 26.7582, -3.3683999, 33.680798, 26.7582, -3.6130998, 36.6041, 23.412498, -3.4847, 36.6059, -26.7572, -2.6892, 33.737698, -23.412498, -1.9093, 30.3681, -23.412498, -2.6287, 33.7341, -26.7572, -2.6892, 33.737698, -26.7572, -2.0054, 30.3737, -23.412498, -1.9093, 30.3681, -16.7232, -2.6272, 33.726997, -13.378601, -1.8938999, 30.3512, -13.378601, -2.619, 33.7235, -16.7232, -2.6272, 33.726997, -16.7232, -1.9068999, 30.3568, -13.378601, -1.8938999, 30.3512, 13.378599, -3.1099, 33.695, 10.033899, -3.2403, 36.6129, 10.033899, -2.6137, 33.6986, 13.378599, -3.1099, 33.695, 13.378599, -3.4854, 36.6112, 10.033899, -3.2403, 36.6129, -26.7572, -2.0054, 30.3737, -23.412498, -1.1374999, 26.7572, -23.412498, -1.9093, 30.3681, -26.7572, -2.0054, 30.3737, -26.7572, -1.2701, 26.7572, -23.412498, -1.1374999, 26.7572, 26.7582, -3.0839999, 30.2834, 23.412498, -3.1085, 33.6844, 23.412498, -2.6713, 30.289099, 26.7582, -3.0839999, 30.2834, 26.7582, -3.3683999, 33.680798, 23.412498, -3.1085, 33.6844, 13.378599, -2.6734, 30.306, 10.033899, -2.6137, 33.6986, 10.033899, -1.8853999, 30.311699, 13.378599, -2.6734, 30.306, 13.378599, -3.1099, 33.695, 10.033899, -2.6137, 33.6986, -16.7232, -1.9068999, 30.3568, -13.378601, -1.1209999, 26.7572, -13.378601, -1.8938999, 30.3512, -16.7232, -1.9068999, 30.3568, -16.7232, -1.1374999, 26.7572, -13.378601, -1.1209999, 26.7572, -38.5239, -3.6573, -23.412498, -36.0882, -3.5405, -26.7572, -36.0928, -3.4731998, -23.412498, -38.5239, -3.6573, -23.412498, -38.5239, -3.6573, -26.7572, -36.0882, -3.5405, -26.7572, -38.5239, -3.6573, -20.3888, -36.0928, -3.4731998, -23.412498, -36.0969, -3.2475998, -20.410799, -38.5239, -3.6573, -20.3888, -38.5239, -3.6573, -23.412498, -36.0928, -3.4731998, -23.412498, -38.5239, -3.6573, -20.0679, -36.101498, -3.2484, -17.0662, -38.5239, -3.6573, -17.0441, -36.101498, -3.2484, -17.0662, -38.5239, -3.6573, -20.0679, -38.5239, -3.6573, -20.3888, -36.101498, -3.2484, -17.0662, -38.5239, -3.6573, -20.3888, -36.0969, -3.2475998, -20.410799, -36.0969, -3.2475998, -20.410799, -33.3005, -2.7756, -17.0916, -36.101498, -3.2484, -17.0662, -36.0969, -3.2475998, -20.410799, -33.2906, -2.7739, -20.4364, -33.3005, -2.7756, -17.0916, -33.2906, -2.7739, -20.4364, -30.219301, -2.2554998, -17.1196, -33.3005, -2.7756, -17.0916, -33.2906, -2.7739, -20.4364, -30.2036, -2.2528, -20.4644, -30.219301, -2.2554998, -17.1196, -30.2036, -2.2528, -20.4644, -26.7572, -1.6710999, -17.1511, -30.219301, -2.2554998, -17.1196, -30.2036, -2.2528, -20.4644, -26.7572, -1.6710999, -20.4958, -26.7572, -1.6710999, -17.1511, -38.5239, -3.6573, -16.7232, -36.106598, -2.8727, -13.378599, -38.5239, -3.6573, -13.378599, -36.106598, -2.8727, -13.378599, -38.5239, -3.6573, -16.7232, -38.5239, -3.6573, -17.0441, -36.106598, -2.8727, -13.378599, -38.5239, -3.6573, -17.0441, -36.101498, -3.2484, -17.0662, -38.5239, -3.6573, -10.033899, -36.106598, -2.8727, -13.378599, -36.1112, -2.8741999, -10.033899, -38.5239, -3.6573, -10.033899, -38.5239, -3.6573, -13.378599, -36.106598, -2.8727, -13.378599, -38.5239, -3.6573, -6.6893, -38.5239, -3.6573, -10.033899, -36.1112, -2.8741999, -10.033899, -38.5239, -3.6573, -6.6893, -36.1112, -2.8741999, -10.033899, -36.1158, -2.8757, -6.6893, -36.1112, -2.8741999, -10.033899, -33.3313, -1.9719, -6.6893, -36.1158, -2.8757, -6.6893, -36.106598, -2.8727, -13.378599, -33.3214, -1.9686999, -10.033899, -36.1112, -2.8741999, -10.033899, -36.1112, -2.8741999, -10.033899, -33.3214, -1.9686999, -10.033899, -33.3313, -1.9719, -6.6893, -36.106598, -2.8727, -13.378599, -33.3115, -1.9655, -13.378599, -33.3214, -1.9686999, -10.033899, -33.3214, -1.9686999, -10.033899, -30.2685, -0.9777, -6.6893, -33.3313, -1.9719, -6.6893, -33.3115, -1.9655, -13.378599, -30.252699, -0.9726, -10.033899, -33.3214, -1.9686999, -10.033899, -33.3214, -1.9686999, -10.033899, -30.252699, -0.9726, -10.033899, -30.2685, -0.9777, -6.6893, -30.252699, -0.9726, -10.033899, -29.294899, -0.6617, -6.6893, -30.2685, -0.9777, -6.6893, -33.3115, -1.9655, -13.378599, -30.237001, -0.9675, -13.378599, -30.252699, -0.9726, -10.033899, -30.252699, -0.9726, -10.033899, -29.294899, -0.6617, -10.033899, -29.294899, -0.6617, -6.6893, -30.237001, -0.9675, -13.378599, -29.294899, -0.6617, -10.033899, -30.252699, -0.9726, -10.033899, -30.237001, -0.9675, -13.378599, -29.294899, -0.6617, -13.378599, -29.294899, -0.6617, -10.033899, -38.5239, -3.6573, -3.3446002, -36.1158, -2.8757, -6.6893, -36.1204, -2.9744, -3.3446002, -38.5239, -3.6573, -3.3446002, -38.5239, -3.6573, -6.6893, -36.1158, -2.8757, -6.6893, -38.5239, -3.6573, 0, -36.1204, -2.9744, -3.3446002, -36.125, -2.9757, 0, -38.5239, -3.6573, 0, -38.5239, -3.6573, -3.3446002, -36.1204, -2.9744, -3.3446002, -36.1204, -2.9744, -3.3446002, -33.3512, -2.1875, 0, -36.125, -2.9757, 0, -36.1204, -2.9744, -3.3446002, -33.341198, -2.1847, -3.3446002, -33.3512, -2.1875, 0, -33.341198, -2.1847, -3.3446002, -30.300001, -1.3205999, 0, -33.3512, -2.1875, 0, -33.341198, -2.1847, -3.3446002, -30.2842, -1.3161, -3.3446002, -30.300001, -1.3205999, 0, -30.2842, -1.3161, -3.3446002, -27.9812, -0.6617, 0, -30.300001, -1.3205999, 0, -30.2842, -1.3161, -3.3446002, -27.9812, -0.6617, -3.3446002, -27.9812, -0.6617, 0, -29.294899, -0.6617, 3.3446, -30.3315, -0.9982, 6.6893, -30.315699, -0.9931, 3.3446, -29.294899, -0.6617, 3.3446, -29.294899, -0.6617, 6.6893, -30.3315, -0.9982, 6.6893, -29.294899, -0.6617, 10.033899, -30.3315, -0.9982, 6.6893, -29.294899, -0.6617, 6.6893, -30.3315, -0.9982, 6.6893, -29.294899, -0.6617, 10.033899, -29.294899, -0.6617, 13.378601, -30.3315, -0.9982, 6.6893, -33.3611, -1.9814999, 3.3446, -30.315699, -0.9931, 3.3446, -30.3315, -0.9982, 6.6893, -29.294899, -0.6617, 13.378601, -30.363, -1.0084, 13.378601, -30.3315, -0.9982, 6.6893, -33.371, -1.9848, 6.6893, -33.3611, -1.9814999, 3.3446, -30.363, -1.0084, 13.378601, -33.371, -1.9848, 6.6893, -30.3315, -0.9982, 6.6893, -33.371, -1.9848, 6.6893, -36.1296, -2.8801, 3.3446, -33.3611, -1.9814999, 3.3446, -30.363, -1.0084, 13.378601, -33.3908, -1.9912, 13.378601, -33.371, -1.9848, 6.6893, -33.371, -1.9848, 6.6893, -36.134197, -2.8816, 6.6893, -36.1296, -2.8801, 3.3446, -33.3908, -1.9912, 13.378601, -36.134197, -2.8816, 6.6893, -33.371, -1.9848, 6.6893, -36.134197, -2.8816, 6.6893, -38.5239, -3.6573, 3.3446, -36.1296, -2.8801, 3.3446, -33.3908, -1.9912, 13.378601, -36.1433, -2.8846, 13.378601, -36.134197, -2.8816, 6.6893, -36.134197, -2.8816, 6.6893, -38.5239, -3.6573, 6.6893, -38.5239, -3.6573, 3.3446, -38.5239, -3.6573, 13.378601, -36.134197, -2.8816, 6.6893, -36.1433, -2.8846, 13.378601, -36.134197, -2.8816, 6.6893, -38.5239, -3.6573, 10.033899, -38.5239, -3.6573, 6.6893, -36.134197, -2.8816, 6.6893, -38.5239, -3.6573, 13.378601, -38.5239, -3.6573, 10.033899, -29.294899, -1.0265, 16.7232, -30.394499, -1.8736, 20.0679, -30.378698, -1.3354, 16.7232, -29.294899, -1.0265, 16.7232, -26.7572, -1.0756, 20.0679, -30.394499, -1.8736, 20.0679, -26.7572, -1.0756, 20.0679, -30.4102, -1.8771, 23.412498, -30.394499, -1.8736, 20.0679, -26.7572, -1.0756, 20.0679, -26.7572, -1.0756, 23.412498, -30.4102, -1.8771, 23.412498, -30.4102, -1.8771, 23.412498, -33.4106, -2.5354, 20.0679, -30.394499, -1.8736, 20.0679, -30.4102, -1.8771, 23.412498, -33.420498, -2.5376, 23.412498, -33.4106, -2.5354, 20.0679, -33.420498, -2.5376, 23.412498, -36.1525, -3.1369998, 20.0679, -33.4106, -2.5354, 20.0679, -33.420498, -2.5376, 23.412498, -36.1571, -3.138, 23.412498, -36.1525, -3.1369998, 20.0679, -36.1571, -3.138, 23.412498, -38.5239, -3.6573, 20.0679, -36.1525, -3.1369998, 20.0679, -36.1571, -3.138, 23.412498, -38.5239, -3.6573, 23.412498, -38.5239, -3.6573, 20.0679, -26.7572, -1.0756, 23.412498, -30.426, -2.0144, 26.7572, -30.4102, -1.8771, 23.412498, -26.7572, -1.0756, 23.412498, -26.7572, -1.2701, 26.7572, -30.426, -2.0144, 26.7572, -29.294899, -0.6617, 13.378601, -30.378698, -1.3354, 16.7232, -30.363, -1.0084, 13.378601, -29.294899, -0.6617, 13.378601, -29.294899, -1.0265, 16.7232, -30.378698, -1.3354, 16.7232, -30.173899, -3.2568, -26.7572, -26.7572, -2.7663999, -23.412498, -30.1897, -3.0263, -23.412498, -30.173899, -3.2568, -26.7572, -26.7572, -3.0929, -26.7572, -26.7572, -2.7663999, -23.412498, -30.426, -2.0144, 26.7572, -33.420498, -2.5376, 23.412498, -30.4102, -1.8771, 23.412498, -30.426, -2.0144, 26.7572, -33.4304, -2.6239998, 26.7572, -33.420498, -2.5376, 23.412498, -30.219301, -2.2554998, -17.1196, -29.294899, -0.6617, -13.378599, -30.237001, -0.9675, -13.378599, -30.219301, -2.2554998, -17.1196, -26.7572, -1.6710999, -17.1511, -29.294899, -0.6617, -13.378599, -30.394499, -1.8736, 20.0679, -33.4007, -2.1969, 16.7232, -30.378698, -1.3354, 16.7232, -30.394499, -1.8736, 20.0679, -33.4106, -2.5354, 20.0679, -33.4007, -2.1969, 16.7232, -30.1897, -3.0263, -23.412498, -26.7572, -1.6710999, -20.4958, -30.2036, -2.2528, -20.4644, -30.1897, -3.0263, -23.412498, -26.7572, -2.7663999, -23.412498, -26.7572, -1.6710999, -20.4958, -30.315699, -0.9931, 3.3446, -33.3512, -2.1875, 0, -30.300001, -1.3205999, 0, -30.315699, -0.9931, 3.3446, -33.3611, -1.9814999, 3.3446, -33.3512, -2.1875, 0, -30.2685, -0.9777, -6.6893, -27.9812, -0.6617, -3.3446002, -30.2842, -1.3161, -3.3446002, -30.2685, -0.9777, -6.6893, -29.294899, -0.6617, -6.6893, -27.9812, -0.6617, -3.3446002, -30.378698, -1.3354, 16.7232, -33.3908, -1.9912, 13.378601, -30.363, -1.0084, 13.378601, -30.378698, -1.3354, 16.7232, -33.4007, -2.1969, 16.7232, -33.3908, -1.9912, 13.378601, -33.2719, -3.4053998, -26.7572, -30.1897, -3.0263, -23.412498, -33.2818, -3.2603998, -23.412498, -33.2719, -3.4053998, -26.7572, -30.173899, -3.2568, -26.7572, -30.1897, -3.0263, -23.412498, -33.4007, -2.1969, 16.7232, -36.1433, -2.8846, 13.378601, -33.3908, -1.9912, 13.378601, -33.4007, -2.1969, 16.7232, -36.1479, -2.98, 16.7232, -36.1433, -2.8846, 13.378601, -33.4304, -2.6239998, 26.7572, -36.1571, -3.138, 23.412498, -33.420498, -2.5376, 23.412498, -33.4304, -2.6239998, 26.7572, -36.161697, -3.1780999, 26.7572, -36.1571, -3.138, 23.412498, -33.3005, -2.7756, -17.0916, -30.237001, -0.9675, -13.378599, -33.3115, -1.9655, -13.378599, -33.3005, -2.7756, -17.0916, -30.219301, -2.2554998, -17.1196, -30.237001, -0.9675, -13.378599, -33.2818, -3.2603998, -23.412498, -30.2036, -2.2528, -20.4644, -33.2906, -2.7739, -20.4364, -33.2818, -3.2603998, -23.412498, -30.1897, -3.0263, -23.412498, -30.2036, -2.2528, -20.4644, -33.3611, -1.9814999, 3.3446, -36.125, -2.9757, 0, -33.3512, -2.1875, 0, -33.3611, -1.9814999, 3.3446, -36.1296, -2.8801, 3.3446, -36.125, -2.9757, 0, -33.4106, -2.5354, 20.0679, -36.1479, -2.98, 16.7232, -33.4007, -2.1969, 16.7232, -33.4106, -2.5354, 20.0679, -36.1525, -3.1369998, 20.0679, -36.1479, -2.98, 16.7232, -33.3313, -1.9719, -6.6893, -30.2842, -1.3161, -3.3446002, -33.341198, -2.1847, -3.3446002, -33.3313, -1.9719, -6.6893, -30.2685, -0.9777, -6.6893, -30.2842, -1.3161, -3.3446002, -36.161697, -3.1780999, 26.7572, -38.5239, -3.6573, 23.412498, -36.1571, -3.138, 23.412498, -36.161697, -3.1780999, 26.7572, -38.5239, -3.6573, 26.7572, -38.5239, -3.6573, 23.412498, -36.101498, -3.2484, -17.0662, -33.3115, -1.9655, -13.378599, -36.106598, -2.8727, -13.378599, -36.101498, -3.2484, -17.0662, -33.3005, -2.7756, -17.0916, -33.3115, -1.9655, -13.378599, -36.0928, -3.4731998, -23.412498, -33.2906, -2.7739, -20.4364, -36.0969, -3.2475998, -20.410799, -36.0928, -3.4731998, -23.412498, -33.2818, -3.2603998, -23.412498, -33.2906, -2.7739, -20.4364, -36.1296, -2.8801, 3.3446, -38.5239, -3.6573, 0, -36.125, -2.9757, 0, -36.1296, -2.8801, 3.3446, -38.5239, -3.6573, 3.3446, -38.5239, -3.6573, 0, -36.1158, -2.8757, -6.6893, -33.341198, -2.1847, -3.3446002, -36.1204, -2.9744, -3.3446002, -36.1158, -2.8757, -6.6893, -33.3313, -1.9719, -6.6893, -33.341198, -2.1847, -3.3446002, -36.1525, -3.1369998, 20.0679, -38.5239, -3.6573, 16.7232, -36.1479, -2.98, 16.7232, -36.1525, -3.1369998, 20.0679, -38.5239, -3.6573, 20.0679, -38.5239, -3.6573, 16.7232, -36.0882, -3.5405, -26.7572, -33.2818, -3.2603998, -23.412498, -36.0928, -3.4731998, -23.412498, -36.0882, -3.5405, -26.7572, -33.2719, -3.4053998, -26.7572, -33.2818, -3.2603998, -23.412498, -36.1479, -2.98, 16.7232, -38.5239, -3.6573, 13.378601, -36.1433, -2.8846, 13.378601, -36.1479, -2.98, 16.7232, -38.5239, -3.6573, 16.7232, -38.5239, -3.6573, 13.378601, -41.627, -4.8403997, 26.7572, -38.5239, -3.6573, 26.7572, -40.2616, -4.8514, 28.2242, -44.537098, -5.95, 26.7572, -41.627, -4.8403997, 26.7572, -43.2833, -5.9677997, 28.143099, -47.4473, -7.0596, 26.7572, -44.537098, -5.95, 26.7572, -46.202698, -7.0731997, 28.132399, -53.117798, -8.6939, 26.7572, -50.290497, -8.1437, 26.7572, -51.906998, -8.6923, 28.058699, -55.5951, -9.176, 26.7572, -53.117798, -8.6939, 26.7572, -54.5561, -9.1761, 27.885199, -58.463497, -9.7343, 26.7572, -55.5951, -9.176, 26.7572, -57.2836, -9.7381, 28.062199, -33.0801, -3.7473, 32.6325, -32.8819, -4.9104, 36.1535, -34.7309, -4.8956, 34.1667, -33.0801, -3.7473, 32.6325, -31.247301, -3.7775998, 34.6107, -32.8819, -4.9104, 36.1535, -34.7309, -4.8956, 34.1667, -34.5837, -6.0910997, 37.759598, -36.416798, -6.0650997, 35.7334, -34.7309, -4.8956, 34.1667, -32.8819, -4.9104, 36.1535, -34.5837, -6.0910997, 37.759598, -36.416798, -6.0650997, 35.7334, -36.1511, -7.1832, 39.2388, -37.9957, -7.1629996, 37.2007, -36.416798, -6.0650997, 35.7334, -34.5837, -6.0910997, 37.759598, -36.1511, -7.1832, 39.2388, -37.9957, -7.1629996, 37.2007, -37.5247, -8.1437, 40.5352, -39.4027, -8.1437, 38.5083, -37.9957, -7.1629996, 37.2007, -36.1511, -7.1832, 39.2388, -37.5247, -8.1437, 40.5352, -39.4027, -8.1437, 38.5083, -39.0032, -8.6748, 41.930298, -40.895798, -8.6774, 39.8957, -39.4027, -8.1437, 38.5083, -37.5247, -8.1437, 40.5352, -39.0032, -8.6748, 41.930298, -40.895798, -8.6774, 39.8957, -40.403297, -9.1775, 43.2515, -42.2959, -9.1772995, 41.196598, -40.895798, -8.6774, 39.8957, -39.0032, -8.6748, 41.930298, -40.403297, -9.1775, 43.2515, -42.2959, -9.1772995, 41.196598, -42.1009, -9.787, 44.8533, -43.9867, -9.7809, 42.7677, -42.2959, -9.1772995, 41.196598, -40.403297, -9.1775, 43.2515, -42.1009, -9.787, 44.8533, -43.9867, -9.7809, 42.7677, -43.8985, -10.4337, 46.549698, -45.8121, -10.4337, 44.463898, -43.9867, -9.7809, 42.7677, -42.1009, -9.787, 44.8533, -43.8985, -10.4337, 46.549698, -31.247301, -3.7775998, 34.6107, -31.153502, -4.9242997, 38.010498, -32.8819, -4.9104, 36.1535, -31.247301, -3.7775998, 34.6107, -29.5455, -3.8058, 36.4474, -31.153502, -4.9242997, 38.010498, -32.8819, -4.9104, 36.1535, -32.857998, -6.1155996, 39.6672, -34.5837, -6.0910997, 37.759598, -32.8819, -4.9104, 36.1535, -31.153502, -4.9242997, 38.010498, -32.857998, -6.1155996, 39.6672, -34.5837, -6.0910997, 37.759598, -34.403698, -7.2022996, 41.169598, -36.1511, -7.1832, 39.2388, -34.5837, -6.0910997, 37.759598, -32.857998, -6.1155996, 39.6672, -34.403698, -7.2022996, 41.169598, -36.1511, -7.1832, 39.2388, -35.7366, -8.1437, 42.4652, -37.5247, -8.1437, 40.5352, -36.1511, -7.1832, 39.2388, -34.403698, -7.2022996, 41.169598, -35.7366, -8.1437, 42.4652, -37.5247, -8.1437, 40.5352, -37.1909, -8.672299, 43.878498, -39.0032, -8.6748, 41.930298, -37.5247, -8.1437, 40.5352, -35.7366, -8.1437, 42.4652, -37.1909, -8.672299, 43.878498, -42.1009, -9.787, 44.8533, -42.032497, -10.4337, 48.5836, -43.8985, -10.4337, 46.549698, -42.1009, -9.787, 44.8533, -40.2735, -9.7929, 46.8743, -42.032497, -10.4337, 48.5836, -39.0032, -8.6748, 41.930298, -38.5812, -9.1777, 45.2297, -40.403297, -9.1775, 43.2515, -39.0032, -8.6748, 41.930298, -37.1909, -8.672299, 43.878498, -38.5812, -9.1777, 45.2297, -40.403297, -9.1775, 43.2515, -40.2735, -9.7929, 46.8743, -42.1009, -9.787, 44.8533, -40.403297, -9.1775, 43.2515, -38.5812, -9.1777, 45.2297, -40.2735, -9.7929, 46.8743, -40.2735, -9.7929, 46.8743, -40.572998, -10.4337, 50.1744, -42.032497, -10.4337, 48.5836, -40.2735, -9.7929, 46.8743, -38.8322, -9.7975, 48.4683, -40.572998, -10.4337, 50.1744, -32.857998, -6.1155996, 39.6672, -32.985, -7.2179, 42.7371, -34.403698, -7.2022996, 41.169598, -32.857998, -6.1155996, 39.6672, -31.445799, -6.1355996, 41.2283, -32.985, -7.2179, 42.7371, -38.5812, -9.1777, 45.2297, -38.8322, -9.7975, 48.4683, -40.2735, -9.7929, 46.8743, -38.5812, -9.1777, 45.2297, -37.1326, -9.1778, 46.802498, -38.8322, -9.7975, 48.4683, -34.403698, -7.2022996, 41.169598, -34.2949, -8.1437, 44.0212, -35.7366, -8.1437, 42.4652, -34.403698, -7.2022996, 41.169598, -32.985, -7.2179, 42.7371, -34.2949, -8.1437, 44.0212, -35.7366, -8.1437, 42.4652, -35.7403, -8.6704, 45.4379, -37.1909, -8.672299, 43.878498, -35.7366, -8.1437, 42.4652, -34.2949, -8.1437, 44.0212, -35.7403, -8.6704, 45.4379, -29.5455, -3.8058, 36.4474, -29.727, -4.9357, 39.5433, -31.153502, -4.9242997, 38.010498, -29.5455, -3.8058, 36.4474, -28.1288, -3.8291998, 37.976498, -29.727, -4.9357, 39.5433, -37.1909, -8.672299, 43.878498, -37.1326, -9.1778, 46.802498, -38.5812, -9.1777, 45.2297, -37.1909, -8.672299, 43.878498, -35.7403, -8.6704, 45.4379, -37.1326, -9.1778, 46.802498, -31.153502, -4.9242997, 38.010498, -31.445799, -6.1355996, 41.2283, -32.857998, -6.1155996, 39.6672, -31.153502, -4.9242997, 38.010498, -29.727, -4.9357, 39.5433, -31.445799, -6.1355996, 41.2283, -28.1288, -3.8291998, 37.976498, -28.3464, -4.9467, 41.0267, -29.727, -4.9357, 39.5433, -28.1288, -3.8291998, 37.976498, -26.7572, -3.8518999, 39.456898, -28.3464, -4.9467, 41.0267, -29.727, -4.9357, 39.5433, -30.0795, -6.1549, 42.738598, -31.445799, -6.1355996, 41.2283, -29.727, -4.9357, 39.5433, -28.3464, -4.9467, 41.0267, -30.0795, -6.1549, 42.738598, -38.8322, -9.7975, 48.4683, -39.1638, -10.4337, 51.7104, -40.572998, -10.4337, 50.1744, -38.8322, -9.7975, 48.4683, -37.44, -9.802, 50.008, -39.1638, -10.4337, 51.7104, -31.445799, -6.1355996, 41.2283, -31.612902, -7.2328997, 44.2532, -32.985, -7.2179, 42.7371, -31.445799, -6.1355996, 41.2283, -30.0795, -6.1549, 42.738598, -31.612902, -7.2328997, 44.2532, -37.1326, -9.1778, 46.802498, -37.44, -9.802, 50.008, -38.8322, -9.7975, 48.4683, -37.1326, -9.1778, 46.802498, -35.7329, -9.177899, 48.322197, -37.44, -9.802, 50.008, -32.985, -7.2179, 42.7371, -32.901, -8.1437, 45.5256, -34.2949, -8.1437, 44.0212, -32.985, -7.2179, 42.7371, -31.612902, -7.2328997, 44.2532, -32.901, -8.1437, 45.5256, -34.2949, -8.1437, 44.0212, -34.3383, -8.6685, 46.945, -35.7403, -8.6704, 45.4379, -34.2949, -8.1437, 44.0212, -32.901, -8.1437, 45.5256, -34.3383, -8.6685, 46.945, -35.7403, -8.6704, 45.4379, -35.7329, -9.177899, 48.322197, -37.1326, -9.1778, 46.802498, -35.7403, -8.6704, 45.4379, -34.3383, -8.6685, 46.945, -35.7329, -9.177899, 48.322197, -34.3383, -8.6685, 46.945, -34.0797, -9.1781, 50.1171, -35.7329, -9.177899, 48.322197, -34.3383, -8.6685, 46.945, -32.6904, -8.6662, 48.7166, -34.0797, -9.1781, 50.1171, -26.7572, -3.8518999, 39.456898, -26.7572, -4.9595, 42.7343, -28.3464, -4.9467, 41.0267, -28.3464, -4.9467, 41.0267, -28.496698, -6.1773996, 44.4883, -30.0795, -6.1549, 42.738598, -28.3464, -4.9467, 41.0267, -26.7572, -4.9595, 42.7343, -28.496698, -6.1773996, 44.4883, -37.44, -9.802, 50.008, -37.4804, -10.4337, 53.5453, -39.1638, -10.4337, 51.7104, -37.44, -9.802, 50.008, -35.786, -9.8073, 51.8372, -37.4804, -10.4337, 53.5453, -30.0795, -6.1549, 42.738598, -30.0148, -7.2503, 46.0191, -31.612902, -7.2328997, 44.2532, -30.0795, -6.1549, 42.738598, -28.496698, -6.1773996, 44.4883, -30.0148, -7.2503, 46.0191, -32.901, -8.1437, 45.5256, -32.6904, -8.6662, 48.7166, -34.3383, -8.6685, 46.945, -32.901, -8.1437, 45.5256, -31.270601, -8.1437, 47.285297, -32.6904, -8.6662, 48.7166, -35.7329, -9.177899, 48.322197, -35.786, -9.8073, 51.8372, -37.44, -9.802, 50.008, -35.7329, -9.177899, 48.322197, -34.0797, -9.1781, 50.1171, -35.786, -9.8073, 51.8372, -31.612902, -7.2328997, 44.2532, -31.270601, -8.1437, 47.285297, -32.901, -8.1437, 45.5256, -31.612902, -7.2328997, 44.2532, -30.0148, -7.2503, 46.0191, -31.270601, -8.1437, 47.285297, -26.7572, -4.9595, 42.7343, -26.7572, -6.2019997, 46.4111, -28.496698, -6.1773996, 44.4883, -35.786, -9.8073, 51.8372, -35.735397, -10.4337, 55.4473, -37.4804, -10.4337, 53.5453, -35.786, -9.8073, 51.8372, -34.0528, -9.8129, 53.753998, -35.735397, -10.4337, 55.4473, -28.496698, -6.1773996, 44.4883, -28.2762, -7.2693996, 47.940197, -30.0148, -7.2503, 46.0191, -28.496698, -6.1773996, 44.4883, -26.7572, -6.2019997, 46.4111, -28.2762, -7.2693996, 47.940197, -32.6904, -8.6662, 48.7166, -32.3284, -9.1783, 52.0186, -34.0797, -9.1781, 50.1171, -32.6904, -8.6662, 48.7166, -30.9289, -8.663899, 50.6101, -32.3284, -9.1783, 52.0186, -31.270601, -8.1437, 47.285297, -30.9289, -8.663899, 50.6101, -32.6904, -8.6662, 48.7166, -31.270601, -8.1437, 47.285297, -29.511599, -8.1437, 49.1838, -30.9289, -8.663899, 50.6101, -34.0797, -9.1781, 50.1171, -34.0528, -9.8129, 53.753998, -35.786, -9.8073, 51.8372, -34.0797, -9.1781, 50.1171, -32.3284, -9.1783, 52.0186, -34.0528, -9.8129, 53.753998, -30.0148, -7.2503, 46.0191, -29.511599, -8.1437, 49.1838, -31.270601, -8.1437, 47.285297, -30.0148, -7.2503, 46.0191, -28.2762, -7.2693996, 47.940197, -29.511599, -8.1437, 49.1838, -28.2762, -7.2693996, 47.940197, -27.9641, -8.1437, 50.853897, -29.511599, -8.1437, 49.1838, -28.2762, -7.2693996, 47.940197, -26.7572, -7.286, 49.6186, -27.9641, -8.1437, 50.853897, -34.0528, -9.8129, 53.753998, -34.1461, -10.4337, 57.1796, -35.735397, -10.4337, 55.4473, -34.0528, -9.8129, 53.753998, -32.4888, -9.8179, 55.4837, -34.1461, -10.4337, 57.1796, -26.7572, -6.2019997, 46.4111, -26.7572, -7.286, 49.6186, -28.2762, -7.2693996, 47.940197, -30.9289, -8.663899, 50.6101, -30.7632, -9.1784, 53.718, -32.3284, -9.1783, 52.0186, -30.9289, -8.663899, 50.6101, -29.3669, -8.6617, 52.2893, -30.7632, -9.1784, 53.718, -29.511599, -8.1437, 49.1838, -29.3669, -8.6617, 52.2893, -30.9289, -8.663899, 50.6101, -29.511599, -8.1437, 49.1838, -27.9641, -8.1437, 50.853897, -29.3669, -8.6617, 52.2893, -32.3284, -9.1783, 52.0186, -32.4888, -9.8179, 55.4837, -34.0528, -9.8129, 53.753998, -32.3284, -9.1783, 52.0186, -30.7632, -9.1784, 53.718, -32.4888, -9.8179, 55.4837, -29.3669, -8.6617, 52.2893, -29.525799, -9.1785, 55.061497, -30.7632, -9.1784, 53.718, -29.3669, -8.6617, 52.2893, -28.140299, -8.6601, 53.6078, -29.525799, -9.1785, 55.061497, -27.9641, -8.1437, 50.853897, -28.140299, -8.6601, 53.6078, -29.3669, -8.6617, 52.2893, -27.9641, -8.1437, 50.853897, -26.7572, -8.1437, 52.156597, -28.140299, -8.6601, 53.6078, -30.7632, -9.1784, 53.718, -31.2423, -9.821899, 56.862297, -32.4888, -9.8179, 55.4837, -30.7632, -9.1784, 53.718, -29.525799, -9.1785, 55.061497, -31.2423, -9.821899, 56.862297, -26.7572, -7.286, 49.6186, -26.7572, -8.1437, 52.156597, -27.9641, -8.1437, 50.853897, -32.4888, -9.8179, 55.4837, -32.870197, -10.4337, 58.570297, -34.1461, -10.4337, 57.1796, -32.4888, -9.8179, 55.4837, -31.2423, -9.821899, 56.862297, -32.870197, -10.4337, 58.570297, -28.140299, -8.6601, 53.6078, -28.1322, -9.1786995, 56.5746, -29.525799, -9.1785, 55.061497, -28.140299, -8.6601, 53.6078, -26.7572, -8.658199, 55.0947, -28.1322, -9.1786995, 56.5746, -26.7572, -8.1437, 52.156597, -26.7572, -8.658199, 55.0947, -28.140299, -8.6601, 53.6078, -29.525799, -9.1785, 55.061497, -29.8403, -9.8265, 58.4128, -31.2423, -9.821899, 56.862297, -29.525799, -9.1785, 55.061497, -28.1322, -9.1786995, 56.5746, -29.8403, -9.8265, 58.4128, -31.2423, -9.821899, 56.862297, -31.437601, -10.4337, 60.131798, -32.870197, -10.4337, 58.570297, -31.2423, -9.821899, 56.862297, -29.8403, -9.8265, 58.4128, -31.437601, -10.4337, 60.131798, -26.7572, -8.658199, 55.0947, -26.7572, -9.1788, 58.067497, -28.1322, -9.1786995, 56.5746, -28.1322, -9.1786995, 56.5746, -28.4674, -9.8309, 59.9312, -29.8403, -9.8265, 58.4128, -28.1322, -9.1786995, 56.5746, -26.7572, -9.1788, 58.067497, -28.4674, -9.8309, 59.9312, -29.8403, -9.8265, 58.4128, -30.044802, -10.4337, 61.649998, -31.437601, -10.4337, 60.131798, -29.8403, -9.8265, 58.4128, -28.4674, -9.8309, 59.9312, -30.044802, -10.4337, 61.649998, -28.4674, -9.8309, 59.9312, -28.2834, -10.4337, 63.569798, -30.044802, -10.4337, 61.649998, -28.4674, -9.8309, 59.9312, -26.7572, -9.8364, 61.822598, -28.2834, -10.4337, 63.569798, -26.7572, -9.1788, 58.067497, -26.7572, -9.8364, 61.822598, -28.4674, -9.8309, 59.9312, -26.7572, -9.8364, 61.822598, -26.7572, -10.4337, 65.2334, -28.2834, -10.4337, 63.569798, -39.7387, -7.144, 35.2747, -39.4027, -8.1437, 38.5083, -41.179398, -8.1437, 36.5907, -39.7387, -7.144, 35.2747, -37.9957, -7.1629996, 37.2007, -39.4027, -8.1437, 38.5083, -34.806297, -3.7187998, 30.769499, -34.7309, -4.8956, 34.1667, -36.474598, -4.8817, 32.293198, -34.806297, -3.7187998, 30.769499, -33.0801, -3.7473, 32.6325, -34.7309, -4.8956, 34.1667, -45.7693, -9.7752, 40.7963, -45.8121, -10.4337, 44.463898, -47.620697, -10.4337, 42.4927, -45.7693, -9.7752, 40.7963, -43.9867, -9.7809, 42.7677, -45.8121, -10.4337, 44.463898, -36.474598, -4.8817, 32.293198, -36.416798, -6.0650997, 35.7334, -38.147, -6.0406, 33.8208, -36.474598, -4.8817, 32.293198, -34.7309, -4.8956, 34.1667, -36.416798, -6.0650997, 35.7334, -38.147, -6.0406, 33.8208, -37.9957, -7.1629996, 37.2007, -39.7387, -7.144, 35.2747, -38.147, -6.0406, 33.8208, -36.416798, -6.0650997, 35.7334, -37.9957, -7.1629996, 37.2007, -41.179398, -8.1437, 36.5907, -40.895798, -8.6774, 39.8957, -42.6861, -8.6798, 37.9712, -41.179398, -8.1437, 36.5907, -39.4027, -8.1437, 38.5083, -40.895798, -8.6774, 39.8957, -42.6861, -8.6798, 37.9712, -42.2959, -9.1772995, 41.196598, -44.0855, -9.1771, 39.253498, -42.6861, -8.6798, 37.9712, -40.895798, -8.6774, 39.8957, -42.2959, -9.1772995, 41.196598, -44.0855, -9.1771, 39.253498, -43.9867, -9.7809, 42.7677, -45.7693, -9.7752, 40.7963, -44.0855, -9.1771, 39.253498, -42.2959, -9.1772995, 41.196598, -43.9867, -9.7809, 42.7677, -41.209297, -7.1278996, 33.6499, -41.179398, -8.1437, 36.5907, -42.6902, -8.1437, 34.960197, -41.209297, -7.1278996, 33.6499, -39.7387, -7.144, 35.2747, -41.179398, -8.1437, 36.5907, -36.2241, -3.6952999, 29.2393, -36.474598, -4.8817, 32.293198, -37.9203, -4.8701, 30.739899, -36.2241, -3.6952999, 29.2393, -34.806297, -3.7187998, 30.769499, -36.474598, -4.8817, 32.293198, -47.322998, -9.7702, 39.078, -47.620697, -10.4337, 42.4927, -49.212498, -10.4337, 40.7576, -47.322998, -9.7702, 39.078, -45.7693, -9.7752, 40.7963, -47.620697, -10.4337, 42.4927, -37.9203, -4.8701, 30.739899, -38.147, -6.0406, 33.8208, -39.5943, -6.0200996, 32.221, -37.9203, -4.8701, 30.739899, -36.474598, -4.8817, 32.293198, -38.147, -6.0406, 33.8208, -39.5943, -6.0200996, 32.221, -39.7387, -7.144, 35.2747, -41.209297, -7.1278996, 33.6499, -39.5943, -6.0200996, 32.221, -38.147, -6.0406, 33.8208, -39.7387, -7.144, 35.2747, -42.6902, -8.1437, 34.960197, -42.6861, -8.6798, 37.9712, -44.2212, -8.6819, 36.321, -42.6902, -8.1437, 34.960197, -41.179398, -8.1437, 36.5907, -42.6861, -8.6798, 37.9712, -44.2212, -8.6819, 36.321, -44.0855, -9.1771, 39.253498, -45.6317, -9.177, 37.5747, -44.2212, -8.6819, 36.321, -42.6861, -8.6798, 37.9712, -44.0855, -9.1771, 39.253498, -45.6317, -9.177, 37.5747, -45.7693, -9.7752, 40.7963, -47.322998, -9.7702, 39.078, -45.6317, -9.177, 37.5747, -44.0855, -9.1771, 39.253498, -45.7693, -9.7752, 40.7963, -42.4946, -7.1138, 32.2296, -42.6902, -8.1437, 34.960197, -44.0089, -8.1437, 33.5369, -42.4946, -7.1138, 32.2296, -41.209297, -7.1278996, 33.6499, -42.6902, -8.1437, 34.960197, -37.4711, -3.6747, 27.8934, -37.9203, -4.8701, 30.739899, -39.1893, -4.8599997, 29.3764, -37.4711, -3.6747, 27.8934, -36.2241, -3.6952999, 29.2393, -37.9203, -4.8701, 30.739899, -48.6723, -9.7657995, 37.585697, -49.212498, -10.4337, 40.7576, -50.5928, -10.4337, 39.2532, -48.6723, -9.7657995, 37.585697, -47.322998, -9.7702, 39.078, -49.212498, -10.4337, 40.7576, -39.1893, -4.8599997, 29.3764, -39.5943, -6.0200996, 32.221, -40.8617, -6.0021, 30.82, -39.1893, -4.8599997, 29.3764, -37.9203, -4.8701, 30.739899, -39.5943, -6.0200996, 32.221, -40.8617, -6.0021, 30.82, -41.209297, -7.1278996, 33.6499, -42.4946, -7.1138, 32.2296, -40.8617, -6.0021, 30.82, -39.5943, -6.0200996, 32.221, -41.209297, -7.1278996, 33.6499, -44.0089, -8.1437, 33.5369, -44.2212, -8.6819, 36.321, -45.559097, -8.6837, 34.882698, -44.0089, -8.1437, 33.5369, -42.6902, -8.1437, 34.960197, -44.2212, -8.6819, 36.321, -45.559097, -8.6837, 34.882698, -45.6317, -9.177, 37.5747, -46.9771, -9.1769, 36.114, -45.559097, -8.6837, 34.882698, -44.2212, -8.6819, 36.321, -45.6317, -9.177, 37.5747, -46.9771, -9.1769, 36.114, -47.322998, -9.7702, 39.078, -48.6723, -9.7657995, 37.585697, -46.9771, -9.1769, 36.114, -45.6317, -9.177, 37.5747, -47.322998, -9.7702, 39.078, -43.5821, -7.1018996, 31.028, -44.0089, -8.1437, 33.5369, -45.125797, -8.1437, 32.3315, -43.5821, -7.1018996, 31.028, -42.4946, -7.1138, 32.2296, -44.0089, -8.1437, 33.5369, -38.5239, -3.6573, 26.7572, -39.1893, -4.8599997, 29.3764, -40.2616, -4.8514, 28.2242, -38.5239, -3.6573, 26.7572, -37.4711, -3.6747, 27.8934, -39.1893, -4.8599997, 29.3764, -49.816998, -9.7621, 36.319798, -50.5928, -10.4337, 39.2532, -51.764698, -10.4337, 37.9757, -49.816998, -9.7621, 36.319798, -48.6723, -9.7657995, 37.585697, -50.5928, -10.4337, 39.2532, -40.2616, -4.8514, 28.2242, -40.8617, -6.0021, 30.82, -41.9332, -5.9869, 29.6356, -40.2616, -4.8514, 28.2242, -39.1893, -4.8599997, 29.3764, -40.8617, -6.0021, 30.82, -41.9332, -5.9869, 29.6356, -42.4946, -7.1138, 32.2296, -43.5821, -7.1018996, 31.028, -41.9332, -5.9869, 29.6356, -40.8617, -6.0021, 30.82, -42.4946, -7.1138, 32.2296, -45.125797, -8.1437, 32.3315, -45.559097, -8.6837, 34.882698, -46.692997, -8.6852, 33.6638, -45.125797, -8.1437, 32.3315, -44.0089, -8.1437, 33.5369, -45.559097, -8.6837, 34.882698, -46.692997, -8.6852, 33.6638, -46.9771, -9.1769, 36.114, -48.118, -9.1768, 34.875298, -46.692997, -8.6852, 33.6638, -45.559097, -8.6837, 34.882698, -46.9771, -9.1769, 36.114, -48.118, -9.1768, 34.875298, -48.6723, -9.7657995, 37.585697, -49.816998, -9.7621, 36.319798, -48.118, -9.1768, 34.875298, -46.9771, -9.1769, 36.114, -48.6723, -9.7657995, 37.585697, -44.939, -7.0871, 29.5287, -45.125797, -8.1437, 32.3315, -46.506897, -8.1437, 30.840899, -44.939, -7.0871, 29.5287, -43.5821, -7.1018996, 31.028, -45.125797, -8.1437, 32.3315, -51.1965, -9.7577, 34.794, -51.764698, -10.4337, 37.9757, -53.163, -10.4337, 36.4517, -51.1965, -9.7577, 34.794, -49.816998, -9.7621, 36.319798, -51.764698, -10.4337, 37.9757, -41.627, -4.8403997, 26.7572, -41.9332, -5.9869, 29.6356, -43.2833, -5.9677997, 28.143099, -41.627, -4.8403997, 26.7572, -40.2616, -4.8514, 28.2242, -41.9332, -5.9869, 29.6356, -43.2833, -5.9677997, 28.143099, -43.5821, -7.1018996, 31.028, -44.939, -7.0871, 29.5287, -43.2833, -5.9677997, 28.143099, -41.9332, -5.9869, 29.6356, -43.5821, -7.1018996, 31.028, -46.506897, -8.1437, 30.840899, -46.692997, -8.6852, 33.6638, -48.083, -8.687099, 32.1695, -46.506897, -8.1437, 30.840899, -45.125797, -8.1437, 32.3315, -46.692997, -8.6852, 33.6638, -48.083, -8.687099, 32.1695, -48.118, -9.1768, 34.875298, -49.5056, -9.1765995, 33.3687, -48.083, -8.687099, 32.1695, -46.692997, -8.6852, 33.6638, -48.118, -9.1768, 34.875298, -49.5056, -9.1765995, 33.3687, -49.816998, -9.7621, 36.319798, -51.1965, -9.7577, 34.794, -49.5056, -9.1765995, 33.3687, -48.118, -9.1768, 34.875298, -49.816998, -9.7621, 36.319798, -46.202698, -7.0731997, 28.132399, -46.506897, -8.1437, 30.840899, -47.7968, -8.1437, 29.4487, -46.202698, -7.0731997, 28.132399, -44.939, -7.0871, 29.5287, -46.506897, -8.1437, 30.840899, -52.4946, -9.7535, 33.3584, -53.163, -10.4337, 36.4517, -54.4828, -10.4337, 35.0131, -52.4946, -9.7535, 33.3584, -51.1965, -9.7577, 34.794, -53.163, -10.4337, 36.4517, -44.537098, -5.95, 26.7572, -44.939, -7.0871, 29.5287, -46.202698, -7.0731997, 28.132399, -44.537098, -5.95, 26.7572, -43.2833, -5.9677997, 28.143099, -44.939, -7.0871, 29.5287, -47.7968, -8.1437, 29.4487, -48.083, -8.687099, 32.1695, -49.384697, -8.6889, 30.770199, -47.7968, -8.1437, 29.4487, -46.506897, -8.1437, 30.840899, -48.083, -8.687099, 32.1695, -49.384697, -8.6889, 30.770199, -49.5056, -9.1765995, 33.3687, -50.808, -9.176499, 31.954699, -49.384697, -8.6889, 30.770199, -48.083, -8.687099, 32.1695, -49.5056, -9.1765995, 33.3687, -50.808, -9.176499, 31.954699, -51.1965, -9.7577, 34.794, -52.4946, -9.7535, 33.3584, -50.808, -9.176499, 31.954699, -49.5056, -9.1765995, 33.3687, -51.1965, -9.7577, 34.794, -47.4473, -7.0596, 26.7572, -47.7968, -8.1437, 29.4487, -49.074398, -8.1437, 28.0697, -47.4473, -7.0596, 26.7572, -46.202698, -7.0731997, 28.132399, -47.7968, -8.1437, 29.4487, -53.7992, -9.7493, 31.915699, -54.4828, -10.4337, 35.0131, -55.8173, -10.4337, 33.5585, -53.7992, -9.7493, 31.915699, -52.4946, -9.7535, 33.3584, -54.4828, -10.4337, 35.0131, -49.074398, -8.1437, 28.0697, -49.384697, -8.6889, 30.770199, -50.6806, -8.690599, 29.3771, -49.074398, -8.1437, 28.0697, -47.7968, -8.1437, 29.4487, -49.384697, -8.6889, 30.770199, -50.6806, -8.690599, 29.3771, -50.808, -9.176499, 31.954699, -52.1102, -9.1764, 30.5408, -50.6806, -8.690599, 29.3771, -49.384697, -8.6889, 30.770199, -50.808, -9.176499, 31.954699, -52.1102, -9.1764, 30.5408, -52.4946, -9.7535, 33.3584, -53.7992, -9.7493, 31.915699, -52.1102, -9.1764, 30.5408, -50.808, -9.176499, 31.954699, -52.4946, -9.7535, 33.3584, -50.290497, -8.1437, 26.7572, -50.6806, -8.690599, 29.3771, -51.906998, -8.6923, 28.058699, -50.290497, -8.1437, 26.7572, -49.074398, -8.1437, 28.0697, -50.6806, -8.690599, 29.3771, -51.906998, -8.6923, 28.058699, -52.1102, -9.1764, 30.5408, -53.3363, -9.1763, 29.2095, -51.906998, -8.6923, 28.058699, -50.6806, -8.690599, 29.3771, -52.1102, -9.1764, 30.5408, -55.0202, -9.745399, 30.5653, -55.8173, -10.4337, 33.5585, -57.058, -10.4337, 32.2062, -55.0202, -9.745399, 30.5653, -53.7992, -9.7493, 31.915699, -55.8173, -10.4337, 33.5585, -53.3363, -9.1763, 29.2095, -53.7992, -9.7493, 31.915699, -55.0202, -9.745399, 30.5653, -53.3363, -9.1763, 29.2095, -52.1102, -9.1764, 30.5408, -53.7992, -9.7493, 31.915699, -53.117798, -8.6939, 26.7572, -53.3363, -9.1763, 29.2095, -54.5561, -9.1761, 27.885199, -53.117798, -8.6939, 26.7572, -51.906998, -8.6923, 28.058699, -53.3363, -9.1763, 29.2095, -56.245598, -9.7414, 29.2101, -57.058, -10.4337, 32.2062, -58.316597, -10.4337, 30.834398, -56.245598, -9.7414, 29.2101, -55.0202, -9.745399, 30.5653, -57.058, -10.4337, 32.2062, -54.5561, -9.1761, 27.885199, -55.0202, -9.745399, 30.5653, -56.245598, -9.7414, 29.2101, -54.5561, -9.1761, 27.885199, -53.3363, -9.1763, 29.2095, -55.0202, -9.745399, 30.5653, -57.2836, -9.7381, 28.062199, -58.316597, -10.4337, 30.834398, -59.3761, -10.4337, 29.679499, -57.2836, -9.7381, 28.062199, -56.245598, -9.7414, 29.2101, -58.316597, -10.4337, 30.834398, -55.5951, -9.176, 26.7572, -56.245598, -9.7414, 29.2101, -57.2836, -9.7381, 28.062199, -55.5951, -9.176, 26.7572, -54.5561, -9.1761, 27.885199, -56.245598, -9.7414, 29.2101, -58.463497, -9.7343, 26.7572, -59.3761, -10.4337, 29.679499, -60.611, -10.4337, 28.3335, -58.463497, -9.7343, 26.7572, -57.2836, -9.7381, 28.062199, -59.3761, -10.4337, 29.679499, 20.0676, 0.0021, 3.3446, 23.4123, 0, -0.000100016594, 23.4123, 0, 3.3446, 20.0676, 0.0021, 3.3446, 20.229, 0.030399999, -0.05219996, 23.4123, 0, -0.000100016594, 6.6893, 3.3913999, 3.3446, 10.0336, 3.1929998, -0.000100016594, 10.0336, 3.1929998, 3.3446, 6.6893, 3.3913999, 3.3446, 6.6893, 3.3913999, -0.000100016594, 10.0336, 3.1929998, -0.000100016594, 23.4123, 0, 3.3446, 26.7582, -0.83089995, 6.6893, 23.4123, 0, 6.6893, 23.4123, 0, 3.3446, 26.7575, -0.7312, 3.3446, 26.7582, -0.83089995, 6.6893, 23.4123, 0, 6.6893, 26.7582, -0.83089995, 6.6893, 23.4123, -0.35529998, 10.033899, 20.0676, 0.0021, 3.3446, 20.0676, 0, 10.0338, 17.491, 0.0034999999, 7.408, 17.491, 0.0034999999, 7.408, 14.0257, 0.0442, 7.3251996, 20.0676, 0.0021, 3.3446, 20.0676, 0, 10.0338, 23.4123, 0, 6.6893, 23.4123, -0.35529998, 10.033899, 20.0676, 0.0021, 3.3446, 23.4123, 0, 3.3446, 23.4123, 0, 6.6893, 12.7055, 1.032, 4.0926, 11.277899, 2.3706, -0.000100016594, 14.261799, 0.9069, 1.1804999, 12.7055, 1.032, 4.0926, 11.2782, 2.1174, 3.3446, 11.277899, 2.3706, -0.000100016594, 10.0336, 3.1929998, 3.3446, 11.277899, 2.3706, -0.000100016594, 11.2782, 2.1174, 3.3446, 10.0336, 3.1929998, 3.3446, 10.0336, 3.1929998, -0.000100016594, 11.277899, 2.3706, -0.000100016594, 20.229, 0.030399999, -0.05219996, 20.0676, 0.0021, 3.3446, 14.261799, 0.9069, 1.1804999, 14.0257, 0.0442, 7.3251996, 14.261799, 0.9069, 1.1804999, 20.0676, 0.0021, 3.3446, 12.7055, 1.032, 4.0926, 14.261799, 0.9069, 1.1804999, 14.0257, 0.0442, 7.3251996, 26.7575, -0.7312, 3.3446, 23.4123, 0, 3.3446, 23.4123, 0, -0.000100016594, 20.0676, 0, 10.0338, 20.0676, 0.0021, 3.3446, 23.4123, 0, 6.6893, 17.253021, -20.068901, 7.9789844, 17.227175, -19.735691, 9.261204, 17.026821, -20.231876, 8.073181, 17.253021, -20.068901, 7.9789844, 17.434578, -19.605183, 9.12792, 17.227175, -19.735691, 9.261204, 17.434578, -19.605183, 9.12792, 17.571707, -18.471199, 10.41548, 17.227175, -19.735691, 9.261204, 17.434578, -19.605183, 9.12792, 17.629164, -18.471626, 10.358021, 17.571707, -18.471199, 10.41548, 17.227175, -19.735691, 9.261204, 17.571707, -18.471199, 10.41548, 17.608658, -18.470558, 10.487461, 17.026821, -20.231876, 8.073181, 17.227175, -19.735691, 9.261204, 17.33739, -19.946938, 9.406449, 17.227175, -19.735691, 9.261204, 17.608658, -18.470558, 10.487461, 17.33739, -19.946938, 9.406449, 17.026821, -20.231876, 8.073181, 17.33739, -19.946938, 9.406449, 17.131271, -20.495667, 8.150929, 17.33739, -19.946938, 9.406449, 17.608658, -18.470558, 10.487461, 17.688543, -18.470558, 10.474859, 17.131271, -20.495667, 8.150929, 17.33739, -19.946938, 9.406449, 17.61293, -19.946938, 9.362875, 17.33739, -19.946938, 9.406449, 17.688543, -18.470558, 10.474859, 17.61293, -19.946938, 9.362875, 17.131271, -20.495667, 8.150929, 17.61293, -19.946938, 9.362875, 17.421762, -20.495667, 8.105006, 17.61293, -19.946938, 9.362875, 17.688543, -18.470558, 10.474859, 17.701359, -18.471199, 10.394974, 17.421762, -20.495667, 8.105006, 17.61293, -19.946938, 9.362875, 17.67295, -19.735691, 9.190718, 17.61293, -19.946938, 9.362875, 17.701359, -18.471199, 10.394974, 17.67295, -19.735691, 9.190718, 17.67295, -19.735691, 9.190718, 17.701359, -18.471199, 10.394974, 17.629164, -18.471626, 10.358021, 17.421762, -20.495667, 8.105006, 17.67295, -19.735691, 9.190718, 17.497162, -20.231876, 7.998849, 17.67295, -19.735691, 9.190718, 17.629164, -18.471626, 10.358021, 17.434578, -19.605183, 9.12792, 17.497162, -20.231876, 7.998849, 17.67295, -19.735691, 9.190718, 17.434578, -19.605183, 9.12792, 17.497162, -20.231876, 7.998849, 17.434578, -19.605183, 9.12792, 17.253021, -20.068901, 7.9789844, 16.28308, -22.030998, 7.9881697, 17.28207, -19.97385, 8.213941, 16.7107, -22.030998, 8.612513, 16.28308, -22.030998, 7.9881697, 17.120804, -19.944588, 7.9881687, 17.28207, -19.97385, 8.213941, 17.120804, -19.944588, 7.9881687, 17.36345, -17.907732, 8.087917, 17.28207, -19.97385, 8.213941, 17.120804, -19.944588, 7.9881687, 17.291254, -17.900043, 7.9881682, 17.36345, -17.907732, 8.087917, 17.28207, -19.97385, 8.213941, 17.36345, -17.907732, 8.087917, 17.480074, -17.92012, 8.049685, 16.7107, -22.030998, 8.612513, 17.28207, -19.97385, 8.213941, 17.543299, -20.021269, 8.127647, 17.28207, -19.97385, 8.213941, 17.480074, -17.92012, 8.049685, 17.543299, -20.021269, 8.127647, 16.7107, -22.030998, 8.612513, 17.543299, -20.021269, 8.127647, 17.402325, -22.030998, 8.373924, 17.543299, -20.021269, 8.127647, 17.480074, -17.92012, 8.049685, 17.480074, -17.92012, 7.926439, 17.402325, -22.030998, 8.373924, 17.543299, -20.021269, 8.127647, 17.543299, -20.02127, 7.8486905, 17.543299, -20.021269, 8.127647, 17.480074, -17.92012, 7.926439, 17.543299, -20.02127, 7.8486905, 17.402325, -22.030998, 8.373924, 17.543299, -20.02127, 7.8486905, 17.402325, -22.030998, 7.602201, 17.543299, -20.02127, 7.8486905, 17.480074, -17.92012, 7.926439, 17.36345, -17.907732, 7.8884187, 17.402325, -22.030998, 7.602201, 17.543299, -20.02127, 7.8486905, 17.28207, -19.97385, 7.7623973, 17.543299, -20.02127, 7.8486905, 17.36345, -17.907732, 7.8884187, 17.28207, -19.97385, 7.7623973, 17.28207, -19.97385, 7.7623973, 17.36345, -17.907732, 7.8884187, 17.291254, -17.900043, 7.9881682, 17.402325, -22.030998, 7.602201, 17.28207, -19.97385, 7.7623973, 16.7107, -22.030998, 7.363827, 17.28207, -19.97385, 7.7623973, 17.291254, -17.900043, 7.9881682, 17.120804, -19.944588, 7.9881687, 16.7107, -22.030998, 7.363827, 17.28207, -19.97385, 7.7623973, 17.120804, -19.944588, 7.9881687, 16.7107, -22.030998, 7.363827, 17.120804, -19.944588, 7.9881687, 16.28308, -22.030998, 7.9881697, 17.194283, -19.802975, 7.602841, 18.323994, -19.463997, 6.9496627, 17.395275, -19.968939, 7.750009, 17.194283, -19.802975, 7.602841, 18.09865, -19.331139, 6.83902, 18.323994, -19.463997, 6.9496627, 18.09865, -19.331139, 6.83902, 19.147194, -18.177078, 6.041237, 18.323994, -19.463997, 6.9496627, 18.09865, -19.331139, 6.83902, 19.067095, -18.177505, 6.0213723, 19.147194, -18.177078, 6.041237, 18.323994, -19.463997, 6.9496627, 19.147194, -18.177078, 6.041237, 19.190767, -18.176437, 5.9711766, 17.395275, -19.968939, 7.750009, 18.323994, -19.463997, 6.9496627, 18.392344, -19.679089, 6.777077, 18.323994, -19.463997, 6.9496627, 19.190767, -18.176437, 5.9711766, 18.392344, -19.679089, 6.777077, 17.395275, -19.968939, 7.750009, 18.392344, -19.679089, 6.777077, 17.408092, -20.237429, 7.618006, 18.392344, -19.679089, 6.777077, 19.190767, -18.176437, 5.9711766, 19.137367, -18.176437, 5.908166, 17.408092, -20.237429, 7.618006, 18.392344, -19.679089, 6.777077, 18.209078, -19.679089, 6.560063, 18.392344, -19.679089, 6.777077, 19.137367, -18.176437, 5.908166, 18.209078, -19.679089, 6.560063, 17.408092, -20.237429, 7.618006, 18.209078, -19.679089, 6.560063, 17.214787, -20.237429, 7.389458, 18.209078, -19.679089, 6.560063, 19.137367, -18.176437, 5.908166, 19.061113, -18.177078, 5.9391375, 17.214787, -20.237429, 7.389458, 18.209078, -19.679089, 6.560063, 18.027735, -19.463997, 6.5985103, 18.209078, -19.679089, 6.560063, 19.061113, -18.177078, 5.9391375, 18.027735, -19.463997, 6.5985103, 18.027735, -19.463997, 6.5985103, 19.061113, -18.177078, 5.9391375, 19.067095, -18.177505, 6.0213723, 17.214787, -20.237429, 7.389458, 18.027735, -19.463997, 6.5985103, 17.082785, -19.968939, 7.379846, 18.027735, -19.463997, 6.5985103, 19.067095, -18.177505, 6.0213723, 18.09865, -19.331139, 6.83902, 17.082785, -19.968939, 7.379846, 18.027735, -19.463997, 6.5985103, 18.09865, -19.331139, 6.83902, 17.082785, -19.968939, 7.379846, 18.09865, -19.331139, 6.83902, 17.194283, -19.802975, 7.602841, 19.916355, -18.10766, 4.50975, 18.669378, -17.361996, 4.594761, 18.997677, -18.38085, 4.7551727, 18.997677, -18.38085, 4.7551727, 18.669378, -17.361996, 4.594761, 17.938025, -17.356228, 5.7366476, 18.997677, -18.38085, 4.7551727, 17.938025, -17.356228, 5.7366476, 18.45151, -18.10766, 5.7464733, 19.916355, -18.10766, 4.50975, 19.861246, -17.356228, 4.1126738, 18.669378, -17.361996, 4.594761, 20.759848, -18.352869, 5.1768117, 19.861246, -17.356228, 4.1126738, 19.916355, -18.10766, 4.50975, 18.669378, -17.361996, 4.594761, 18.361372, -16.59625, 5.750745, 17.938025, -17.356228, 5.7366476, 20.759848, -18.352869, 5.1768117, 20.69171, -17.339567, 4.9839334, 19.861246, -17.356228, 4.1126738, 20.916626, -18.072844, 6.2943482, 20.69171, -17.339567, 4.9839334, 20.759848, -18.352869, 5.1768117, 19.861246, -17.356228, 4.1126738, 18.80138, -16.33182, 5.093936, 18.669378, -17.361996, 4.594761, 18.669378, -17.361996, 4.594761, 18.80138, -16.33182, 5.093936, 18.361372, -16.59625, 5.750745, 20.69171, -17.339567, 4.9839334, 19.58528, -16.596252, 4.7173653, 19.861246, -17.356228, 4.1126738, 19.861246, -17.356228, 4.1126738, 19.58528, -16.596252, 4.7173653, 18.80138, -16.33182, 5.093936, 20.916626, -18.072844, 6.2943482, 20.971521, -17.322693, 6.3201933, 20.69171, -17.339567, 4.9839334, 20.971521, -17.322693, 6.3201933, 20.916626, -18.072844, 6.2943482, 20.598795, -18.324886, 7.2226386, 20.69171, -17.339567, 4.9839334, 20.07292, -16.326265, 5.255842, 19.58528, -16.596252, 4.7173653, 20.971521, -17.322693, 6.3201933, 20.07292, -16.326265, 5.255842, 20.69171, -17.339567, 4.9839334, 19.58528, -16.596252, 4.7173653, 19.312944, -15.988569, 5.813969, 18.80138, -16.33182, 5.093936, 19.58528, -16.596252, 4.7173653, 20.07292, -16.326265, 5.255842, 19.312944, -15.988569, 5.813969, 20.971521, -17.322693, 6.3201933, 20.39054, -16.581512, 6.067722, 20.07292, -16.326265, 5.255842, 20.971521, -17.322693, 6.3201933, 20.598795, -18.324886, 7.2226386, 20.51122, -17.317139, 7.2331047, 20.39054, -16.581512, 6.067722, 20.971521, -17.322693, 6.3201933, 20.51122, -17.317139, 7.2331047, 20.51122, -17.317139, 7.2331047, 20.598795, -18.324886, 7.2226386, 19.692719, -18.072844, 7.327728, 20.07292, -16.326265, 5.255842, 20.39054, -16.581512, 6.067722, 19.963133, -16.320711, 6.583985, 20.39054, -16.581512, 6.067722, 20.51122, -17.317139, 7.2331047, 19.963133, -16.320711, 6.583985, 20.07292, -16.326265, 5.255842, 19.963133, -16.320711, 6.583985, 19.312944, -15.988569, 5.813969, 20.51122, -17.317139, 7.2331047, 19.692719, -18.072844, 7.327728, 19.590406, -17.322693, 7.4862165, 19.963133, -16.320711, 6.583985, 20.51122, -17.317139, 7.2331047, 19.590406, -17.322693, 7.4862165, 18.836624, -18.352867, 6.800999, 19.590406, -17.322693, 7.4862165, 19.692719, -18.072844, 7.327728, 19.312944, -15.988569, 5.813969, 19.963133, -16.320711, 6.583985, 19.407354, -16.581512, 6.8975444, 19.963133, -16.320711, 6.583985, 19.590406, -17.322693, 7.4862165, 19.407354, -16.581512, 6.8975444, 18.836624, -18.352867, 6.800999, 18.488676, -17.339567, 6.8439317, 19.590406, -17.322693, 7.4862165, 18.488676, -17.339567, 6.8439317, 19.407354, -16.581512, 6.8975444, 19.590406, -17.322693, 7.4862165, 18.45151, -18.10766, 5.7464733, 18.488676, -17.339567, 6.8439317, 18.836624, -18.352867, 6.800999, 18.45151, -18.10766, 5.7464733, 17.938025, -17.356228, 5.7366476, 18.488676, -17.339567, 6.8439317, 18.488676, -17.339567, 6.8439317, 18.691593, -16.326265, 6.4220786, 19.407354, -16.581512, 6.8975444, 19.312944, -15.988569, 5.813969, 19.407354, -16.581512, 6.8975444, 18.691593, -16.326265, 6.4220786, 17.938025, -17.356228, 5.7366476, 18.691593, -16.326265, 6.4220786, 18.488676, -17.339567, 6.8439317, 18.80138, -16.33182, 5.093936, 19.312944, -15.988569, 5.813969, 18.691593, -16.326265, 6.4220786, 17.938025, -17.356228, 5.7366476, 18.361372, -16.59625, 5.750745, 18.691593, -16.326265, 6.4220786, 18.80138, -16.33182, 5.093936, 18.691593, -16.326265, 6.4220786, 18.361372, -16.59625, 5.750745, 20.916626, -18.072844, 6.2943482, 19.867655, -18.690779, 6.0138965, 20.598795, -18.324886, 7.2226386, 20.916626, -18.072844, 6.2943482, 20.759848, -18.352869, 5.1768117, 19.867655, -18.690779, 6.0138965, 20.598795, -18.324886, 7.2226386, 19.867655, -18.690779, 6.0138965, 18.836624, -18.352867, 6.800999, 20.598795, -18.324886, 7.2226386, 18.836624, -18.352867, 6.800999, 19.692719, -18.072844, 7.327728, 19.867655, -18.690779, 6.0138965, 18.45151, -18.10766, 5.7464733, 18.836624, -18.352867, 6.800999, 19.867655, -18.690779, 6.0138965, 18.997677, -18.38085, 4.7551727, 18.45151, -18.10766, 5.7464733, 20.759848, -18.352869, 5.1768117, 18.997677, -18.38085, 4.7551727, 19.867655, -18.690779, 6.0138965, 20.759848, -18.352869, 5.1768117, 19.916355, -18.10766, 4.50975, 18.997677, -18.38085, 4.7551727, 15.432323, -17.738777, 11.597307, 17.151134, -15.819399, 12.648415, 17.029385, -18.463295, 12.383129, 17.029385, -18.463295, 12.383129, 17.151134, -15.819399, 12.648415, 19.233913, -15.752116, 11.624861, 17.029385, -18.463295, 12.383129, 19.233913, -15.752116, 11.624861, 18.862041, -17.774446, 11.419596, 15.432323, -17.738777, 11.597307, 15.174299, -15.860622, 12.197086, 17.151134, -15.819399, 12.648415, 14.790466, -18.280245, 9.459635, 15.174299, -15.860622, 12.197086, 15.432323, -17.738777, 11.597307, 17.151134, -15.819399, 12.648415, 18.632425, -13.811806, 11.039606, 19.233913, -15.752116, 11.624861, 14.790466, -18.280245, 9.459635, 14.663164, -15.672873, 9.612782, 15.174299, -15.860622, 12.197086, 15.765963, -17.546755, 7.471905, 14.663164, -15.672873, 9.612782, 14.790466, -18.280245, 9.459635, 15.174299, -15.860622, 12.197086, 17.378614, -13.149656, 11.438818, 17.151134, -15.819399, 12.648415, 17.151134, -15.819399, 12.648415, 17.378614, -13.149656, 11.438818, 18.632425, -13.811806, 11.039606, 14.663164, -15.672873, 9.612782, 16.05389, -13.860292, 11.293999, 15.174299, -15.860622, 12.197086, 15.174299, -15.860622, 12.197086, 16.05389, -13.860292, 11.293999, 17.378614, -13.149656, 11.438818, 15.765963, -17.546755, 7.471905, 15.501957, -15.53254, 7.0716248, 14.663164, -15.672873, 9.612782, 15.501957, -15.53254, 7.0716248, 15.765963, -17.546755, 7.471905, 17.558678, -18.333218, 6.7520857, 14.663164, -15.672873, 9.612782, 15.88579, -13.113132, 9.809076, 16.05389, -13.860292, 11.293999, 15.501957, -15.53254, 7.0716248, 15.88579, -13.113132, 9.809076, 14.663164, -15.672873, 9.612782, 16.05389, -13.860292, 11.293999, 17.454443, -12.259815, 9.87465, 17.378614, -13.149656, 11.438818, 16.05389, -13.860292, 11.293999, 15.88579, -13.113132, 9.809076, 17.454443, -12.259815, 9.87465, 15.501957, -15.53254, 7.0716248, 16.199562, -13.714193, 8.2419195, 15.88579, -13.113132, 9.809076, 15.501957, -15.53254, 7.0716248, 17.558678, -18.333218, 6.7520857, 17.574484, -15.715379, 6.749949, 16.199562, -13.714193, 8.2419195, 15.501957, -15.53254, 7.0716248, 17.574484, -15.715379, 6.749949, 17.574484, -15.715379, 6.749949, 17.558678, -18.333218, 6.7520857, 19.186922, -17.80478, 7.9181085, 15.88579, -13.113132, 9.809076, 16.199562, -13.714193, 8.2419195, 17.48456, -13.123599, 8.222482, 16.199562, -13.714193, 8.2419195, 17.574484, -15.715379, 6.749949, 17.48456, -13.123599, 8.222482, 15.88579, -13.113132, 9.809076, 17.48456, -13.123599, 8.222482, 17.454443, -12.259815, 9.87465, 17.574484, -15.715379, 6.749949, 19.186922, -17.80478, 7.9181085, 19.541065, -15.924276, 7.9029427, 17.48456, -13.123599, 8.222482, 17.574484, -15.715379, 6.749949, 19.541065, -15.924276, 7.9029427, 19.797594, -18.516268, 9.675581, 19.541065, -15.924276, 7.9029427, 19.186922, -17.80478, 7.9181085, 17.454443, -12.259815, 9.87465, 17.48456, -13.123599, 8.222482, 18.768915, -13.888488, 8.611227, 17.48456, -13.123599, 8.222482, 19.541065, -15.924276, 7.9029427, 18.768915, -13.888488, 8.611227, 19.797594, -18.516268, 9.675581, 20.062454, -15.861905, 9.785582, 19.541065, -15.924276, 7.9029427, 20.062454, -15.861905, 9.785582, 18.768915, -13.888488, 8.611227, 19.541065, -15.924276, 7.9029427, 18.862041, -17.774446, 11.419596, 20.062454, -15.861905, 9.785582, 19.797594, -18.516268, 9.675581, 18.862041, -17.774446, 11.419596, 19.233913, -15.752116, 11.624861, 20.062454, -15.861905, 9.785582, 20.062454, -15.861905, 9.785582, 18.977385, -13.160124, 9.852436, 18.768915, -13.888488, 8.611227, 17.454443, -12.259815, 9.87465, 18.768915, -13.888488, 8.611227, 18.977385, -13.160124, 9.852436, 19.233913, -15.752116, 11.624861, 18.977385, -13.160124, 9.852436, 20.062454, -15.861905, 9.785582, 17.378614, -13.149656, 11.438818, 17.454443, -12.259815, 9.87465, 18.977385, -13.160124, 9.852436, 19.233913, -15.752116, 11.624861, 18.632425, -13.811806, 11.039606, 18.977385, -13.160124, 9.852436, 17.378614, -13.149656, 11.438818, 18.977385, -13.160124, 9.852436, 18.632425, -13.811806, 11.039606, 15.765963, -17.546755, 7.471905, 17.271177, -19.275177, 9.523714, 17.558678, -18.333218, 6.7520857, 15.765963, -17.546755, 7.471905, 14.790466, -18.280245, 9.459635, 17.271177, -19.275177, 9.523714, 17.558678, -18.333218, 6.7520857, 17.271177, -19.275177, 9.523714, 19.797594, -18.516268, 9.675581, 17.558678, -18.333218, 6.7520857, 19.797594, -18.516268, 9.675581, 19.186922, -17.80478, 7.9181085, 17.271177, -19.275177, 9.523714, 18.862041, -17.774446, 11.419596, 19.797594, -18.516268, 9.675581, 17.271177, -19.275177, 9.523714, 17.029385, -18.463295, 12.383129, 18.862041, -17.774446, 11.419596, 14.790466, -18.280245, 9.459635, 17.029385, -18.463295, 12.383129, 17.271177, -19.275177, 9.523714, 14.790466, -18.280245, 9.459635, 15.432323, -17.738777, 11.597307, 17.029385, -18.463295, 12.383129, 6.450511, 5.6386614, 17.721281, 5.116558, 6.049283, 18.568323, 6.1998763, 5.437825, 17.553066, 6.450511, 5.6386614, 17.721281, 5.394739, 6.2101097, 18.690458, 5.116558, 6.049283, 18.568323, 5.394739, 6.2101097, 18.690458, 4.166137, 7.607539, 19.708622, 5.116558, 6.049283, 18.568323, 5.394739, 6.2101097, 18.690458, 4.2640667, 7.6070127, 19.728611, 4.166137, 7.607539, 19.708622, 5.116558, 6.049283, 18.568323, 4.166137, 7.607539, 19.708622, 4.117188, 7.608329, 19.795757, 6.1998763, 5.437825, 17.553066, 5.116558, 6.049283, 18.568323, 5.0425253, 5.7889595, 18.780462, 5.116558, 6.049283, 18.568323, 4.117188, 7.608329, 19.795757, 5.0425253, 5.7889595, 18.780462, 6.1998763, 5.437825, 17.553066, 5.0425253, 5.7889595, 18.780462, 6.190998, 5.1124864, 17.713438, 5.0425253, 5.7889595, 18.780462, 4.117188, 7.608329, 19.795757, 4.1845627, 7.608329, 19.869196, 6.190998, 5.1124864, 17.713438, 5.0425253, 5.7889595, 18.780462, 5.2750225, 5.7889595, 19.03369, 5.0425253, 5.7889595, 18.780462, 4.1845627, 7.608329, 19.869196, 5.2750225, 5.7889595, 19.03369, 6.190998, 5.1124864, 17.713438, 5.2750225, 5.7889595, 19.03369, 6.436242, 5.1124864, 17.980635, 5.2750225, 5.7889595, 19.03369, 4.1845627, 7.608329, 19.869196, 4.2755394, 7.607539, 19.827774, 6.436242, 5.1124864, 17.980635, 5.2750225, 5.7889595, 19.03369, 5.492837, 6.049283, 18.978226, 5.2750225, 5.7889595, 19.03369, 4.2755394, 7.607539, 19.827774, 5.492837, 6.049283, 18.978226, 5.492837, 6.049283, 18.978226, 4.2755394, 7.607539, 19.827774, 4.2640667, 7.6070127, 19.728611, 6.436242, 5.1124864, 17.980635, 5.492837, 6.049283, 18.978226, 6.596695, 5.437825, 17.985352, 5.492837, 6.049283, 18.978226, 4.2640667, 7.6070127, 19.728611, 5.394739, 6.2101097, 18.690458, 6.596695, 5.437825, 17.985352, 5.492837, 6.049283, 18.978226, 5.394739, 6.2101097, 18.690458, 6.596695, 5.437825, 17.985352, 5.394739, 6.2101097, 18.690458, 6.450511, 5.6386614, 17.721281, 5.7805123, 3.2207327, 16.731695, 6.2294216, 5.7555304, 17.911285, 5.4306893, 3.2207327, 17.595985, 5.7805123, 3.2207327, 16.731695, 6.351206, 5.7918544, 17.591637, 6.2294216, 5.7555304, 17.911285, 6.351206, 5.7918544, 17.591637, 6.4141273, 8.301909, 17.908752, 6.2294216, 5.7555304, 17.911285, 6.351206, 5.7918544, 17.591637, 6.4673533, 8.311122, 17.766653, 6.4141273, 8.301909, 17.908752, 6.2294216, 5.7555304, 17.911285, 6.4141273, 8.301909, 17.908752, 6.532635, 8.286643, 18.002592, 5.4306893, 3.2207327, 17.595985, 6.2294216, 5.7555304, 17.911285, 6.495885, 5.697096, 18.12049, 6.2294216, 5.7555304, 17.911285, 6.532635, 8.286643, 18.002592, 6.495885, 5.697096, 18.12049, 5.4306893, 3.2207327, 17.595985, 6.495885, 5.697096, 18.12049, 6.146731, 3.2207327, 18.143703, 6.495885, 5.697096, 18.12049, 6.532635, 8.286643, 18.002592, 6.6594005, 8.286643, 17.918467, 6.146731, 3.2207327, 18.143703, 6.495885, 5.697096, 18.12049, 6.7823133, 5.697096, 17.930405, 6.495885, 5.697096, 18.12049, 6.6594005, 8.286643, 17.918467, 6.7823133, 5.697096, 17.930405, 6.146731, 3.2207327, 18.143703, 6.7823133, 5.697096, 17.930405, 6.939343, 3.2207327, 17.617693, 6.7823133, 5.697096, 17.930405, 6.6594005, 8.286643, 17.918467, 6.6189694, 8.301909, 17.77281, 6.939343, 3.2207327, 17.617693, 6.7823133, 5.697096, 17.930405, 6.693058, 5.7555304, 17.603598, 6.7823133, 5.697096, 17.930405, 6.6189694, 8.301909, 17.77281, 6.693058, 5.7555304, 17.603598, 6.693058, 5.7555304, 17.603598, 6.6189694, 8.301909, 17.77281, 6.4673533, 8.311122, 17.766653, 6.939343, 3.2207327, 17.617693, 6.693058, 5.7555304, 17.603598, 6.712817, 3.2207322, 16.745113, 6.693058, 5.7555304, 17.603598, 6.4673533, 8.311122, 17.766653, 6.351206, 5.7918544, 17.591637, 6.712817, 3.2207322, 16.745113, 6.693058, 5.7555304, 17.603598, 6.351206, 5.7918544, 17.591637, 6.712817, 3.2207322, 16.745113, 6.351206, 5.7918544, 17.591637, 5.7805123, 3.2207327, 16.731695, 6.732394, 5.966105, 17.783781, 8.337761, 6.384096, 17.883148, 6.83874, 5.7618475, 18.072077, 6.83874, 5.7618475, 18.072077, 6.302865, 5.6723533, 17.875813, 6.732394, 5.966105, 17.783781, 6.83874, 5.7618475, 18.072077, 6.3696475, 5.543639, 18.05737, 6.302865, 5.6723533, 17.875813, 6.732394, 5.966105, 17.783781, 8.181509, 6.5478187, 17.616283, 8.337761, 6.384096, 17.883148, 8.181509, 6.5478187, 17.616283, 9.801905, 7.9697266, 17.51171, 8.337761, 6.384096, 17.883148, 8.181509, 6.5478187, 17.616283, 9.733292, 7.9692, 17.436882, 9.801905, 7.9697266, 17.51171, 8.337761, 6.384096, 17.883148, 9.801905, 7.9697266, 17.51171, 9.894417, 7.970516, 17.469269, 8.337761, 6.384096, 17.883148, 9.894417, 7.970516, 17.469269, 8.525363, 6.119035, 17.752645, 6.83874, 5.7618475, 18.072077, 8.337761, 6.384096, 17.883148, 8.525363, 6.119035, 17.752645, 8.525363, 6.119035, 17.752645, 9.894417, 7.970516, 17.469269, 9.882729, 7.970516, 17.368353, 6.9416494, 5.4307184, 17.94534, 6.3696475, 5.543639, 18.05737, 6.83874, 5.7618475, 18.072077, 6.83874, 5.7618475, 18.072077, 8.525363, 6.119035, 17.752645, 6.9416494, 5.4307184, 17.94534, 6.9416494, 5.4307184, 17.94534, 6.4345694, 5.33517, 17.977638, 6.3696475, 5.543639, 18.05737, 8.525363, 6.119035, 17.752645, 9.882729, 7.970516, 17.368353, 8.485138, 6.119035, 17.40499, 6.9416494, 5.4307184, 17.94534, 8.525363, 6.119035, 17.752645, 8.485138, 6.119035, 17.40499, 8.485138, 6.119035, 17.40499, 9.882729, 7.970516, 17.368353, 9.783121, 7.9697266, 17.348215, 6.899346, 5.4307184, 17.578844, 6.4345694, 5.33517, 17.977638, 6.9416494, 5.4307184, 17.94534, 6.9416494, 5.4307184, 17.94534, 8.485138, 6.119035, 17.40499, 6.899346, 5.4307184, 17.578844, 6.899346, 5.4307184, 17.578844, 6.4079695, 5.33517, 17.746672, 6.4345694, 5.33517, 17.977638, 8.485138, 6.119035, 17.40499, 9.783121, 7.9697266, 17.348215, 8.2728, 6.384096, 17.320663, 6.899346, 5.4307184, 17.578844, 8.485138, 6.119035, 17.40499, 8.2728, 6.384096, 17.320663, 8.2728, 6.384096, 17.320663, 9.783121, 7.9697266, 17.348215, 9.733292, 7.9692, 17.436882, 8.2728, 6.384096, 17.320663, 9.733292, 7.9692, 17.436882, 8.181509, 6.5478187, 17.616283, 6.899346, 5.4307184, 17.578844, 8.2728, 6.384096, 17.320663, 6.7699003, 5.7618475, 17.478994, 6.7699003, 5.7618475, 17.478994, 6.4079695, 5.33517, 17.746672, 6.899346, 5.4307184, 17.578844, 6.7699003, 5.7618475, 17.478994, 8.2728, 6.384096, 17.320663, 8.181509, 6.5478187, 17.616283, 6.7699003, 5.7618475, 17.478994, 6.326483, 5.543639, 17.683863, 6.4079695, 5.33517, 17.746672, 6.7699003, 5.7618475, 17.478994, 8.181509, 6.5478187, 17.616283, 6.732394, 5.966105, 17.783781, 6.732394, 5.966105, 17.783781, 6.326483, 5.543639, 17.683863, 6.7699003, 5.7618475, 17.478994, 6.732394, 5.966105, 17.783781, 6.302865, 5.6723533, 17.875813, 6.326483, 5.543639, 17.683863, 11.635334, 8.055535, 16.46335, 10.297312, 8.974432, 15.70101, 10.525072, 7.7188787, 16.089432, 10.525072, 7.7188787, 16.089432, 10.297312, 8.974432, 15.70101, 8.768291, 8.9812765, 16.37455, 10.525072, 7.7188787, 16.089432, 8.768291, 8.9812765, 16.37455, 9.2888565, 8.055536, 16.734821, 11.635334, 8.055535, 16.46335, 11.849512, 8.981276, 16.018257, 10.297312, 8.974432, 15.70101, 12.046843, 7.75336, 17.723677, 11.849512, 8.981276, 16.018257, 11.635334, 8.055535, 16.46335, 10.297312, 8.974432, 15.70101, 9.193269, 9.9178095, 16.677896, 8.768291, 8.9812765, 16.37455, 12.046843, 7.75336, 17.723677, 12.1083145, 9.00207, 17.478806, 11.849512, 8.981276, 16.018257, 11.446236, 8.09844, 18.977428, 12.1083145, 9.00207, 17.478806, 12.046843, 7.75336, 17.723677, 11.849512, 8.981276, 16.018257, 10.092733, 10.2439375, 16.303373, 10.297312, 8.974432, 15.70101, 10.297312, 8.974432, 15.70101, 10.092733, 10.2439375, 16.303373, 9.193269, 9.9178095, 16.677896, 12.1083145, 9.00207, 17.478806, 11.154192, 9.917809, 16.450953, 11.849512, 8.981276, 16.018257, 11.849512, 8.981276, 16.018257, 11.154192, 9.917809, 16.450953, 10.092733, 10.2439375, 16.303373, 11.446236, 8.09844, 18.977428, 11.4849415, 9.022602, 19.041462, 12.1083145, 9.00207, 17.478806, 11.4849415, 9.022602, 19.041462, 11.446236, 8.09844, 18.977428, 10.487244, 7.7878428, 19.713997, 12.1083145, 9.00207, 17.478806, 11.287758, 10.250781, 17.336258, 11.154192, 9.917809, 16.450953, 11.4849415, 9.022602, 19.041462, 11.287758, 10.250781, 17.336258, 12.1083145, 9.00207, 17.478806, 11.154192, 9.917809, 16.450953, 10.127245, 10.667193, 17.391205, 10.092733, 10.2439375, 16.303373, 11.154192, 9.917809, 16.450953, 11.287758, 10.250781, 17.336258, 10.127245, 10.667193, 17.391205, 11.4849415, 9.022602, 19.041462, 11.060484, 9.936234, 18.386164, 11.287758, 10.250781, 17.336258, 11.4849415, 9.022602, 19.041462, 10.487244, 7.7878428, 19.713997, 10.390107, 9.029709, 19.664736, 11.060484, 9.936234, 18.386164, 11.4849415, 9.022602, 19.041462, 10.390107, 9.029709, 19.664736, 10.390107, 9.029709, 19.664736, 10.487244, 7.7878428, 19.713997, 9.485313, 8.098441, 19.204372, 11.287758, 10.250781, 17.336258, 11.060484, 9.936234, 18.386164, 10.269789, 10.257888, 18.624687, 11.060484, 9.936234, 18.386164, 10.390107, 9.029709, 19.664736, 10.269789, 10.257888, 18.624687, 11.287758, 10.250781, 17.336258, 10.269789, 10.257888, 18.624687, 10.127245, 10.667193, 17.391205, 10.390107, 9.029709, 19.664736, 9.485313, 8.098441, 19.204372, 9.272093, 9.022602, 19.297224, 10.269789, 10.257888, 18.624687, 10.390107, 9.029709, 19.664736, 9.272093, 9.022602, 19.297224, 8.965256, 7.7533607, 18.079897, 9.272093, 9.022602, 19.297224, 9.485313, 8.098441, 19.204372, 10.127245, 10.667193, 17.391205, 10.269789, 10.257888, 18.624687, 9.485554, 9.936234, 18.568289, 10.269789, 10.257888, 18.624687, 9.272093, 9.022602, 19.297224, 9.485554, 9.936234, 18.568289, 8.965256, 7.7533607, 18.079897, 8.57874, 9.002071, 17.886866, 9.272093, 9.022602, 19.297224, 8.57874, 9.002071, 17.886866, 9.485554, 9.936234, 18.568289, 9.272093, 9.022602, 19.297224, 9.2888565, 8.055536, 16.734821, 8.57874, 9.002071, 17.886866, 8.965256, 7.7533607, 18.079897, 9.2888565, 8.055536, 16.734821, 8.768291, 8.9812765, 16.37455, 8.57874, 9.002071, 17.886866, 8.57874, 9.002071, 17.886866, 9.07469, 10.250781, 17.592167, 9.485554, 9.936234, 18.568289, 10.127245, 10.667193, 17.391205, 9.485554, 9.936234, 18.568289, 9.07469, 10.250781, 17.592167, 8.768291, 8.9812765, 16.37455, 9.07469, 10.250781, 17.592167, 8.57874, 9.002071, 17.886866, 10.092733, 10.2439375, 16.303373, 10.127245, 10.667193, 17.391205, 9.07469, 10.250781, 17.592167, 8.768291, 8.9812765, 16.37455, 9.193269, 9.9178095, 16.677896, 9.07469, 10.250781, 17.592167, 10.092733, 10.2439375, 16.303373, 9.07469, 10.250781, 17.592167, 9.193269, 9.9178095, 16.677896, 11.446236, 8.09844, 18.977428, 10.560175, 7.3369484, 17.97454, 10.487244, 7.7878428, 19.713997, 11.446236, 8.09844, 18.977428, 12.046843, 7.75336, 17.723677, 10.560175, 7.3369484, 17.97454, 10.487244, 7.7878428, 19.713997, 10.560175, 7.3369484, 17.97454, 8.965256, 7.7533607, 18.079897, 10.487244, 7.7878428, 19.713997, 8.965256, 7.7533607, 18.079897, 9.485313, 8.098441, 19.204372, 10.560175, 7.3369484, 17.97454, 9.2888565, 8.055536, 16.734821, 8.965256, 7.7533607, 18.079897, 10.560175, 7.3369484, 17.97454, 10.525072, 7.7188787, 16.089432, 9.2888565, 8.055536, 16.734821, 12.046843, 7.75336, 17.723677, 10.525072, 7.7188787, 16.089432, 10.560175, 7.3369484, 17.97454, 12.046843, 7.75336, 17.723677, 11.635334, 8.055535, 16.46335, 10.525072, 7.7188787, 16.089432, 1.4948497, 8.510117, 18.317255, 1.5868134, 10.875402, 20.798342, 1.7763877, 7.617279, 20.49278, 1.7763877, 7.617279, 20.49278, 1.5868134, 10.875402, 20.798342, 4.0570173, 10.958316, 22.239437, 1.7763877, 7.617279, 20.49278, 4.0570173, 10.958316, 22.239437, 4.0145273, 8.465897, 21.717955, 1.4948497, 8.510117, 18.317255, 0.70318604, 10.8246, 18.46102, 1.5868134, 10.875402, 20.798342, 3.2524045, 7.8428564, 16.201567, 0.70318604, 10.8246, 18.46102, 1.4948497, 8.510117, 18.317255, 1.5868134, 10.875402, 20.798342, 4.248084, 13.349396, 21.22304, 4.0570173, 10.958316, 22.239437, 3.2524045, 7.8428564, 16.201567, 3.0084083, 11.055706, 16.17521, 0.70318604, 10.8246, 18.46102, 5.9580865, 8.746749, 15.848719, 3.0084083, 11.055706, 16.17521, 3.2524045, 7.8428564, 16.201567, 0.70318604, 10.8246, 18.46102, 2.983596, 14.165375, 20.207823, 1.5868134, 10.875402, 20.798342, 1.5868134, 10.875402, 20.798342, 2.983596, 14.165375, 20.207823, 4.248084, 13.349396, 21.22304, 3.0084083, 11.055706, 16.17521, 2.2301898, 13.289908, 18.748863, 0.70318604, 10.8246, 18.46102, 0.70318604, 10.8246, 18.46102, 2.2301898, 13.289908, 18.748863, 2.983596, 14.165375, 20.207823, 5.9580865, 8.746749, 15.848719, 6.1891904, 11.228903, 15.304886, 3.0084083, 11.055706, 16.17521, 6.1891904, 11.228903, 15.304886, 5.9580865, 8.746749, 15.848719, 7.9187675, 7.777577, 17.198952, 3.0084083, 11.055706, 16.17521, 3.6397526, 14.210384, 17.564486, 2.2301898, 13.289908, 18.748863, 6.1891904, 11.228903, 15.304886, 3.6397526, 14.210384, 17.564486, 3.0084083, 11.055706, 16.17521, 2.2301898, 13.289908, 18.748863, 4.6415443, 15.261944, 19.219688, 2.983596, 14.165375, 20.207823, 2.2301898, 13.289908, 18.748863, 3.6397526, 14.210384, 17.564486, 4.6415443, 15.261944, 19.219688, 6.1891904, 11.228903, 15.304886, 5.462911, 13.469423, 16.818632, 3.6397526, 14.210384, 17.564486, 6.1891904, 11.228903, 15.304886, 7.9187675, 7.777577, 17.198952, 7.931513, 11.003588, 17.213871, 5.462911, 13.469423, 16.818632, 6.1891904, 11.228903, 15.304886, 7.931513, 11.003588, 17.213871, 7.931513, 11.003588, 17.213871, 7.9187675, 7.777577, 17.198952, 7.831026, 8.428781, 19.665352, 3.6397526, 14.210384, 17.564486, 5.462911, 13.469423, 16.818632, 6.3584847, 14.197486, 18.1248, 5.462911, 13.469423, 16.818632, 7.931513, 11.003588, 17.213871, 6.3584847, 14.197486, 18.1248, 3.6397526, 14.210384, 17.564486, 6.3584847, 14.197486, 18.1248, 4.6415443, 15.261944, 19.219688, 7.931513, 11.003588, 17.213871, 7.831026, 8.428781, 19.665352, 8.087843, 10.745896, 20.01901, 6.3584847, 14.197486, 18.1248, 7.931513, 11.003588, 17.213871, 8.087843, 10.745896, 20.01901, 6.4428244, 7.5519996, 21.489801, 8.087843, 10.745896, 20.01901, 7.831026, 8.428781, 19.665352, 4.6415443, 15.261944, 19.219688, 6.3584847, 14.197486, 18.1248, 6.8342867, 13.254899, 19.708595, 6.3584847, 14.197486, 18.1248, 8.087843, 10.745896, 20.01901, 6.8342867, 13.254899, 19.708595, 6.4428244, 7.5519996, 21.489801, 6.510137, 10.823021, 21.836857, 8.087843, 10.745896, 20.01901, 6.510137, 10.823021, 21.836857, 6.8342867, 13.254899, 19.708595, 8.087843, 10.745896, 20.01901, 4.0145273, 8.465897, 21.717955, 6.510137, 10.823021, 21.836857, 6.4428244, 7.5519996, 21.489801, 4.0145273, 8.465897, 21.717955, 4.0570173, 10.958316, 22.239437, 6.510137, 10.823021, 21.836857, 6.510137, 10.823021, 21.836857, 5.702328, 14.152213, 20.768137, 6.8342867, 13.254899, 19.708595, 4.6415443, 15.261944, 19.219688, 6.8342867, 13.254899, 19.708595, 5.702328, 14.152213, 20.768137, 4.0570173, 10.958316, 22.239437, 5.702328, 14.152213, 20.768137, 6.510137, 10.823021, 21.836857, 2.983596, 14.165375, 20.207823, 4.6415443, 15.261944, 19.219688, 5.702328, 14.152213, 20.768137, 4.0570173, 10.958316, 22.239437, 4.248084, 13.349396, 21.22304, 5.702328, 14.152213, 20.768137, 2.983596, 14.165375, 20.207823, 5.702328, 14.152213, 20.768137, 4.248084, 13.349396, 21.22304, 5.9580865, 8.746749, 15.848719, 4.877001, 6.61652, 18.79238, 7.9187675, 7.777577, 17.198952, 5.9580865, 8.746749, 15.848719, 3.2524045, 7.8428564, 16.201567, 4.877001, 6.61652, 18.79238, 7.9187675, 7.777577, 17.198952, 4.877001, 6.61652, 18.79238, 6.4428244, 7.5519996, 21.489801, 7.9187675, 7.777577, 17.198952, 6.4428244, 7.5519996, 21.489801, 7.831026, 8.428781, 19.665352, 4.877001, 6.61652, 18.79238, 4.0145273, 8.465897, 21.717955, 6.4428244, 7.5519996, 21.489801, 4.877001, 6.61652, 18.79238, 1.7763877, 7.617279, 20.49278, 4.0145273, 8.465897, 21.717955, 3.2524045, 7.8428564, 16.201567, 1.7763877, 7.617279, 20.49278, 4.877001, 6.61652, 18.79238, 3.2524045, 7.8428564, 16.201567, 1.4948497, 8.510117, 18.317255, 1.7763877, 7.617279, 20.49278, -17.758068, 7.5774293, -6.836121, -20.570257, 8.332888, -7.573155, -17.84524, 7.2079325, -7.384581, -17.758068, 7.5774293, -6.836121, -20.386482, 8.628776, -7.04528, -20.570257, 8.332888, -7.573155, -20.386482, 8.628776, -7.04528, -23.297365, 11.199757, -7.426018, -20.570257, 8.332888, -7.573155, -20.386482, 8.628776, -7.04528, -23.201662, 11.198789, -7.2690005, -23.297365, 11.199757, -7.426018, -20.570257, 8.332888, -7.573155, -23.297365, 11.199757, -7.426018, -23.476107, 11.20121, -7.3828626, -17.84524, 7.2079325, -7.384581, -20.570257, 8.332888, -7.573155, -20.948885, 7.853947, -7.4072576, -20.570257, 8.332888, -7.573155, -23.476107, 11.20121, -7.3828626, -20.948885, 7.853947, -7.4072576, -17.84524, 7.2079325, -7.384581, -20.948885, 7.853947, -7.4072576, -18.072498, 6.6093764, -7.195704, -20.948885, 7.853947, -7.4072576, -23.476107, 11.20121, -7.3828626, -23.490705, 11.20121, -7.200089, -18.072498, 6.6093764, -7.195704, -20.948885, 7.853947, -7.4072576, -20.999002, 7.853947, -6.776777, -20.948885, 7.853947, -7.4072576, -23.490705, 11.20121, -7.200089, -20.999002, 7.853947, -6.776777, -18.072498, 6.6093764, -7.195704, -20.999002, 7.853947, -6.776777, -18.12548, 6.6093764, -6.5305486, -20.999002, 7.853947, -6.776777, -23.490705, 11.20121, -7.200089, -23.320944, 11.199757, -7.129349, -18.12548, 6.6093764, -6.5305486, -20.999002, 7.853947, -6.776777, -20.651468, 8.332888, -6.5526786, -20.999002, 7.853947, -6.776777, -23.320944, 11.199757, -7.129349, -20.651468, 8.332888, -6.5526786, -20.651468, 8.332888, -6.5526786, -23.320944, 11.199757, -7.129349, -23.201662, 11.198789, -7.2690005, -18.12548, 6.6093764, -6.5305486, -20.651468, 8.332888, -6.5526786, -17.930893, 7.2079325, -6.308391, -20.651468, 8.332888, -6.5526786, -23.201662, 11.198789, -7.2690005, -20.386482, 8.628776, -7.04528, -17.930893, 7.2079325, -6.308391, -20.651468, 8.332888, -6.5526786, -20.386482, 8.628776, -7.04528, -17.930893, 7.2079325, -6.308391, -20.386482, 8.628776, -7.04528, -17.758068, 7.5774293, -6.836121, -17.263433, 3.1289406, -8.978432, -18.291029, 7.792444, -6.8961415, -18.866385, 3.1289406, -8.367504, -17.263433, 3.1289406, -8.978432, -17.707705, 7.8592725, -7.132322, -18.291029, 7.792444, -6.8961415, -17.707705, 7.8592725, -7.132322, -18.056295, 12.477256, -6.650377, -18.291029, 7.792444, -6.8961415, -17.707705, 7.8592725, -7.132322, -17.798122, 12.494205, -6.756602, -18.056295, 12.477256, -6.650377, -18.291029, 7.792444, -6.8961415, -18.056295, 12.477256, -6.650377, -18.034353, 12.449168, -6.373137, -18.866385, 3.1289406, -8.367504, -18.291029, 7.792444, -6.8961415, -18.239275, 7.6849365, -6.2750163, -18.291029, 7.792444, -6.8961415, -18.034353, 12.449168, -6.373137, -18.239275, 7.6849365, -6.2750163, -18.866385, 3.1289406, -8.367504, -18.239275, 7.6849365, -6.2750163, -18.707829, 3.1289406, -6.716517, -18.239275, 7.6849365, -6.2750163, -18.034353, 12.449168, -6.373137, -17.762217, 12.449168, -6.3076463, -18.707829, 3.1289406, -6.716517, -18.239275, 7.6849365, -6.2750163, -17.624376, 7.6849365, -6.1270385, -18.239275, 7.6849365, -6.2750163, -17.762217, 12.449168, -6.3076463, -17.624376, 7.6849365, -6.1270385, -18.707829, 3.1289406, -6.716517, -17.624376, 7.6849365, -6.1270385, -17.006262, 3.1289406, -6.3070283, -17.624376, 7.6849365, -6.1270385, -17.762217, 12.449168, -6.3076463, -17.616545, 12.477256, -6.5445485, -17.006262, 3.1289406, -6.3070283, -17.624376, 7.6849365, -6.1270385, -17.295702, 7.792444, -6.656613, -17.624376, 7.6849365, -6.1270385, -17.616545, 12.477256, -6.5445485, -17.295702, 7.792444, -6.656613, -17.295702, 7.792444, -6.656613, -17.616545, 12.477256, -6.5445485, -17.798122, 12.494205, -6.756602, -17.006262, 3.1289406, -6.3070283, -17.295702, 7.792444, -6.656613, -16.113934, 3.1289406, -7.705115, -17.295702, 7.792444, -6.656613, -17.798122, 12.494205, -6.756602, -17.707705, 7.8592725, -7.132322, -16.113934, 3.1289406, -7.705115, -17.295702, 7.792444, -6.656613, -17.707705, 7.8592725, -7.132322, -16.113934, 3.1289406, -7.705115, -17.707705, 7.8592725, -7.132322, -17.263433, 3.1289406, -8.978432, -17.48928, 8.179858, -6.3779426, -15.612703, 8.948875, -4.0898643, -17.744648, 7.804066, -5.8735633, -17.744648, 7.804066, -5.8735633, -18.151243, 7.6394153, -6.841582, -17.48928, 8.179858, -6.3779426, -17.744648, 7.804066, -5.8735633, -18.312298, 7.402608, -6.524203, -18.151243, 7.6394153, -6.841582, -17.48928, 8.179858, -6.3779426, -15.448719, 9.250092, -4.6346655, -15.612703, 8.948875, -4.0898643, -15.448719, 9.250092, -4.6346655, -13.278458, 11.866108, -2.5817409, -15.612703, 8.948875, -4.0898643, -15.448719, 9.250092, -4.6346655, -13.263538, 11.865139, -2.7679238, -13.278458, 11.866108, -2.5817409, -15.612703, 8.948875, -4.0898643, -13.278458, 11.866108, -2.5817409, -13.1054, 11.867559, -2.5102086, -15.612703, 8.948875, -4.0898643, -13.1054, 11.867559, -2.5102086, -15.201872, 8.461218, -4.00046, -17.744648, 7.804066, -5.8735633, -15.612703, 8.948875, -4.0898643, -15.201872, 8.461218, -4.00046, -15.201872, 8.461218, -4.00046, -13.1054, 11.867559, -2.5102086, -12.98403, 11.867559, -2.6523442, -17.444958, 7.1948566, -5.8935885, -18.312298, 7.402608, -6.524203, -17.744648, 7.804066, -5.8735633, -17.744648, 7.804066, -5.8735633, -15.201872, 8.461218, -4.00046, -17.444958, 7.1948566, -5.8935885, -17.444958, 7.1948566, -5.8935885, -18.123537, 7.0190673, -6.536556, -18.312298, 7.402608, -6.524203, -15.201872, 8.461218, -4.00046, -12.98403, 11.867559, -2.6523442, -14.783697, 8.461218, -4.490066, -17.444958, 7.1948566, -5.8935885, -15.201872, 8.461218, -4.00046, -14.783697, 8.461218, -4.490066, -14.783697, 8.461218, -4.490066, -12.98403, 11.867559, -2.6523442, -13.081631, 11.866107, -2.811812, -17.003992, 7.1948566, -6.4095883, -18.123537, 7.0190673, -6.536556, -17.444958, 7.1948566, -5.8935885, -17.444958, 7.1948566, -5.8935885, -14.783697, 8.461218, -4.490066, -17.003992, 7.1948566, -6.4095883, -17.003992, 7.1948566, -6.4095883, -17.845564, 7.0190673, -6.8616605, -18.123537, 7.0190673, -6.536556, -14.783697, 8.461218, -4.490066, -13.081631, 11.866107, -2.811812, -14.935968, 8.948875, -4.8818517, -17.003992, 7.1948566, -6.4095883, -14.783697, 8.461218, -4.490066, -14.935968, 8.948875, -4.8818517, -14.935968, 8.948875, -4.8818517, -13.081631, 11.866107, -2.811812, -13.263538, 11.865139, -2.7679238, -14.935968, 8.948875, -4.8818517, -13.263538, 11.865139, -2.7679238, -15.448719, 9.250092, -4.6346655, -17.003992, 7.1948566, -6.4095883, -14.935968, 8.948875, -4.8818517, -17.031534, 7.8040657, -6.7090955, -17.031534, 7.8040657, -6.7090955, -17.845564, 7.0190673, -6.8616605, -17.003992, 7.1948566, -6.4095883, -17.031534, 7.8040657, -6.7090955, -14.935968, 8.948875, -4.8818517, -15.448719, 9.250092, -4.6346655, -17.031534, 7.8040657, -6.7090955, -17.862965, 7.402608, -7.050143, -17.845564, 7.0190673, -6.8616605, -17.031534, 7.8040657, -6.7090955, -15.448719, 9.250092, -4.6346655, -17.48928, 8.179858, -6.3779426, -17.48928, 8.179858, -6.3779426, -17.862965, 7.402608, -7.050143, -17.031534, 7.8040657, -6.7090955, -17.48928, 8.179858, -6.3779426, -18.151243, 7.6394153, -6.841582, -17.862965, 7.402608, -7.050143, -9.569414, 12.023977, -1.423661, -10.217659, 13.714559, -4.1817064, -10.455915, 11.404599, -3.3882995, -10.455915, 11.404599, -3.3882995, -10.217659, 13.714559, -4.1817064, -13.040308, 13.727151, -5.3989296, -10.455915, 11.404599, -3.3882995, -13.040308, 13.727151, -5.3989296, -12.873928, 12.023979, -4.2461476, -9.569414, 12.023977, -1.423661, -8.701311, 13.727149, -1.6924205, -10.217659, 13.714559, -4.1817064, -10.752665, 11.468038, 0.7093334, -8.701311, 13.727149, -1.6924205, -9.569414, 12.023977, -1.423661, -10.217659, 13.714559, -4.1817064, -12.916918, 15.450177, -4.4462667, -13.040308, 13.727151, -5.3989296, -10.752665, 11.468038, 0.7093334, -10.345652, 13.765407, 0.48551416, -8.701311, 13.727149, -1.6924205, -13.194582, 12.102914, 1.4700174, -10.345652, 13.765407, 0.48551416, -10.752665, 11.468038, 0.7093334, -8.701311, 13.727149, -1.6924205, -11.285702, 16.050184, -3.7030501, -10.217659, 13.714559, -4.1817064, -10.217659, 13.714559, -4.1817064, -11.285702, 16.050184, -3.7030501, -12.916918, 15.450177, -4.4462667, -10.345652, 13.765407, 0.48551416, -10.15527, 15.450176, -2.0876474, -8.701311, 13.727149, -1.6924205, -8.701311, 13.727149, -1.6924205, -10.15527, 15.450176, -2.0876474, -11.285702, 16.050184, -3.7030501, -13.194582, 12.102914, 1.4700174, -13.23241, 13.80318, 1.6023726, -10.345652, 13.765407, 0.48551416, -13.23241, 13.80318, 1.6023726, -13.194582, 12.102914, 1.4700174, -15.388298, 11.531479, 1.0999866, -10.345652, 13.765407, 0.48551416, -11.1811695, 16.062777, -0.7989111, -10.15527, 15.450176, -2.0876474, -13.23241, 13.80318, 1.6023726, -11.1811695, 16.062777, -0.7989111, -10.345652, 13.765407, 0.48551416, -10.15527, 15.450176, -2.0876474, -12.708605, 16.828888, -2.2941785, -11.285702, 16.050184, -3.7030501, -10.15527, 15.450176, -2.0876474, -11.1811695, 16.062777, -0.7989111, -12.708605, 16.828888, -2.2941785, -13.23241, 13.80318, 1.6023726, -12.880806, 15.484076, 0.2096405, -11.1811695, 16.062777, -0.7989111, -13.23241, 13.80318, 1.6023726, -15.388298, 11.531479, 1.0999866, -15.443556, 13.8162565, 0.90737486, -12.880806, 15.484076, 0.2096405, -13.23241, 13.80318, 1.6023726, -15.443556, 13.8162565, 0.90737486, -15.443556, 13.8162565, 0.90737486, -15.388298, 11.531479, 1.0999866, -15.956232, 12.102915, -0.88860226, -11.1811695, 16.062777, -0.7989111, -12.880806, 15.484076, 0.2096405, -14.192514, 16.075851, -0.5573001, -12.880806, 15.484076, 0.2096405, -15.443556, 13.8162565, 0.90737486, -14.192514, 16.075851, -0.5573001, -11.1811695, 16.062777, -0.7989111, -14.192514, 16.075851, -0.5573001, -12.708605, 16.828888, -2.2941785, -15.443556, 13.8162565, 0.90737486, -15.956232, 12.102915, -0.88860226, -16.3484, 13.803182, -1.0596855, -14.192514, 16.075851, -0.5573001, -15.443556, 13.8162565, 0.90737486, -16.3484, 13.803182, -1.0596855, -15.09202, 11.4680395, -2.9977593, -16.3484, 13.803182, -1.0596855, -15.956232, 12.102915, -0.88860226, -12.708605, 16.828888, -2.2941785, -14.192514, 16.075851, -0.5573001, -15.09865, 15.4840765, -1.6848848, -14.192514, 16.075851, -0.5573001, -16.3484, 13.803182, -1.0596855, -15.09865, 15.4840765, -1.6848848, -15.09202, 11.4680395, -2.9977593, -15.31592, 13.7654085, -3.7604294, -16.3484, 13.803182, -1.0596855, -15.31592, 13.7654085, -3.7604294, -15.09865, 15.4840765, -1.6848848, -16.3484, 13.803182, -1.0596855, -12.873928, 12.023979, -4.2461476, -15.31592, 13.7654085, -3.7604294, -15.09202, 11.4680395, -2.9977593, -12.873928, 12.023979, -4.2461476, -13.040308, 13.727151, -5.3989296, -15.31592, 13.7654085, -3.7604294, -15.31592, 13.7654085, -3.7604294, -14.29763, 16.062777, -3.461082, -15.09865, 15.4840765, -1.6848848, -12.708605, 16.828888, -2.2941785, -15.09865, 15.4840765, -1.6848848, -14.29763, 16.062777, -3.461082, -13.040308, 13.727151, -5.3989296, -14.29763, 16.062777, -3.461082, -15.31592, 13.7654085, -3.7604294, -11.285702, 16.050184, -3.7030501, -12.708605, 16.828888, -2.2941785, -14.29763, 16.062777, -3.461082, -13.040308, 13.727151, -5.3989296, -12.916918, 15.450177, -4.4462667, -14.29763, 16.062777, -3.461082, -11.285702, 16.050184, -3.7030501, -14.29763, 16.062777, -3.461082, -12.916918, 15.450177, -4.4462667, -13.194582, 12.102914, 1.4700174, -12.95261, 10.701926, -0.98015237, -15.388298, 11.531479, 1.0999866, -13.194582, 12.102914, 1.4700174, -10.752665, 11.468038, 0.7093334, -12.95261, 10.701926, -0.98015237, -15.388298, 11.531479, 1.0999866, -12.95261, 10.701926, -0.98015237, -15.09202, 11.4680395, -2.9977593, -15.388298, 11.531479, 1.0999866, -15.09202, 11.4680395, -2.9977593, -15.956232, 12.102915, -0.88860226, -12.95261, 10.701926, -0.98015237, -12.873928, 12.023979, -4.2461476, -15.09202, 11.4680395, -2.9977593, -12.95261, 10.701926, -0.98015237, -10.455915, 11.404599, -3.3882995, -12.873928, 12.023979, -4.2461476, -10.752665, 11.468038, 0.7093334, -10.455915, 11.404599, -3.3882995, -12.95261, 10.701926, -0.98015237, -10.752665, 11.468038, 0.7093334, -9.569414, 12.023977, -1.423661, -10.455915, 11.404599, -3.3882995, -24.767572, 12.860314, -12.76874, -27.996283, 17.21195, -9.537579, -27.347048, 11.217676, -9.664753, -27.347048, 11.217676, -9.664753, -27.996283, 17.21195, -9.537579, -26.84493, 17.364494, -4.4035864, -27.347048, 11.217676, -9.664753, -26.84493, 17.364494, -4.4035864, -26.195316, 12.778957, -5.113938, -24.767572, 12.860314, -12.76874, -25.952778, 17.118486, -13.655661, -27.996283, 17.21195, -9.537579, -19.715057, 11.632692, -13.049604, -25.952778, 17.118486, -13.655661, -24.767572, 12.860314, -12.76874, -27.996283, 17.21195, -9.537579, -25.235796, 21.763588, -5.4189744, -26.84493, 17.364494, -4.4035864, -19.715057, 11.632692, -13.049604, -19.985107, 17.543673, -13.411457, -25.952778, 17.118486, -13.655661, -15.851015, 13.295669, -9.844912, -19.985107, 17.543673, -13.411457, -19.715057, 11.632692, -13.049604, -25.952778, 17.118486, -13.655661, -25.451134, 23.26482, -8.394608, -27.996283, 17.21195, -9.537579, -27.996283, 17.21195, -9.537579, -25.451134, 23.26482, -8.394608, -25.235796, 21.763588, -5.4189744, -19.985107, 17.543673, -13.411457, -24.42836, 21.654142, -11.237154, -25.952778, 17.118486, -13.655661, -25.952778, 17.118486, -13.655661, -24.42836, 21.654142, -11.237154, -25.451134, 23.26482, -8.394608, -15.851015, 13.295669, -9.844912, -14.828638, 17.862318, -10.214518, -19.985107, 17.543673, -13.411457, -14.828638, 17.862318, -10.214518, -15.851015, 13.295669, -9.844912, -15.215309, 11.512592, -5.5114245, -19.985107, 17.543673, -13.411457, -21.066832, 23.347628, -10.820686, -24.42836, 21.654142, -11.237154, -14.828638, 17.862318, -10.214518, -21.066832, 23.347628, -10.820686, -19.985107, 17.543673, -13.411457, -24.42836, 21.654142, -11.237154, -22.043024, 25.28228, -7.397609, -25.451134, 23.26482, -8.394608, -24.42836, 21.654142, -11.237154, -21.066832, 23.347628, -10.820686, -22.043024, 25.28228, -7.397609, -14.828638, 17.862318, -10.214518, -17.778353, 21.984411, -9.297602, -21.066832, 23.347628, -10.820686, -14.828638, 17.862318, -10.214518, -15.215309, 11.512592, -5.5114245, -15.219456, 17.447784, -5.475563, -17.778353, 21.984411, -9.297602, -14.828638, 17.862318, -10.214518, -15.219456, 17.447784, -5.475563, -15.219456, 17.447784, -5.475563, -15.215309, 11.512592, -5.5114245, -18.649284, 12.710672, -2.540853, -21.066832, 23.347628, -10.820686, -17.778353, 21.984411, -9.297602, -18.417158, 23.323898, -6.4548, -17.778353, 21.984411, -9.297602, -15.219456, 17.447784, -5.475563, -18.417158, 23.323898, -6.4548, -21.066832, 23.347628, -10.820686, -18.417158, 23.323898, -6.4548, -22.043024, 25.28228, -7.397609, -15.219456, 17.447784, -5.475563, -18.649284, 12.710672, -2.540853, -18.8043, 16.973686, -1.7518203, -18.417158, 23.323898, -6.4548, -15.219456, 17.447784, -5.475563, -18.8043, 16.973686, -1.7518203, -22.846716, 11.097576, -2.1269307, -18.8043, 16.973686, -1.7518203, -18.649284, 12.710672, -2.540853, -22.043024, 25.28228, -7.397609, -18.417158, 23.323898, -6.4548, -19.955843, 21.589733, -3.8300514, -18.417158, 23.323898, -6.4548, -18.8043, 16.973686, -1.7518203, -19.955843, 21.589733, -3.8300514, -22.846716, 11.097576, -2.1269307, -23.23016, 17.115578, -1.6015713, -18.8043, 16.973686, -1.7518203, -23.23016, 17.115578, -1.6015713, -19.955843, 21.589733, -3.8300514, -18.8043, 16.973686, -1.7518203, -26.195316, 12.778957, -5.113938, -23.23016, 17.115578, -1.6015713, -22.846716, 11.097576, -2.1269307, -26.195316, 12.778957, -5.113938, -26.84493, 17.364494, -4.4035864, -23.23016, 17.115578, -1.6015713, -23.23016, 17.115578, -1.6015713, -22.801462, 23.240604, -4.0287204, -19.955843, 21.589733, -3.8300514, -22.043024, 25.28228, -7.397609, -19.955843, 21.589733, -3.8300514, -22.801462, 23.240604, -4.0287204, -26.84493, 17.364494, -4.4035864, -22.801462, 23.240604, -4.0287204, -23.23016, 17.115578, -1.6015713, -25.451134, 23.26482, -8.394608, -22.043024, 25.28228, -7.397609, -22.801462, 23.240604, -4.0287204, -26.84493, 17.364494, -4.4035864, -25.235796, 21.763588, -5.4189744, -22.801462, 23.240604, -4.0287204, -25.451134, 23.26482, -8.394608, -22.801462, 23.240604, -4.0287204, -25.235796, 21.763588, -5.4189744, -15.851015, 13.295669, -9.844912, -21.172243, 9.376487, -7.615418, -15.215309, 11.512592, -5.5114245, -15.851015, 13.295669, -9.844912, -19.715057, 11.632692, -13.049604, -21.172243, 9.376487, -7.615418, -15.215309, 11.512592, -5.5114245, -21.172243, 9.376487, -7.615418, -22.846716, 11.097576, -2.1269307, -15.215309, 11.512592, -5.5114245, -22.846716, 11.097576, -2.1269307, -18.649284, 12.710672, -2.540853, -21.172243, 9.376487, -7.615418, -26.195316, 12.778957, -5.113938, -22.846716, 11.097576, -2.1269307, -21.172243, 9.376487, -7.615418, -27.347048, 11.217676, -9.664753, -26.195316, 12.778957, -5.113938, -19.715057, 11.632692, -13.049604, -27.347048, 11.217676, -9.664753, -21.172243, 9.376487, -7.615418, -19.715057, 11.632692, -13.049604, -24.767572, 12.860314, -12.76874, -27.347048, 11.217676, -9.664753, 15.271845, 5.6313887, -13.373863, 13.937893, 6.0420103, -12.526823, 15.02121, 5.4305525, -13.542078, 15.271845, 5.6313887, -13.373863, 14.216074, 6.202837, -12.404688, 13.937893, 6.0420103, -12.526823, 14.216074, 6.202837, -12.404688, 12.987471, 7.6002665, -11.386524, 13.937893, 6.0420103, -12.526823, 14.216074, 6.202837, -12.404688, 13.085402, 7.59974, -11.366533, 12.987471, 7.6002665, -11.386524, 13.937893, 6.0420103, -12.526823, 12.987471, 7.6002665, -11.386524, 12.938522, 7.601056, -11.299387, 15.02121, 5.4305525, -13.542078, 13.937893, 6.0420103, -12.526823, 13.863859, 5.781687, -12.314682, 13.937893, 6.0420103, -12.526823, 12.938522, 7.601056, -11.299387, 13.863859, 5.781687, -12.314682, 15.02121, 5.4305525, -13.542078, 13.863859, 5.781687, -12.314682, 15.012333, 5.1052136, -13.381706, 13.863859, 5.781687, -12.314682, 12.938522, 7.601056, -11.299387, 13.0058975, 7.601056, -11.22595, 15.012333, 5.1052136, -13.381706, 13.863859, 5.781687, -12.314682, 14.096357, 5.781687, -12.061455, 13.863859, 5.781687, -12.314682, 13.0058975, 7.601056, -11.22595, 14.096357, 5.781687, -12.061455, 15.012333, 5.1052136, -13.381706, 14.096357, 5.781687, -12.061455, 15.257576, 5.1052136, -13.11451, 14.096357, 5.781687, -12.061455, 13.0058975, 7.601056, -11.22595, 13.096874, 7.6002665, -11.26737, 15.257576, 5.1052136, -13.11451, 14.096357, 5.781687, -12.061455, 14.314171, 6.0420103, -12.116919, 14.096357, 5.781687, -12.061455, 13.096874, 7.6002665, -11.26737, 14.314171, 6.0420103, -12.116919, 14.314171, 6.0420103, -12.116919, 13.096874, 7.6002665, -11.26737, 13.085402, 7.59974, -11.366533, 15.257576, 5.1052136, -13.11451, 14.314171, 6.0420103, -12.116919, 15.41803, 5.4305525, -13.109793, 14.314171, 6.0420103, -12.116919, 13.085402, 7.59974, -11.366533, 14.216074, 6.202837, -12.404688, 15.41803, 5.4305525, -13.109793, 14.314171, 6.0420103, -12.116919, 14.216074, 6.202837, -12.404688, 15.41803, 5.4305525, -13.109793, 14.216074, 6.202837, -12.404688, 15.271845, 5.6313887, -13.373863, 14.601847, 3.21346, -14.363449, 15.050756, 5.7482576, -13.183859, 14.252024, 3.21346, -13.499159, 14.601847, 3.21346, -14.363449, 15.172541, 5.7845817, -13.503508, 15.050756, 5.7482576, -13.183859, 15.172541, 5.7845817, -13.503508, 15.235462, 8.294637, -13.186392, 15.050756, 5.7482576, -13.183859, 15.172541, 5.7845817, -13.503508, 15.288689, 8.303849, -13.328491, 15.235462, 8.294637, -13.186392, 15.050756, 5.7482576, -13.183859, 15.235462, 8.294637, -13.186392, 15.35397, 8.27937, -13.092552, 14.252024, 3.21346, -13.499159, 15.050756, 5.7482576, -13.183859, 15.317219, 5.689823, -12.974655, 15.050756, 5.7482576, -13.183859, 15.35397, 8.27937, -13.092552, 15.317219, 5.689823, -12.974655, 14.252024, 3.21346, -13.499159, 15.317219, 5.689823, -12.974655, 14.968065, 3.21346, -12.951441, 15.317219, 5.689823, -12.974655, 15.35397, 8.27937, -13.092552, 15.480734, 8.27937, -13.17668, 14.968065, 3.21346, -12.951441, 15.317219, 5.689823, -12.974655, 15.603647, 5.689823, -13.16474, 15.317219, 5.689823, -12.974655, 15.480734, 8.27937, -13.17668, 15.603647, 5.689823, -13.16474, 14.968065, 3.21346, -12.951441, 15.603647, 5.689823, -13.16474, 15.760677, 3.21346, -13.477451, 15.603647, 5.689823, -13.16474, 15.480734, 8.27937, -13.17668, 15.440304, 8.294637, -13.322334, 15.760677, 3.21346, -13.477451, 15.603647, 5.689823, -13.16474, 15.514393, 5.7482576, -13.491547, 15.603647, 5.689823, -13.16474, 15.440304, 8.294637, -13.322334, 15.514393, 5.7482576, -13.491547, 15.514393, 5.7482576, -13.491547, 15.440304, 8.294637, -13.322334, 15.288689, 8.303849, -13.328491, 15.760677, 3.21346, -13.477451, 15.514393, 5.7482576, -13.491547, 15.534151, 3.2134595, -14.350031, 15.514393, 5.7482576, -13.491547, 15.288689, 8.303849, -13.328491, 15.172541, 5.7845817, -13.503508, 15.534151, 3.2134595, -14.350031, 15.514393, 5.7482576, -13.491547, 15.172541, 5.7845817, -13.503508, 15.534151, 3.2134595, -14.350031, 15.172541, 5.7845817, -13.503508, 14.601847, 3.21346, -14.363449, 15.553728, 5.9588323, -13.311363, 17.159096, 6.3768234, -13.211996, 15.660074, 5.754575, -13.023067, 15.660074, 5.754575, -13.023067, 15.124199, 5.6650805, -13.219332, 15.553728, 5.9588323, -13.311363, 15.660074, 5.754575, -13.023067, 15.190983, 5.5363665, -13.037777, 15.124199, 5.6650805, -13.219332, 15.553728, 5.9588323, -13.311363, 17.002844, 6.540546, -13.478861, 17.159096, 6.3768234, -13.211996, 17.002844, 6.540546, -13.478861, 18.62324, 7.962454, -13.583435, 17.159096, 6.3768234, -13.211996, 17.002844, 6.540546, -13.478861, 18.554626, 7.9619274, -13.658262, 18.62324, 7.962454, -13.583435, 17.159096, 6.3768234, -13.211996, 18.62324, 7.962454, -13.583435, 18.715752, 7.9632435, -13.625875, 17.159096, 6.3768234, -13.211996, 18.715752, 7.9632435, -13.625875, 17.346697, 6.111762, -13.342499, 15.660074, 5.754575, -13.023067, 17.159096, 6.3768234, -13.211996, 17.346697, 6.111762, -13.342499, 17.346697, 6.111762, -13.342499, 18.715752, 7.9632435, -13.625875, 18.704063, 7.9632435, -13.726791, 15.762983, 5.4234457, -13.149805, 15.190983, 5.5363665, -13.037777, 15.660074, 5.754575, -13.023067, 15.660074, 5.754575, -13.023067, 17.346697, 6.111762, -13.342499, 15.762983, 5.4234457, -13.149805, 15.762983, 5.4234457, -13.149805, 15.255903, 5.327897, -13.117506, 15.190983, 5.5363665, -13.037777, 17.346697, 6.111762, -13.342499, 18.704063, 7.9632435, -13.726791, 17.30647, 6.111762, -13.690155, 15.762983, 5.4234457, -13.149805, 17.346697, 6.111762, -13.342499, 17.30647, 6.111762, -13.690155, 17.30647, 6.111762, -13.690155, 18.704063, 7.9632435, -13.726791, 18.604456, 7.962454, -13.746929, 15.72068, 5.4234457, -13.5163, 15.255903, 5.327897, -13.117506, 15.762983, 5.4234457, -13.149805, 15.762983, 5.4234457, -13.149805, 17.30647, 6.111762, -13.690155, 15.72068, 5.4234457, -13.5163, 15.72068, 5.4234457, -13.5163, 15.229303, 5.327897, -13.348473, 15.255903, 5.327897, -13.117506, 17.30647, 6.111762, -13.690155, 18.604456, 7.962454, -13.746929, 17.094133, 6.3768234, -13.774481, 15.72068, 5.4234457, -13.5163, 17.30647, 6.111762, -13.690155, 17.094133, 6.3768234, -13.774481, 17.094133, 6.3768234, -13.774481, 18.604456, 7.962454, -13.746929, 18.554626, 7.9619274, -13.658262, 17.094133, 6.3768234, -13.774481, 18.554626, 7.9619274, -13.658262, 17.002844, 6.540546, -13.478861, 15.72068, 5.4234457, -13.5163, 17.094133, 6.3768234, -13.774481, 15.591234, 5.754575, -13.61615, 15.591234, 5.754575, -13.61615, 15.229303, 5.327897, -13.348473, 15.72068, 5.4234457, -13.5163, 15.591234, 5.754575, -13.61615, 17.094133, 6.3768234, -13.774481, 17.002844, 6.540546, -13.478861, 15.591234, 5.754575, -13.61615, 15.147818, 5.5363665, -13.411282, 15.229303, 5.327897, -13.348473, 15.591234, 5.754575, -13.61615, 17.002844, 6.540546, -13.478861, 15.553728, 5.9588323, -13.311363, 15.553728, 5.9588323, -13.311363, 15.147818, 5.5363665, -13.411282, 15.591234, 5.754575, -13.61615, 15.553728, 5.9588323, -13.311363, 15.124199, 5.6650805, -13.219332, 15.147818, 5.5363665, -13.411282, 20.456669, 8.048263, -14.631794, 19.118647, 8.967159, -15.3941345, 19.346407, 7.711606, -15.0057125, 19.346407, 7.711606, -15.0057125, 19.118647, 8.967159, -15.3941345, 17.589626, 8.974004, -14.720594, 19.346407, 7.711606, -15.0057125, 17.589626, 8.974004, -14.720594, 18.110191, 8.048264, -14.360323, 20.456669, 8.048263, -14.631794, 20.670847, 8.974003, -15.076887, 19.118647, 8.967159, -15.3941345, 20.868176, 7.746087, -13.371468, 20.670847, 8.974003, -15.076887, 20.456669, 8.048263, -14.631794, 19.118647, 8.967159, -15.3941345, 18.014603, 9.910537, -14.417248, 17.589626, 8.974004, -14.720594, 20.868176, 7.746087, -13.371468, 20.929647, 8.994798, -13.616339, 20.670847, 8.974003, -15.076887, 20.26757, 8.091167, -12.117716, 20.929647, 8.994798, -13.616339, 20.868176, 7.746087, -13.371468, 20.670847, 8.974003, -15.076887, 18.914066, 10.236665, -14.791771, 19.118647, 8.967159, -15.3941345, 19.118647, 8.967159, -15.3941345, 18.914066, 10.236665, -14.791771, 18.014603, 9.910537, -14.417248, 20.929647, 8.994798, -13.616339, 19.975527, 9.910536, -14.644192, 20.670847, 8.974003, -15.076887, 20.670847, 8.974003, -15.076887, 19.975527, 9.910536, -14.644192, 18.914066, 10.236665, -14.791771, 20.26757, 8.091167, -12.117716, 20.306274, 9.015329, -12.053684, 20.929647, 8.994798, -13.616339, 20.306274, 9.015329, -12.053684, 20.26757, 8.091167, -12.117716, 19.308578, 7.78057, -11.381147, 20.929647, 8.994798, -13.616339, 20.10909, 10.243508, -13.758886, 19.975527, 9.910536, -14.644192, 20.306274, 9.015329, -12.053684, 20.10909, 10.243508, -13.758886, 20.929647, 8.994798, -13.616339, 19.975527, 9.910536, -14.644192, 18.948578, 10.659921, -13.703939, 18.914066, 10.236665, -14.791771, 19.975527, 9.910536, -14.644192, 20.10909, 10.243508, -13.758886, 18.948578, 10.659921, -13.703939, 20.306274, 9.015329, -12.053684, 19.881817, 9.928962, -12.708981, 20.10909, 10.243508, -13.758886, 20.306274, 9.015329, -12.053684, 19.308578, 7.78057, -11.381147, 19.211441, 9.022436, -11.43041, 19.881817, 9.928962, -12.708981, 20.306274, 9.015329, -12.053684, 19.211441, 9.022436, -11.43041, 19.211441, 9.022436, -11.43041, 19.308578, 7.78057, -11.381147, 18.306646, 8.091168, -11.890772, 20.10909, 10.243508, -13.758886, 19.881817, 9.928962, -12.708981, 19.091122, 10.250615, -12.470457, 19.881817, 9.928962, -12.708981, 19.211441, 9.022436, -11.43041, 19.091122, 10.250615, -12.470457, 20.10909, 10.243508, -13.758886, 19.091122, 10.250615, -12.470457, 18.948578, 10.659921, -13.703939, 19.211441, 9.022436, -11.43041, 18.306646, 8.091168, -11.890772, 18.093428, 9.015329, -11.79792, 19.091122, 10.250615, -12.470457, 19.211441, 9.022436, -11.43041, 18.093428, 9.015329, -11.79792, 17.78659, 7.746088, -13.015247, 18.093428, 9.015329, -11.79792, 18.306646, 8.091168, -11.890772, 18.948578, 10.659921, -13.703939, 19.091122, 10.250615, -12.470457, 18.306887, 9.928962, -12.526855, 19.091122, 10.250615, -12.470457, 18.093428, 9.015329, -11.79792, 18.306887, 9.928962, -12.526855, 17.78659, 7.746088, -13.015247, 17.400074, 8.994799, -13.208279, 18.093428, 9.015329, -11.79792, 17.400074, 8.994799, -13.208279, 18.306887, 9.928962, -12.526855, 18.093428, 9.015329, -11.79792, 18.110191, 8.048264, -14.360323, 17.400074, 8.994799, -13.208279, 17.78659, 7.746088, -13.015247, 18.110191, 8.048264, -14.360323, 17.589626, 8.974004, -14.720594, 17.400074, 8.994799, -13.208279, 17.400074, 8.994799, -13.208279, 17.896025, 10.243508, -13.502977, 18.306887, 9.928962, -12.526855, 18.948578, 10.659921, -13.703939, 18.306887, 9.928962, -12.526855, 17.896025, 10.243508, -13.502977, 17.589626, 8.974004, -14.720594, 17.896025, 10.243508, -13.502977, 17.400074, 8.994799, -13.208279, 18.914066, 10.236665, -14.791771, 18.948578, 10.659921, -13.703939, 17.896025, 10.243508, -13.502977, 17.589626, 8.974004, -14.720594, 18.014603, 9.910537, -14.417248, 17.896025, 10.243508, -13.502977, 18.914066, 10.236665, -14.791771, 17.896025, 10.243508, -13.502977, 18.014603, 9.910537, -14.417248, 20.26757, 8.091167, -12.117716, 19.381508, 7.3296757, -13.120605, 19.308578, 7.78057, -11.381147, 20.26757, 8.091167, -12.117716, 20.868176, 7.746087, -13.371468, 19.381508, 7.3296757, -13.120605, 19.308578, 7.78057, -11.381147, 19.381508, 7.3296757, -13.120605, 17.78659, 7.746088, -13.015247, 19.308578, 7.78057, -11.381147, 17.78659, 7.746088, -13.015247, 18.306646, 8.091168, -11.890772, 19.381508, 7.3296757, -13.120605, 18.110191, 8.048264, -14.360323, 17.78659, 7.746088, -13.015247, 19.381508, 7.3296757, -13.120605, 19.346407, 7.711606, -15.0057125, 18.110191, 8.048264, -14.360323, 20.868176, 7.746087, -13.371468, 19.346407, 7.711606, -15.0057125, 19.381508, 7.3296757, -13.120605, 20.868176, 7.746087, -13.371468, 20.456669, 8.048263, -14.631794, 19.346407, 7.711606, -15.0057125, 10.316184, 8.502844, -12.777889, 10.408148, 10.86813, -10.2968025, 10.597721, 7.6100063, -10.6023655, 10.597721, 7.6100063, -10.6023655, 10.408148, 10.86813, -10.2968025, 12.878351, 10.951043, -8.855707, 10.597721, 7.6100063, -10.6023655, 12.878351, 10.951043, -8.855707, 12.835861, 8.458624, -9.37719, 10.316184, 8.502844, -12.777889, 9.524521, 10.8173275, -12.634127, 10.408148, 10.86813, -10.2968025, 12.073738, 7.8355837, -14.893578, 9.524521, 10.8173275, -12.634127, 10.316184, 8.502844, -12.777889, 10.408148, 10.86813, -10.2968025, 13.069418, 13.342123, -9.872105, 12.878351, 10.951043, -8.855707, 12.073738, 7.8355837, -14.893578, 11.829742, 11.048433, -14.919933, 9.524521, 10.8173275, -12.634127, 14.779421, 8.739476, -15.246426, 11.829742, 11.048433, -14.919933, 12.073738, 7.8355837, -14.893578, 9.524521, 10.8173275, -12.634127, 11.80493, 14.158102, -10.887323, 10.408148, 10.86813, -10.2968025, 10.408148, 10.86813, -10.2968025, 11.80493, 14.158102, -10.887323, 13.069418, 13.342123, -9.872105, 11.829742, 11.048433, -14.919933, 11.051523, 13.282636, -12.346281, 9.524521, 10.8173275, -12.634127, 9.524521, 10.8173275, -12.634127, 11.051523, 13.282636, -12.346281, 11.80493, 14.158102, -10.887323, 14.779421, 8.739476, -15.246426, 15.010525, 11.22163, -15.7902565, 11.829742, 11.048433, -14.919933, 15.010525, 11.22163, -15.7902565, 14.779421, 8.739476, -15.246426, 16.7401, 7.770304, -13.896193, 11.829742, 11.048433, -14.919933, 12.461086, 14.203112, -13.530659, 11.051523, 13.282636, -12.346281, 15.010525, 11.22163, -15.7902565, 12.461086, 14.203112, -13.530659, 11.829742, 11.048433, -14.919933, 11.051523, 13.282636, -12.346281, 13.462879, 15.254671, -11.875458, 11.80493, 14.158102, -10.887323, 11.051523, 13.282636, -12.346281, 12.461086, 14.203112, -13.530659, 13.462879, 15.254671, -11.875458, 15.010525, 11.22163, -15.7902565, 14.284246, 13.462151, -14.276512, 12.461086, 14.203112, -13.530659, 15.010525, 11.22163, -15.7902565, 16.7401, 7.770304, -13.896193, 16.752848, 10.996315, -13.881273, 14.284246, 13.462151, -14.276512, 15.010525, 11.22163, -15.7902565, 16.752848, 10.996315, -13.881273, 16.752848, 10.996315, -13.881273, 16.7401, 7.770304, -13.896193, 16.65236, 8.421508, -11.429792, 12.461086, 14.203112, -13.530659, 14.284246, 13.462151, -14.276512, 15.179819, 14.190213, -12.970345, 14.284246, 13.462151, -14.276512, 16.752848, 10.996315, -13.881273, 15.179819, 14.190213, -12.970345, 12.461086, 14.203112, -13.530659, 15.179819, 14.190213, -12.970345, 13.462879, 15.254671, -11.875458, 16.752848, 10.996315, -13.881273, 16.65236, 8.421508, -11.429792, 16.909176, 10.738624, -11.076134, 15.179819, 14.190213, -12.970345, 16.752848, 10.996315, -13.881273, 16.909176, 10.738624, -11.076134, 15.264158, 7.544727, -9.605343, 16.909176, 10.738624, -11.076134, 16.65236, 8.421508, -11.429792, 13.462879, 15.254671, -11.875458, 15.179819, 14.190213, -12.970345, 15.655621, 13.247626, -11.386549, 15.179819, 14.190213, -12.970345, 16.909176, 10.738624, -11.076134, 15.655621, 13.247626, -11.386549, 15.264158, 7.544727, -9.605343, 15.3314705, 10.815748, -9.258287, 16.909176, 10.738624, -11.076134, 15.3314705, 10.815748, -9.258287, 15.655621, 13.247626, -11.386549, 16.909176, 10.738624, -11.076134, 12.835861, 8.458624, -9.37719, 15.3314705, 10.815748, -9.258287, 15.264158, 7.544727, -9.605343, 12.835861, 8.458624, -9.37719, 12.878351, 10.951043, -8.855707, 15.3314705, 10.815748, -9.258287, 15.3314705, 10.815748, -9.258287, 14.523663, 14.14494, -10.327007, 15.655621, 13.247626, -11.386549, 13.462879, 15.254671, -11.875458, 15.655621, 13.247626, -11.386549, 14.523663, 14.14494, -10.327007, 12.878351, 10.951043, -8.855707, 14.523663, 14.14494, -10.327007, 15.3314705, 10.815748, -9.258287, 11.80493, 14.158102, -10.887323, 13.462879, 15.254671, -11.875458, 14.523663, 14.14494, -10.327007, 12.878351, 10.951043, -8.855707, 13.069418, 13.342123, -9.872105, 14.523663, 14.14494, -10.327007, 11.80493, 14.158102, -10.887323, 14.523663, 14.14494, -10.327007, 13.069418, 13.342123, -9.872105, 14.779421, 8.739476, -15.246426, 13.698336, 6.609247, -12.302765, 16.7401, 7.770304, -13.896193, 14.779421, 8.739476, -15.246426, 12.073738, 7.8355837, -14.893578, 13.698336, 6.609247, -12.302765, 16.7401, 7.770304, -13.896193, 13.698336, 6.609247, -12.302765, 15.264158, 7.544727, -9.605343, 16.7401, 7.770304, -13.896193, 15.264158, 7.544727, -9.605343, 16.65236, 8.421508, -11.429792, 13.698336, 6.609247, -12.302765, 12.835861, 8.458624, -9.37719, 15.264158, 7.544727, -9.605343, 13.698336, 6.609247, -12.302765, 10.597721, 7.6100063, -10.6023655, 12.835861, 8.458624, -9.37719, 12.073738, 7.8355837, -14.893578, 10.597721, 7.6100063, -10.6023655, 13.698336, 6.609247, -12.302765, 12.073738, 7.8355837, -14.893578, 10.316184, 8.502844, -12.777889, 10.597721, 7.6100063, -10.6023655, 0.9335377, -18.661976, -2.173924, 1.8889831, -18.823338, -0.58916974, 1.5114222, -18.399822, -1.4170885, 0.9335377, -18.661976, -2.173924, 0.9335377, -18.661974, 1.0975094, 1.8889831, -18.823338, -0.58916974, 1.8889831, -18.823338, -0.58916974, 0.9335377, -18.661974, 1.0975094, 1.6508096, -18.476696, 0.3862325, -0.9279604, -18.475744, -2.1898937, 0.9335377, -18.661976, -2.173924, -0.001098156, -18.845234, -2.413155, -1.7685199, -18.399822, -0.5382087, 0.9335377, -18.661976, -2.173924, -0.9279604, -18.475744, -2.1898937, 0.9335377, -18.661974, 1.0975094, 0.9335377, -18.661976, -2.173924, -1.7685199, -18.399822, -0.5382087, -1.7685199, -18.399822, -0.5382087, -0.9279604, -18.475744, -2.1898937, -1.6464877, -18.661976, -1.4825313, 0.9335377, -18.661974, 1.0975094, -1.7685199, -18.399822, -0.5382087, -0.95520806, -18.661974, 1.0975094, -0.95520806, -18.661974, 1.0975094, -1.7685199, -18.399822, -0.5382087, -1.6464877, -18.661974, 0.40619445, 0.9335377, -18.661974, 1.0975094, -0.95520806, -18.661974, 1.0975094, -0.010835171, -18.399822, 1.2195511, 1.5114222, -18.399822, -1.4170885, -0.010835171, -14.884386, -0.5382099, 0.9335377, -18.661976, -2.173924, 0.9335377, -18.661976, -2.173924, -0.010835171, -14.884386, -0.5382099, -0.001098156, -18.845234, -2.413155, -0.001098156, -18.845234, -2.413155, -0.010835171, -14.884386, -0.5382099, -0.9279604, -18.475744, -2.1898937, -0.9279604, -18.475744, -2.1898937, -0.010835171, -14.884386, -0.5382099, -1.6464877, -18.661976, -1.4825313, -1.6464877, -18.661976, -1.4825313, -0.010835171, -14.884386, -0.5382099, -1.7685199, -18.399822, -0.5382087, -1.7685199, -18.399822, -0.5382087, -0.010835171, -14.884386, -0.5382099, -1.6464877, -18.661974, 0.40619445, -1.6464877, -18.661974, 0.40619445, -0.010835171, -14.884386, -0.5382099, -0.95520806, -18.661974, 1.0975094, -0.95520806, -18.661974, 1.0975094, -0.010835171, -14.884386, -0.5382099, -0.010835171, -18.399822, 1.2195511, -0.010835171, -18.399822, 1.2195511, -0.010835171, -14.884386, -0.5382099, 0.9335377, -18.661974, 1.0975094, 0.9335377, -18.661974, 1.0975094, -0.010835171, -14.884386, -0.5382099, 1.6508096, -18.476696, 0.3862325, 1.6508096, -18.476696, 0.3862325, -0.010835171, -14.884386, -0.5382099, 1.8889831, -18.823338, -0.58916974, 1.8889831, -18.823338, -0.58916974, -0.010835171, -14.884386, -0.5382099, 1.5114222, -18.399822, -1.4170885, 0.79053855, -17.29707, -2.0162632, 1.2353985, -16.7235, -0.5382092, 1.198884, -17.024681, -1.2366474, 0.79053855, -17.29707, -2.0162632, 0.4979446, -16.83131, 0.34912467, 1.2353985, -16.7235, -0.5382092, 1.2353985, -16.7235, -0.5382092, 0.4979446, -16.83131, 0.34912467, 1.198884, -17.024681, 0.16022909, 0.4979446, -16.83131, 0.34912467, -0.88492775, -17.22686, 1.0175039, -0.010835171, -17.024681, 0.8586674, 0.4979446, -16.83131, 0.34912467, -1.2205544, -17.024681, 0.16022909, -0.88492775, -17.22686, 1.0175039, 0.79053855, -17.29707, -2.0162632, -1.2205544, -17.024681, 0.16022909, 0.4979446, -16.83131, 0.34912467, -1.2205544, -17.024681, 0.16022909, 0.79053855, -17.29707, -2.0162632, -0.6996014, -16.880219, -1.4757214, -0.6996014, -16.880219, -1.4757214, 0.79053855, -17.29707, -2.0162632, -0.010835171, -17.024681, -1.9350855, -1.2205544, -17.024681, 0.16022909, -0.6996014, -16.880219, -1.4757214, -1.2205544, -17.024681, -1.2366474, -1.2205544, -17.024681, 0.16022909, -1.2205544, -17.024681, -1.2366474, -1.5400834, -17.234833, -0.5375037, 1.198884, -17.024681, -1.2366474, -0.010835171, -14.231086, -0.53821015, 0.79053855, -17.29707, -2.0162632, 0.79053855, -17.29707, -2.0162632, -0.010835171, -14.231086, -0.53821015, -0.010835171, -17.024681, -1.9350855, -0.010835171, -17.024681, -1.9350855, -0.010835171, -14.231086, -0.53821015, -0.6996014, -16.880219, -1.4757214, -0.6996014, -16.880219, -1.4757214, -0.010835171, -14.231086, -0.53821015, -1.2205544, -17.024681, -1.2366474, -1.2205544, -17.024681, -1.2366474, -0.010835171, -14.231086, -0.53821015, -1.5400834, -17.234833, -0.5375037, -1.5400834, -17.234833, -0.5375037, -0.010835171, -14.231086, -0.53821015, -1.2205544, -17.024681, 0.16022909, -1.2205544, -17.024681, 0.16022909, -0.010835171, -14.231086, -0.53821015, -0.88492775, -17.22686, 1.0175039, -0.88492775, -17.22686, 1.0175039, -0.010835171, -14.231086, -0.53821015, -0.010835171, -17.024681, 0.8586674, -0.010835171, -17.024681, 0.8586674, -0.010835171, -14.231086, -0.53821015, 0.4979446, -16.83131, 0.34912467, 0.4979446, -16.83131, 0.34912467, -0.010835171, -14.231086, -0.53821015, 1.198884, -17.024681, 0.16022909, 1.198884, -17.024681, 0.16022909, -0.010835171, -14.231086, -0.53821015, 1.2353985, -16.7235, -0.5382092, 1.2353985, -16.7235, -0.5382092, -0.010835171, -14.231086, -0.53821015, 1.198884, -17.024681, -1.2366474, 1.3094504, -20.552734, -2.7694182, 2.5816762, -20.552734, -0.51041746, 2.2008963, -20.875694, -1.8151531, 1.3094504, -20.552734, -2.7694182, 1.2661028, -20.875694, 1.6735137, 2.5816762, -20.552734, -0.51041746, 2.5816762, -20.552734, -0.51041746, 1.2661028, -20.875694, 1.6735137, 2.2298722, -20.624132, 0.7323996, -1.2877731, -20.875696, -2.7499266, 1.3094504, -20.552734, -2.7694182, -0.039105892, -21.061094, -3.1164474, -2.5646338, -20.875694, -0.53820777, 1.3094504, -20.552734, -2.7694182, -1.2877731, -20.875696, -2.7499266, 1.2661028, -20.875694, 1.6735137, 1.3094504, -20.552734, -2.7694182, -2.5646338, -20.875694, -0.53820777, -2.5646338, -20.875694, -0.53820777, -1.2877731, -20.875696, -2.7499266, -2.1870723, -20.562729, -1.6952248, 1.2661028, -20.875694, 1.6735137, -2.5646338, -20.875694, -0.53820777, -1.2877731, -20.875694, 1.6735137, -1.2877731, -20.875694, 1.6735137, -2.5646338, -20.875694, -0.53820777, -2.1694045, -20.59831, 0.7562758, 1.2661028, -20.875694, 1.6735137, -1.2877731, -20.875694, 1.6735137, -0.033530712, -21.08287, 2.0138073, 2.2008963, -20.875694, -1.8151531, -0.010835171, -15.767945, -0.5382097, 1.3094504, -20.552734, -2.7694182, 1.3094504, -20.552734, -2.7694182, -0.010835171, -15.767945, -0.5382097, -0.039105892, -21.061094, -3.1164474, -0.039105892, -21.061094, -3.1164474, -0.010835171, -15.767945, -0.5382097, -1.2877731, -20.875696, -2.7499266, -1.2877731, -20.875696, -2.7499266, -0.010835171, -15.767945, -0.5382097, -2.1870723, -20.562729, -1.6952248, -2.1870723, -20.562729, -1.6952248, -0.010835171, -15.767945, -0.5382097, -2.5646338, -20.875694, -0.53820777, -2.5646338, -20.875694, -0.53820777, -0.010835171, -15.767945, -0.5382097, -2.1694045, -20.59831, 0.7562758, -2.1694045, -20.59831, 0.7562758, -0.010835171, -15.767945, -0.5382097, -1.2877731, -20.875694, 1.6735137, -1.2877731, -20.875694, 1.6735137, -0.010835171, -15.767945, -0.5382097, -0.033530712, -21.08287, 2.0138073, -0.033530712, -21.08287, 2.0138073, -0.010835171, -15.767945, -0.5382097, 1.2661028, -20.875694, 1.6735137, 1.2661028, -20.875694, 1.6735137, -0.010835171, -15.767945, -0.5382097, 2.2298722, -20.624132, 0.7323996, 2.2298722, -20.624132, 0.7323996, -0.010835171, -15.767945, -0.5382097, 2.5816762, -20.552734, -0.51041746, 2.5816762, -20.552734, -0.51041746, -0.010835171, -15.767945, -0.5382097, 2.2008963, -20.875694, -1.8151531, 0.5583327, -21.275528, -0.82769513, 0.37442207, -20.354485, -1.1662683, 0.31780314, -21.275528, -1.0682597, 0.5583327, -21.275528, -0.82769513, 0.65641344, -20.354485, -0.8842943, 0.37442207, -20.354485, -1.1662683, 0.6890812, -22.029617, -0.9031601, 0.5583327, -21.275528, -0.82769513, 0.31780314, -21.275528, -1.0682597, 0.6890812, -22.029617, -0.9031601, 0.31780314, -21.275528, -1.0682597, 0.39326835, -22.029617, -1.1989906, 0.31780314, -21.275528, -1.0682597, -0.010835171, -20.354485, -1.2695241, -0.010835171, -21.275528, -1.1563258, 0.31780314, -21.275528, -1.0682597, 0.37442207, -20.354485, -1.1662683, -0.010835171, -20.354485, -1.2695241, 0.39326835, -22.029617, -1.1989906, 0.31780314, -21.275528, -1.0682597, -0.010835171, -21.275528, -1.1563258, 0.39326835, -22.029617, -1.1989906, -0.010835171, -21.275528, -1.1563258, -0.010835171, -22.029617, -1.307256, -0.010835171, -21.275528, -1.1563258, -0.3960147, -20.354485, -1.1662683, -0.339396, -21.275528, -1.0682597, -0.010835171, -21.275528, -1.1563258, -0.010835171, -20.354485, -1.2695241, -0.3960147, -20.354485, -1.1662683, -0.010835171, -22.029617, -1.307256, -0.010835171, -21.275528, -1.1563258, -0.339396, -21.275528, -1.0682597, -0.010835171, -22.029617, -1.307256, -0.339396, -21.275528, -1.0682597, -0.4148612, -22.029617, -1.1989906, -0.339396, -21.275528, -1.0682597, -0.6780064, -20.354485, -0.8842943, -0.580003, -21.275528, -0.82769513, -0.339396, -21.275528, -1.0682597, -0.3960147, -20.354485, -1.1662683, -0.6780064, -20.354485, -0.8842943, -0.4148612, -22.029617, -1.1989906, -0.339396, -21.275528, -1.0682597, -0.580003, -21.275528, -0.82769513, -0.4148612, -22.029617, -1.1989906, -0.580003, -21.275528, -0.82769513, -0.71067405, -22.029617, -0.9031601, -0.580003, -21.275528, -0.82769513, -0.78126955, -20.354485, -0.49906468, -0.6680341, -21.275528, -0.49906445, -0.580003, -21.275528, -0.82769513, -0.6780064, -20.354485, -0.8842943, -0.78126955, -20.354485, -0.49906468, -0.71067405, -22.029617, -0.9031601, -0.580003, -21.275528, -0.82769513, -0.6680341, -21.275528, -0.49906445, -0.71067405, -22.029617, -0.9031601, -0.6680341, -21.275528, -0.49906445, -0.8189645, -22.029617, -0.49906397, -0.6680341, -21.275528, -0.49906445, -0.6780064, -20.354485, -0.11391497, -0.580003, -21.275528, -0.17051363, -0.6680341, -21.275528, -0.49906445, -0.78126955, -20.354485, -0.49906468, -0.6780064, -20.354485, -0.11391497, -0.8189645, -22.029617, -0.49906397, -0.6680341, -21.275528, -0.49906445, -0.580003, -21.275528, -0.17051363, -0.8189645, -22.029617, -0.49906397, -0.580003, -21.275528, -0.17051363, -0.71067405, -22.029617, -0.09504819, -0.580003, -21.275528, -0.17051363, -0.3960147, -20.354485, 0.16813636, -0.339396, -21.275528, 0.07004857, -0.580003, -21.275528, -0.17051363, -0.6780064, -20.354485, -0.11391497, -0.3960147, -20.354485, 0.16813636, -0.71067405, -22.029617, -0.09504819, -0.580003, -21.275528, -0.17051363, -0.339396, -21.275528, 0.07004857, -0.71067405, -22.029617, -0.09504819, -0.339396, -21.275528, 0.07004857, -0.4148612, -22.029617, 0.2007823, -0.339396, -21.275528, 0.07004857, -0.010835171, -20.354485, 0.27131462, -0.010835171, -21.275528, 0.15811706, -0.339396, -21.275528, 0.07004857, -0.3960147, -20.354485, 0.16813636, -0.010835171, -20.354485, 0.27131462, -0.4148612, -22.029617, 0.2007823, -0.339396, -21.275528, 0.07004857, -0.010835171, -21.275528, 0.15811706, -0.4148612, -22.029617, 0.2007823, -0.010835171, -21.275528, 0.15811706, -0.010835171, -22.029617, 0.3090477, -0.010835171, -21.275528, 0.15811706, 0.37442207, -20.354485, 0.16813636, 0.31780314, -21.275528, 0.07004857, -0.010835171, -21.275528, 0.15811706, -0.010835171, -20.354485, 0.27131462, 0.37442207, -20.354485, 0.16813636, -0.010835171, -22.029617, 0.3090477, -0.010835171, -21.275528, 0.15811706, 0.31780314, -21.275528, 0.07004857, -0.010835171, -22.029617, 0.3090477, 0.31780314, -21.275528, 0.07004857, 0.39326835, -22.029617, 0.2007823, 0.31780314, -21.275528, 0.07004857, 0.65641344, -20.354485, -0.11391497, 0.5583327, -21.275528, -0.17051363, 0.31780314, -21.275528, 0.07004857, 0.37442207, -20.354485, 0.16813636, 0.65641344, -20.354485, -0.11391497, 0.39326835, -22.029617, 0.2007823, 0.31780314, -21.275528, 0.07004857, 0.5583327, -21.275528, -0.17051363, 0.39326835, -22.029617, 0.2007823, 0.5583327, -21.275528, -0.17051363, 0.6890812, -22.029617, -0.09504819, 0.5583327, -21.275528, -0.17051363, 0.759599, -20.354485, -0.49906468, 0.64644134, -21.275528, -0.49906445, 0.5583327, -21.275528, -0.17051363, 0.65641344, -20.354485, -0.11391497, 0.759599, -20.354485, -0.49906468, 0.6890812, -22.029617, -0.09504819, 0.5583327, -21.275528, -0.17051363, 0.64644134, -21.275528, -0.49906445, 0.6890812, -22.029617, -0.09504819, 0.64644134, -21.275528, -0.49906445, 0.7973716, -22.029617, -0.49906397, 0.64644134, -21.275528, -0.49906445, 0.65641344, -20.354485, -0.8842943, 0.5583327, -21.275528, -0.82769513, 0.64644134, -21.275528, -0.49906445, 0.759599, -20.354485, -0.49906468, 0.65641344, -20.354485, -0.8842943, 0.7973716, -22.029617, -0.49906397, 0.64644134, -21.275528, -0.49906445, 0.5583327, -21.275528, -0.82769513, 0.7973716, -22.029617, -0.49906397, 0.5583327, -21.275528, -0.82769513, 0.6890812, -22.029617, -0.9031601, 0.39326835, -22.029617, -1.1989906, -0.010835171, -22.029617, -0.49906397, 0.6890812, -22.029617, -0.9031601, 0.6890812, -22.029617, -0.9031601, -0.010835171, -22.029617, -0.49906397, 0.7973716, -22.029617, -0.49906397, -0.010835171, -22.029617, -1.307256, -0.010835171, -22.029617, -0.49906397, 0.39326835, -22.029617, -1.1989906, 0.7973716, -22.029617, -0.49906397, -0.010835171, -22.029617, -0.49906397, 0.6890812, -22.029617, -0.09504819, -0.4148612, -22.029617, -1.1989906, -0.010835171, -22.029617, -0.49906397, -0.010835171, -22.029617, -1.307256, 0.6890812, -22.029617, -0.09504819, -0.010835171, -22.029617, -0.49906397, 0.39326835, -22.029617, 0.2007823, -0.71067405, -22.029617, -0.9031601, -0.010835171, -22.029617, -0.49906397, -0.4148612, -22.029617, -1.1989906, 0.39326835, -22.029617, 0.2007823, -0.010835171, -22.029617, -0.49906397, -0.010835171, -22.029617, 0.3090477, -0.8189645, -22.029617, -0.49906397, -0.010835171, -22.029617, -0.49906397, -0.71067405, -22.029617, -0.9031601, -0.010835171, -22.029617, 0.3090477, -0.010835171, -22.029617, -0.49906397, -0.4148612, -22.029617, 0.2007823, -0.71067405, -22.029617, -0.09504819, -0.010835171, -22.029617, -0.49906397, -0.8189645, -22.029617, -0.49906397, -0.4148612, -22.029617, 0.2007823, -0.010835171, -22.029617, -0.49906397, -0.71067405, -22.029617, -0.09504819, 9.266797, -17.581049, 9.7409115, 10.985607, -16.33984, 10.792019, 10.863857, -18.049467, 10.526733, 10.863857, -18.049467, 10.526733, 10.985607, -16.33984, 10.792019, 13.068386, -16.296267, 9.768465, 10.863857, -18.049467, 10.526733, 13.068386, -16.296267, 9.768465, 12.696516, -17.604118, 9.563199, 9.266797, -17.581049, 9.7409115, 9.008772, -16.366539, 10.340904, 10.985607, -16.33984, 10.792019, 8.624725, -17.931135, 7.6034517, 9.008772, -16.366539, 10.340904, 9.266797, -17.581049, 9.7409115, 10.985607, -16.33984, 10.792019, 12.4669, -15.041601, 9.18321, 13.068386, -16.296267, 9.768465, 8.624725, -17.931135, 7.6034517, 8.497637, -16.245218, 7.7565994, 9.008772, -16.366539, 10.340904, 9.600649, -17.456738, 5.615509, 8.497637, -16.245218, 7.7565994, 8.624725, -17.931135, 7.6034517, 9.008772, -16.366539, 10.340904, 11.213087, -14.61334, 9.582636, 10.985607, -16.33984, 10.792019, 10.985607, -16.33984, 10.792019, 11.213087, -14.61334, 9.582636, 12.4669, -15.041601, 9.18321, 8.497637, -16.245218, 7.7565994, 9.8885765, -15.072786, 9.437603, 9.008772, -16.366539, 10.340904, 9.008772, -16.366539, 10.340904, 9.8885765, -15.072786, 9.437603, 11.213087, -14.61334, 9.582636, 9.600649, -17.456738, 5.615509, 9.33643, -16.15444, 5.2154427, 8.497637, -16.245218, 7.7565994, 9.33643, -16.15444, 5.2154427, 9.600649, -17.456738, 5.615509, 11.392937, -17.965525, 4.8956895, 8.497637, -16.245218, 7.7565994, 9.720263, -14.589846, 7.952894, 9.8885765, -15.072786, 9.437603, 9.33643, -16.15444, 5.2154427, 9.720263, -14.589846, 7.952894, 8.497637, -16.245218, 7.7565994, 9.8885765, -15.072786, 9.437603, 11.288914, -14.037912, 8.018254, 11.213087, -14.61334, 9.582636, 9.8885765, -15.072786, 9.437603, 9.720263, -14.589846, 7.952894, 11.288914, -14.037912, 8.018254, 9.33643, -16.15444, 5.2154427, 10.034036, -14.978592, 6.3855233, 9.720263, -14.589846, 7.952894, 9.33643, -16.15444, 5.2154427, 11.392937, -17.965525, 4.8956895, 11.408957, -16.27256, 4.8937664, 10.034036, -14.978592, 6.3855233, 9.33643, -16.15444, 5.2154427, 11.408957, -16.27256, 4.8937664, 11.408957, -16.27256, 4.8937664, 11.392937, -17.965525, 4.8956895, 13.021395, -17.62377, 6.061926, 9.720263, -14.589846, 7.952894, 10.034036, -14.978592, 6.3855233, 11.318819, -14.596681, 6.3662996, 10.034036, -14.978592, 6.3855233, 11.408957, -16.27256, 4.8937664, 11.318819, -14.596681, 6.3662996, 9.720263, -14.589846, 7.952894, 11.318819, -14.596681, 6.3662996, 11.288914, -14.037912, 8.018254, 11.408957, -16.27256, 4.8937664, 13.021395, -17.62377, 6.061926, 13.375538, -16.407764, 6.04676, 11.318819, -14.596681, 6.3662996, 11.408957, -16.27256, 4.8937664, 13.375538, -16.407764, 6.04676, 13.632067, -18.083643, 7.819184, 13.375538, -16.407764, 6.04676, 13.021395, -17.62377, 6.061926, 11.288914, -14.037912, 8.018254, 11.318819, -14.596681, 6.3662996, 12.603388, -15.091156, 6.7550454, 11.318819, -14.596681, 6.3662996, 13.375538, -16.407764, 6.04676, 12.603388, -15.091156, 6.7550454, 13.632067, -18.083643, 7.819184, 13.896927, -16.367395, 7.9291854, 13.375538, -16.407764, 6.04676, 13.896927, -16.367395, 7.9291854, 12.603388, -15.091156, 6.7550454, 13.375538, -16.407764, 6.04676, 12.696516, -17.604118, 9.563199, 13.896927, -16.367395, 7.9291854, 13.632067, -18.083643, 7.819184, 12.696516, -17.604118, 9.563199, 13.068386, -16.296267, 9.768465, 13.896927, -16.367395, 7.9291854, 13.896927, -16.367395, 7.9291854, 12.811857, -14.620176, 7.996041, 12.603388, -15.091156, 6.7550454, 11.288914, -14.037912, 8.018254, 12.603388, -15.091156, 6.7550454, 12.811857, -14.620176, 7.996041, 13.068386, -16.296267, 9.768465, 12.811857, -14.620176, 7.996041, 13.896927, -16.367395, 7.9291854, 11.213087, -14.61334, 9.582636, 11.288914, -14.037912, 8.018254, 12.811857, -14.620176, 7.996041, 13.068386, -16.296267, 9.768465, 12.4669, -15.041601, 9.18321, 12.811857, -14.620176, 7.996041, 11.213087, -14.61334, 9.582636, 12.811857, -14.620176, 7.996041, 12.4669, -15.041601, 9.18321, 9.600649, -17.456738, 5.615509, 11.105434, -18.574488, 7.6675305, 11.392937, -17.965525, 4.8956895, 9.600649, -17.456738, 5.615509, 8.624725, -17.931135, 7.6034517, 11.105434, -18.574488, 7.6675305, 11.392937, -17.965525, 4.8956895, 11.105434, -18.574488, 7.6675305, 13.632067, -18.083643, 7.819184, 11.392937, -17.965525, 4.8956895, 13.632067, -18.083643, 7.819184, 13.021395, -17.62377, 6.061926, 11.105434, -18.574488, 7.6675305, 12.696516, -17.604118, 9.563199, 13.632067, -18.083643, 7.819184, 11.105434, -18.574488, 7.6675305, 10.863857, -18.049467, 10.526733, 12.696516, -17.604118, 9.563199, 8.624725, -17.931135, 7.6034517, 10.863857, -18.049467, 10.526733, 11.105434, -18.574488, 7.6675305, 8.624725, -17.931135, 7.6034517, 9.266797, -17.581049, 9.7409115, 10.863857, -18.049467, 10.526733, 7.21563, -18.140032, 8.584928, 8.205864, -17.407396, 9.311369, 8.061045, -18.408522, 8.982858, 8.061045, -18.408522, 8.982858, 8.205864, -17.407396, 9.311369, 9.478685, -17.401842, 8.917924, 8.061045, -18.408522, 8.982858, 9.478685, -17.401842, 8.917924, 9.09955, -18.140032, 8.584928, 7.21563, -18.140032, 8.584928, 7.005238, -17.401842, 8.917924, 8.205864, -17.407396, 9.311369, 7.005238, -18.380968, 7.5489845, 7.005238, -17.401842, 8.917924, 7.21563, -18.140032, 8.584928, 8.205864, -17.407396, 9.311369, 9.169825, -16.65511, 8.638754, 9.478685, -17.401842, 8.917924, 7.005238, -18.380968, 7.5489845, 6.93411, -17.385181, 7.7371626, 7.005238, -17.401842, 8.917924, 7.595832, -18.105644, 6.610869, 6.93411, -17.385181, 7.7371626, 7.005238, -18.380968, 7.5489845, 7.005238, -17.401842, 8.917924, 8.423091, -16.394949, 8.852991, 8.205864, -17.407396, 9.311369, 8.205864, -17.407396, 9.311369, 8.423091, -16.394949, 8.852991, 9.169825, -16.65511, 8.638754, 6.93411, -17.385181, 7.7371626, 7.595832, -16.65511, 8.638754, 7.005238, -17.401842, 8.917924, 7.005238, -17.401842, 8.917924, 7.595832, -16.65511, 8.638754, 8.423091, -16.394949, 8.852991, 7.595832, -18.105644, 6.610869, 7.571055, -17.368736, 6.5566154, 6.93411, -17.385181, 7.7371626, 7.571055, -17.368736, 6.5566154, 7.595832, -18.105644, 6.610869, 8.423091, -18.353416, 6.1153255, 6.93411, -17.385181, 7.7371626, 7.571055, -16.389395, 7.925341, 7.595832, -16.65511, 8.638754, 7.571055, -17.368736, 6.5566154, 7.571055, -16.389395, 7.925341, 6.93411, -17.385181, 7.7371626, 7.595832, -16.65511, 8.638754, 8.495287, -16.05768, 7.988138, 8.423091, -16.394949, 8.852991, 7.595832, -16.65511, 8.638754, 7.571055, -16.389395, 7.925341, 8.495287, -16.05768, 7.988138, 7.571055, -17.368736, 6.5566154, 7.847235, -16.640371, 7.1145287, 7.571055, -16.389395, 7.925341, 7.571055, -17.368736, 6.5566154, 8.423091, -18.353416, 6.1153255, 8.495287, -17.363182, 6.1631703, 7.847235, -16.640371, 7.1145287, 7.571055, -17.368736, 6.5566154, 8.495287, -17.363182, 6.1631703, 8.495287, -17.363182, 6.1631703, 8.423091, -18.353416, 6.1153255, 9.169825, -18.105644, 6.610869, 7.571055, -16.389395, 7.925341, 7.847235, -16.640371, 7.1145287, 8.495287, -16.384056, 6.9979053, 7.847235, -16.640371, 7.1145287, 8.495287, -17.363182, 6.1631703, 8.495287, -16.384056, 6.9979053, 7.571055, -16.389395, 7.925341, 8.495287, -16.384056, 6.9979053, 8.495287, -16.05768, 7.988138, 8.495287, -17.363182, 6.1631703, 9.169825, -18.105644, 6.610869, 9.347322, -17.368736, 6.5566154, 8.495287, -16.384056, 6.9979053, 8.495287, -17.363182, 6.1631703, 9.347322, -17.368736, 6.5566154, 9.478685, -18.380968, 7.5489845, 9.347322, -17.368736, 6.5566154, 9.169825, -18.105644, 6.610869, 8.495287, -16.05768, 7.988138, 8.495287, -16.384056, 6.9979053, 9.1112995, -16.640371, 7.1145287, 8.495287, -16.384056, 6.9979053, 9.347322, -17.368736, 6.5566154, 9.1112995, -16.640371, 7.1145287, 9.478685, -18.380968, 7.5489845, 9.767254, -17.385181, 7.7371626, 9.347322, -17.368736, 6.5566154, 9.767254, -17.385181, 7.7371626, 9.1112995, -16.640371, 7.1145287, 9.347322, -17.368736, 6.5566154, 9.09955, -18.140032, 8.584928, 9.767254, -17.385181, 7.7371626, 9.478685, -18.380968, 7.5489845, 9.09955, -18.140032, 8.584928, 9.478685, -17.401842, 8.917924, 9.767254, -17.385181, 7.7371626, 9.767254, -17.385181, 7.7371626, 9.347322, -16.389395, 7.925341, 9.1112995, -16.640371, 7.1145287, 8.495287, -16.05768, 7.988138, 9.1112995, -16.640371, 7.1145287, 9.347322, -16.389395, 7.925341, 9.478685, -17.401842, 8.917924, 9.347322, -16.389395, 7.925341, 9.767254, -17.385181, 7.7371626, 8.423091, -16.394949, 8.852991, 8.495287, -16.05768, 7.988138, 9.347322, -16.389395, 7.925341, 9.478685, -17.401842, 8.917924, 9.169825, -16.65511, 8.638754, 9.347322, -16.389395, 7.925341, 8.423091, -16.394949, 8.852991, 9.347322, -16.389395, 7.925341, 9.169825, -16.65511, 8.638754, 7.595832, -18.105644, 6.610869, 8.205864, -18.712898, 7.486401, 8.423091, -18.353416, 6.1153255, 7.595832, -18.105644, 6.610869, 7.005238, -18.380968, 7.5489845, 8.205864, -18.712898, 7.486401, 8.423091, -18.353416, 6.1153255, 8.205864, -18.712898, 7.486401, 9.478685, -18.380968, 7.5489845, 8.423091, -18.353416, 6.1153255, 9.478685, -18.380968, 7.5489845, 9.169825, -18.105644, 6.610869, 8.205864, -18.712898, 7.486401, 9.09955, -18.140032, 8.584928, 9.478685, -18.380968, 7.5489845, 8.205864, -18.712898, 7.486401, 8.061045, -18.408522, 8.982858, 9.09955, -18.140032, 8.584928, 7.005238, -18.380968, 7.5489845, 8.061045, -18.408522, 8.982858, 8.205864, -18.712898, 7.486401, 7.005238, -18.380968, 7.5489845, 7.21563, -18.140032, 8.584928, 8.061045, -18.408522, 8.982858, 11.220137, -19.80587, 7.9881396, 9.957781, -19.472874, 7.7623677, 11.16268, -19.969059, 7.7499795, 11.220137, -19.80587, 7.9881396, 10.0568905, -19.342154, 7.988139, 9.957781, -19.472874, 7.7623677, 10.0568905, -19.342154, 7.988139, 8.76399, -18.20817, 7.922565, 9.957781, -19.472874, 7.7623677, 10.0568905, -19.342154, 7.988139, 8.811623, -18.20881, 7.9881387, 8.76399, -18.20817, 7.922565, 9.957781, -19.472874, 7.7623677, 8.76399, -18.20817, 7.922565, 8.686882, -18.207743, 7.9475555, 11.16268, -19.969059, 7.7499795, 9.957781, -19.472874, 7.7623677, 9.797157, -19.68412, 7.848661, 9.957781, -19.472874, 7.7623677, 8.686882, -18.207743, 7.9475555, 9.797157, -19.68412, 7.848661, 11.16268, -19.969059, 7.7499795, 9.797157, -19.68412, 7.848661, 11.069338, -20.23285, 7.8409715, 9.797157, -19.68412, 7.848661, 8.686882, -18.207743, 7.9475555, 8.686882, -18.207743, 8.028723, 11.069338, -20.23285, 7.8409715, 9.797157, -19.68412, 7.848661, 9.797157, -19.68412, 8.127618, 9.797157, -19.68412, 7.848661, 8.686882, -18.207743, 8.028723, 9.797157, -19.68412, 8.127618, 11.069338, -20.23285, 7.8409715, 9.797157, -19.68412, 8.127618, 11.069338, -20.23285, 8.135094, 9.797157, -19.68412, 8.127618, 8.686882, -18.207743, 8.028723, 8.76399, -18.20817, 8.053713, 11.069338, -20.23285, 8.135094, 9.797157, -19.68412, 8.127618, 9.957781, -19.472874, 8.213911, 9.797157, -19.68412, 8.127618, 8.76399, -18.20817, 8.053713, 9.957781, -19.472874, 8.213911, 9.957781, -19.472874, 8.213911, 8.76399, -18.20817, 8.053713, 8.811623, -18.20881, 7.9881387, 11.069338, -20.23285, 8.135094, 9.957781, -19.472874, 8.213911, 11.16268, -19.969059, 8.226086, 9.957781, -19.472874, 8.213911, 8.811623, -18.20881, 7.9881387, 10.0568905, -19.342154, 7.988139, 11.16268, -19.969059, 8.226086, 9.957781, -19.472874, 8.213911, 10.0568905, -19.342154, 7.988139, 11.16268, -19.969059, 8.226086, 10.0568905, -19.342154, 7.988139, 11.220137, -19.80587, 7.9881396, 10.050482, -22.030905, 7.98814, 11.049473, -19.97397, 8.213911, 10.477888, -22.030905, 8.612482, 10.050482, -22.030905, 7.98814, 10.888208, -19.944494, 7.9881396, 11.049473, -19.97397, 8.213911, 10.888208, -19.944494, 7.9881396, 11.13064, -17.907639, 8.087889, 11.049473, -19.97397, 8.213911, 10.888208, -19.944494, 7.9881396, 11.058659, -17.900164, 7.9881387, 11.13064, -17.907639, 8.087889, 11.049473, -19.97397, 8.213911, 11.13064, -17.907639, 8.087889, 11.247263, -17.92024, 8.049868, 10.477888, -22.030905, 8.612482, 11.049473, -19.97397, 8.213911, 11.310488, -20.021389, 8.127618, 11.049473, -19.97397, 8.213911, 11.247263, -17.92024, 8.049868, 11.310488, -20.021389, 8.127618, 10.477888, -22.030905, 8.612482, 11.310488, -20.021389, 8.127618, 11.169514, -22.031118, 8.374109, 11.310488, -20.021389, 8.127618, 11.247263, -17.92024, 8.049868, 11.247263, -17.92024, 7.9264092, 11.169514, -22.031118, 8.374109, 11.310488, -20.021389, 8.127618, 11.310488, -20.021389, 7.848661, 11.310488, -20.021389, 8.127618, 11.247263, -17.92024, 7.9264092, 11.310488, -20.021389, 7.848661, 11.169514, -22.031118, 8.374109, 11.310488, -20.021389, 7.848661, 11.169514, -22.031118, 7.6021714, 11.310488, -20.021389, 7.848661, 11.247263, -17.92024, 7.9264092, 11.13064, -17.907639, 7.888389, 11.169514, -22.031118, 7.6021714, 11.310488, -20.021389, 7.848661, 11.049473, -19.97397, 7.762368, 11.310488, -20.021389, 7.848661, 11.13064, -17.907639, 7.888389, 11.049473, -19.97397, 7.762368, 11.049473, -19.97397, 7.762368, 11.13064, -17.907639, 7.888389, 11.058659, -17.900164, 7.9881387, 11.169514, -22.031118, 7.6021714, 11.049473, -19.97397, 7.762368, 10.477888, -22.030905, 7.3637977, 11.049473, -19.97397, 7.762368, 11.058659, -17.900164, 7.9881387, 10.888208, -19.944494, 7.9881396, 10.477888, -22.030905, 7.3637977, 11.049473, -19.97397, 7.762368, 10.888208, -19.944494, 7.9881396, 10.477888, -22.030905, 7.3637977, 10.888208, -19.944494, 7.9881396, 10.050482, -22.030905, 7.98814, 11.023628, -19.860125, 7.836486, 10.902519, -19.637772, 6.988935, 11.16268, -19.969059, 7.7499795, 11.023628, -19.860125, 7.836486, 10.7801285, -19.55041, 7.099151, 10.902519, -19.637772, 6.988935, 10.7801285, -19.55041, 7.099151, 10.55094, -18.793425, 6.265697, 10.902519, -19.637772, 6.988935, 10.7801285, -19.55041, 7.099151, 10.519327, -18.793852, 6.309698, 10.55094, -18.793425, 6.265697, 10.902519, -19.637772, 6.988935, 10.55094, -18.793425, 6.265697, 10.5189, -18.792997, 6.2221236, 11.16268, -19.969059, 7.7499795, 10.902519, -19.637772, 6.988935, 10.814302, -19.778744, 6.9052052, 10.902519, -19.637772, 6.988935, 10.5189, -18.792997, 6.2221236, 10.814302, -19.778744, 6.9052052, 11.16268, -19.969059, 7.7499795, 10.814302, -19.778744, 6.9052052, 11.085358, -20.145061, 7.710037, 10.814302, -19.778744, 6.9052052, 10.5189, -18.792997, 6.2221236, 10.4674225, -18.792997, 6.238998, 11.085358, -20.145061, 7.710037, 10.814302, -19.778744, 6.9052052, 10.637232, -19.778744, 6.963517, 10.814302, -19.778744, 6.9052052, 10.4674225, -18.792997, 6.238998, 10.637232, -19.778744, 6.963517, 11.085358, -20.145061, 7.710037, 10.637232, -19.778744, 6.963517, 10.898888, -20.145061, 7.771766, 10.637232, -19.778744, 6.963517, 10.4674225, -18.792997, 6.238998, 10.46785, -18.793425, 6.293038, 10.898888, -20.145061, 7.771766, 10.637232, -19.778744, 6.963517, 10.616299, -19.637772, 7.083345, 10.637232, -19.778744, 6.963517, 10.46785, -18.793425, 6.293038, 10.616299, -19.637772, 7.083345, 10.616299, -19.637772, 7.083345, 10.46785, -18.793425, 6.293038, 10.519327, -18.793852, 6.309698, 10.898888, -20.145061, 7.771766, 10.616299, -19.637772, 7.083345, 10.860653, -19.969059, 7.849729, 10.616299, -19.637772, 7.083345, 10.519327, -18.793852, 6.309698, 10.7801285, -19.55041, 7.099151, 10.860653, -19.969059, 7.849729, 10.616299, -19.637772, 7.083345, 10.7801285, -19.55041, 7.099151, 10.860653, -19.969059, 7.849729, 10.7801285, -19.55041, 7.099151, 11.023628, -19.860125, 7.836486, 9.806982, -18.747929, 5.422632, 9.553658, -18.258793, 6.2026863, 9.731583, -18.927135, 6.042062, 9.731583, -18.927135, 6.042062, 9.553658, -18.258793, 6.2026863, 10.069706, -18.254948, 6.927205, 9.731583, -18.927135, 6.042062, 10.069706, -18.254948, 6.927205, 10.201282, -18.747929, 6.6170635, 9.806982, -18.747929, 5.422632, 9.551735, -18.254948, 5.359194, 9.553658, -18.258793, 6.2026863, 10.419577, -18.908766, 5.0725474, 9.551735, -18.254948, 5.359194, 9.806982, -18.747929, 5.422632, 9.553658, -18.258793, 6.2026863, 10.181845, -17.756413, 6.672812, 10.069706, -18.254948, 6.927205, 10.419577, -18.908766, 5.0725474, 10.285225, -18.244055, 5.06678, 9.551735, -18.254948, 5.359194, 11.137901, -18.725073, 5.250473, 10.285225, -18.244055, 5.06678, 10.419577, -18.908766, 5.0725474, 9.551735, -18.254948, 5.359194, 9.889858, -17.582973, 6.244337, 9.553658, -18.258793, 6.2026863, 9.553658, -18.258793, 6.2026863, 9.889858, -17.582973, 6.244337, 10.181845, -17.756413, 6.672812, 10.285225, -18.244055, 5.06678, 9.852264, -17.756414, 5.6751027, 9.551735, -18.254948, 5.359194, 9.551735, -18.254948, 5.359194, 9.852264, -17.756414, 5.6751027, 9.889858, -17.582973, 6.244337, 11.137901, -18.725073, 5.250473, 11.16695, -18.232948, 5.223346, 10.285225, -18.244055, 5.06678, 11.16695, -18.232948, 5.223346, 11.137901, -18.725073, 5.250473, 11.625116, -18.890398, 5.671045, 10.285225, -18.244055, 5.06678, 10.299322, -17.579128, 5.5099926, 9.852264, -17.756414, 5.6751027, 11.16695, -18.232948, 5.223346, 10.299322, -17.579128, 5.5099926, 10.285225, -18.244055, 5.06678, 9.852264, -17.756414, 5.6751027, 10.453112, -17.357628, 6.1091304, 9.889858, -17.582973, 6.244337, 9.852264, -17.756414, 5.6751027, 10.299322, -17.579128, 5.5099926, 10.453112, -17.357628, 6.1091304, 11.16695, -18.232948, 5.223346, 10.871334, -17.746588, 5.5153327, 10.299322, -17.579128, 5.5099926, 11.16695, -18.232948, 5.223346, 11.625116, -18.890398, 5.671045, 11.610165, -18.229317, 5.727007, 10.871334, -17.746588, 5.5153327, 11.16695, -18.232948, 5.223346, 11.610165, -18.229317, 5.727007, 11.610165, -18.229317, 5.727007, 11.625116, -18.890398, 5.671045, 11.467482, -18.725073, 6.2481823, 10.299322, -17.579128, 5.5099926, 10.871334, -17.746588, 5.5153327, 11.080872, -17.575497, 5.9017286, 10.871334, -17.746588, 5.5153327, 11.610165, -18.229317, 5.727007, 11.080872, -17.575497, 5.9017286, 10.299322, -17.579128, 5.5099926, 11.080872, -17.575497, 5.9017286, 10.453112, -17.357628, 6.1091304, 11.610165, -18.229317, 5.727007, 11.467482, -18.725073, 6.2481823, 11.539036, -18.232946, 6.3494267, 11.080872, -17.575497, 5.9017286, 11.610165, -18.229317, 5.727007, 11.539036, -18.232946, 6.3494267, 10.937548, -18.908766, 6.640559, 11.539036, -18.232946, 6.3494267, 11.467482, -18.725073, 6.2481823, 10.453112, -17.357628, 6.1091304, 11.080872, -17.575497, 5.9017286, 11.135979, -17.746588, 6.316746, 11.080872, -17.575497, 5.9017286, 11.539036, -18.232946, 6.3494267, 11.135979, -17.746588, 6.316746, 10.937548, -18.908766, 6.640559, 10.878382, -18.244053, 6.8629127, 11.539036, -18.232946, 6.3494267, 10.878382, -18.244053, 6.8629127, 11.135979, -17.746588, 6.316746, 11.539036, -18.232946, 6.3494267, 10.201282, -18.747929, 6.6170635, 10.878382, -18.244053, 6.8629127, 10.937548, -18.908766, 6.640559, 10.201282, -18.747929, 6.6170635, 10.069706, -18.254948, 6.927205, 10.878382, -18.244053, 6.8629127, 10.878382, -18.244053, 6.8629127, 10.671408, -17.579128, 6.636073, 11.135979, -17.746588, 6.316746, 10.453112, -17.357628, 6.1091304, 11.135979, -17.746588, 6.316746, 10.671408, -17.579128, 6.636073, 10.069706, -18.254948, 6.927205, 10.671408, -17.579128, 6.636073, 10.878382, -18.244053, 6.8629127, 9.889858, -17.582973, 6.244337, 10.453112, -17.357628, 6.1091304, 10.671408, -17.579128, 6.636073, 10.069706, -18.254948, 6.927205, 10.181845, -17.756413, 6.672812, 10.671408, -17.579128, 6.636073, 9.889858, -17.582973, 6.244337, 10.671408, -17.579128, 6.636073, 10.181845, -17.756413, 6.672812, 11.137901, -18.725073, 5.250473, 10.710709, -19.13048, 5.8203487, 11.625116, -18.890398, 5.671045, 11.137901, -18.725073, 5.250473, 10.419577, -18.908766, 5.0725474, 10.710709, -19.13048, 5.8203487, 11.625116, -18.890398, 5.671045, 10.710709, -19.13048, 5.8203487, 10.937548, -18.908766, 6.640559, 11.625116, -18.890398, 5.671045, 10.937548, -18.908766, 6.640559, 11.467482, -18.725073, 6.2481823, 10.710709, -19.13048, 5.8203487, 10.201282, -18.747929, 6.6170635, 10.937548, -18.908766, 6.640559, 10.710709, -19.13048, 5.8203487, 9.731583, -18.927135, 6.042062, 10.201282, -18.747929, 6.6170635, 10.419577, -18.908766, 5.0725474, 9.731583, -18.927135, 6.042062, 10.710709, -19.13048, 5.8203487, 10.419577, -18.908766, 5.0725474, 9.806982, -18.747929, 5.422632, 9.731583, -18.927135, 6.042062, 15.540274, 10.966449, -3.3071837, 18.560066, 13.1471405, -1.460484, 18.346163, 10.143484, -1.9265671, 18.346163, 10.143484, -1.9265671, 18.560066, 13.1471405, -1.460484, 22.219316, 13.223694, -3.2587752, 18.346163, 10.143484, -1.9265671, 22.219316, 13.223694, -3.2587752, 21.565973, 10.92592, -3.6194077, 15.540274, 10.966449, -3.3071837, 15.086948, 13.100231, -2.2530527, 18.560066, 13.1471405, -1.460484, 14.412214, 10.35138, -7.0625033, 15.086948, 13.100231, -2.2530527, 15.540274, 10.966449, -3.3071837, 18.560066, 13.1471405, -1.460484, 21.16256, 15.42765, -4.2870145, 22.219316, 13.223694, -3.2587752, 14.412214, 10.35138, -7.0625033, 14.188929, 13.313381, -6.7934375, 15.086948, 13.100231, -2.2530527, 16.12682, 11.184852, -10.555136, 14.188929, 13.313381, -6.7934375, 14.412214, 10.35138, -7.0625033, 15.086948, 13.100231, -2.2530527, 18.959728, 16.180443, -3.5852609, 18.560066, 13.1471405, -1.460484, 18.560066, 13.1471405, -1.460484, 18.959728, 16.180443, -3.5852609, 21.16256, 15.42765, -4.2870145, 14.188929, 13.313381, -6.7934375, 16.632683, 15.373238, -3.8400683, 15.086948, 13.100231, -2.2530527, 15.086948, 13.100231, -2.2530527, 16.632683, 15.373238, -3.8400683, 18.959728, 16.180443, -3.5852609, 16.12682, 11.184852, -10.555136, 15.662611, 13.473244, -11.258017, 14.188929, 13.313381, -6.7934375, 15.662611, 13.473244, -11.258017, 16.12682, 11.184852, -10.555136, 19.275705, 10.291335, -11.819794, 14.188929, 13.313381, -6.7934375, 16.33697, 16.22172, -6.448566, 16.632683, 15.373238, -3.8400683, 15.662611, 13.473244, -11.258017, 16.33697, 16.22172, -6.448566, 14.188929, 13.313381, -6.7934375, 16.632683, 15.373238, -3.8400683, 19.092949, 17.191416, -6.333734, 18.959728, 16.180443, -3.5852609, 16.632683, 15.373238, -3.8400683, 16.33697, 16.22172, -6.448566, 19.092949, 17.191416, -6.333734, 15.662611, 13.473244, -11.258017, 16.88824, 15.538728, -9.202293, 16.33697, 16.22172, -6.448566, 15.662611, 13.473244, -11.258017, 19.275705, 10.291335, -11.819794, 19.30385, 13.265345, -11.823172, 16.88824, 15.538728, -9.202293, 15.662611, 13.473244, -11.258017, 19.30385, 13.265345, -11.823172, 19.30385, 13.265345, -11.823172, 19.275705, 10.291335, -11.819794, 22.136759, 10.891392, -9.770823, 16.33697, 16.22172, -6.448566, 16.88824, 15.538728, -9.202293, 19.145487, 16.20971, -9.236067, 16.88824, 15.538728, -9.202293, 19.30385, 13.265345, -11.823172, 19.145487, 16.20971, -9.236067, 16.33697, 16.22172, -6.448566, 19.145487, 16.20971, -9.236067, 19.092949, 17.191416, -6.333734, 19.30385, 13.265345, -11.823172, 22.136759, 10.891392, -9.770823, 22.758955, 13.0278, -9.797468, 19.145487, 16.20971, -9.236067, 19.30385, 13.265345, -11.823172, 22.758955, 13.0278, -9.797468, 23.209654, 10.083063, -6.6834817, 22.758955, 13.0278, -9.797468, 22.136759, 10.891392, -9.770823, 19.092949, 17.191416, -6.333734, 19.145487, 16.20971, -9.236067, 21.402355, 15.340961, -8.553076, 19.145487, 16.20971, -9.236067, 22.758955, 13.0278, -9.797468, 21.402355, 15.340961, -8.553076, 23.209654, 10.083063, -6.6834817, 23.674988, 13.098728, -6.4902196, 22.758955, 13.0278, -9.797468, 23.674988, 13.098728, -6.4902196, 21.402355, 15.340961, -8.553076, 22.758955, 13.0278, -9.797468, 21.565973, 10.92592, -3.6194077, 23.674988, 13.098728, -6.4902196, 23.209654, 10.083063, -6.6834817, 21.565973, 10.92592, -3.6194077, 22.219316, 13.223694, -3.2587752, 23.674988, 13.098728, -6.4902196, 23.674988, 13.098728, -6.4902196, 21.768618, 16.168056, -6.372762, 21.402355, 15.340961, -8.553076, 19.092949, 17.191416, -6.333734, 21.402355, 15.340961, -8.553076, 21.768618, 16.168056, -6.372762, 22.219316, 13.223694, -3.2587752, 21.768618, 16.168056, -6.372762, 23.674988, 13.098728, -6.4902196, 18.959728, 16.180443, -3.5852609, 19.092949, 17.191416, -6.333734, 21.768618, 16.168056, -6.372762, 22.219316, 13.223694, -3.2587752, 21.16256, 15.42765, -4.2870145, 21.768618, 16.168056, -6.372762, 18.959728, 16.180443, -3.5852609, 21.768618, 16.168056, -6.372762, 21.16256, 15.42765, -4.2870145, 16.12682, 11.184852, -10.555136, 18.770592, 9.221069, -6.949922, 19.275705, 10.291335, -11.819794, 16.12682, 11.184852, -10.555136, 14.412214, 10.35138, -7.0625033, 18.770592, 9.221069, -6.949922, 19.275705, 10.291335, -11.819794, 18.770592, 9.221069, -6.949922, 23.209654, 10.083063, -6.6834817, 19.275705, 10.291335, -11.819794, 23.209654, 10.083063, -6.6834817, 22.136759, 10.891392, -9.770823, 18.770592, 9.221069, -6.949922, 21.565973, 10.92592, -3.6194077, 23.209654, 10.083063, -6.6834817, 18.770592, 9.221069, -6.949922, 18.346163, 10.143484, -1.9265671, 21.565973, 10.92592, -3.6194077, 14.412214, 10.35138, -7.0625033, 18.346163, 10.143484, -1.9265671, 18.770592, 9.221069, -6.949922, 14.412214, 10.35138, -7.0625033, 15.540274, 10.966449, -3.3071837, 18.346163, 10.143484, -1.9265671, 11.936562, 9.984368, -5.338141, 13.67631, 11.271917, -4.06185, 13.421877, 9.512654, -4.639014, 13.421877, 9.512654, -4.639014, 13.67631, 11.271917, -4.06185, 15.91254, 11.2813, -4.7530966, 13.421877, 9.512654, -4.639014, 15.91254, 11.2813, -4.7530966, 15.246437, 9.984368, -5.338141, 11.936562, 9.984368, -5.338141, 11.566921, 11.2813, -4.7530966, 13.67631, 11.271917, -4.06185, 11.566921, 9.561063, -7.158197, 11.566921, 11.2813, -4.7530966, 11.936562, 9.984368, -5.338141, 13.67631, 11.271917, -4.06185, 15.369902, 12.5936165, -5.243574, 15.91254, 11.2813, -4.7530966, 11.566921, 9.561063, -7.158197, 11.441956, 11.310569, -6.8275857, 11.566921, 11.2813, -4.7530966, 12.604542, 10.044409, -8.806381, 11.441956, 11.310569, -6.8275857, 11.566921, 9.561063, -7.158197, 11.566921, 11.2813, -4.7530966, 14.057959, 13.050319, -4.8671794, 13.67631, 11.271917, -4.06185, 13.67631, 11.271917, -4.06185, 14.057959, 13.050319, -4.8671794, 15.369902, 12.5936165, -5.243574, 11.441956, 11.310569, -6.8275857, 12.604542, 12.5936165, -5.243574, 11.566921, 11.2813, -4.7530966, 11.566921, 11.2813, -4.7530966, 12.604542, 12.5936165, -5.243574, 14.057959, 13.050319, -4.8671794, 12.604542, 10.044409, -8.806381, 12.56101, 11.339464, -8.901699, 11.441956, 11.310569, -6.8275857, 12.56101, 11.339464, -8.901699, 12.604542, 10.044409, -8.806381, 14.057959, 9.609097, -9.677006, 11.441956, 11.310569, -6.8275857, 12.56101, 13.0597, -6.4969745, 12.604542, 12.5936165, -5.243574, 12.56101, 11.339464, -8.901699, 12.56101, 13.0597, -6.4969745, 11.441956, 11.310569, -6.8275857, 12.604542, 12.5936165, -5.243574, 14.184801, 13.643244, -6.386646, 14.057959, 13.050319, -4.8671794, 12.604542, 12.5936165, -5.243574, 12.56101, 13.0597, -6.4969745, 14.184801, 13.643244, -6.386646, 12.56101, 11.339464, -8.901699, 13.046232, 12.619133, -7.921497, 12.56101, 13.0597, -6.4969745, 12.56101, 11.339464, -8.901699, 14.057959, 9.609097, -9.677006, 14.184801, 11.34922, -9.592945, 13.046232, 12.619133, -7.921497, 12.56101, 11.339464, -8.901699, 14.184801, 11.34922, -9.592945, 14.184801, 11.34922, -9.592945, 14.057959, 9.609097, -9.677006, 15.369902, 10.044409, -8.806381, 12.56101, 13.0597, -6.4969745, 13.046232, 12.619133, -7.921497, 14.184801, 13.069456, -8.126394, 13.046232, 12.619133, -7.921497, 14.184801, 11.34922, -9.592945, 14.184801, 13.069456, -8.126394, 12.56101, 13.0597, -6.4969745, 14.184801, 13.069456, -8.126394, 14.184801, 13.643244, -6.386646, 14.184801, 11.34922, -9.592945, 15.369902, 10.044409, -8.806381, 15.681749, 11.339464, -8.901699, 14.184801, 13.069456, -8.126394, 14.184801, 11.34922, -9.592945, 15.681749, 11.339464, -8.901699, 15.91254, 9.561063, -7.158197, 15.681749, 11.339464, -8.901699, 15.369902, 10.044409, -8.806381, 14.184801, 13.643244, -6.386646, 14.184801, 13.069456, -8.126394, 15.2670765, 12.619133, -7.921497, 14.184801, 13.069456, -8.126394, 15.681749, 11.339464, -8.901699, 15.2670765, 12.619133, -7.921497, 15.91254, 9.561063, -7.158197, 16.419529, 11.310569, -6.8275857, 15.681749, 11.339464, -8.901699, 16.419529, 11.310569, -6.8275857, 15.2670765, 12.619133, -7.921497, 15.681749, 11.339464, -8.901699, 15.246437, 9.984368, -5.338141, 16.419529, 11.310569, -6.8275857, 15.91254, 9.561063, -7.158197, 15.246437, 9.984368, -5.338141, 15.91254, 11.2813, -4.7530966, 16.419529, 11.310569, -6.8275857, 16.419529, 11.310569, -6.8275857, 15.681749, 13.0597, -6.4969745, 15.2670765, 12.619133, -7.921497, 14.184801, 13.643244, -6.386646, 15.2670765, 12.619133, -7.921497, 15.681749, 13.0597, -6.4969745, 15.91254, 11.2813, -4.7530966, 15.681749, 13.0597, -6.4969745, 16.419529, 11.310569, -6.8275857, 14.057959, 13.050319, -4.8671794, 14.184801, 13.643244, -6.386646, 15.681749, 13.0597, -6.4969745, 15.91254, 11.2813, -4.7530966, 15.369902, 12.5936165, -5.243574, 15.681749, 13.0597, -6.4969745, 14.057959, 13.050319, -4.8671794, 15.681749, 13.0597, -6.4969745, 15.369902, 12.5936165, -5.243574, 12.604542, 10.044409, -8.806381, 13.67631, 8.977895, -7.268151, 14.057959, 9.609097, -9.677006, 12.604542, 10.044409, -8.806381, 11.566921, 9.561063, -7.158197, 13.67631, 8.977895, -7.268151, 14.057959, 9.609097, -9.677006, 13.67631, 8.977895, -7.268151, 15.91254, 9.561063, -7.158197, 14.057959, 9.609097, -9.677006, 15.91254, 9.561063, -7.158197, 15.369902, 10.044409, -8.806381, 13.67631, 8.977895, -7.268151, 15.246437, 9.984368, -5.338141, 15.91254, 9.561063, -7.158197, 13.67631, 8.977895, -7.268151, 13.421877, 9.512654, -4.639014, 15.246437, 9.984368, -5.338141, 11.566921, 9.561063, -7.158197, 13.421877, 9.512654, -4.639014, 13.67631, 8.977895, -7.268151, 11.566921, 9.561063, -7.158197, 11.936562, 9.984368, -5.338141, 13.421877, 9.512654, -4.639014, 16.48933, 8.916348, -10.894003, 16.04426, 9.775716, -9.5235195, 16.35686, 8.601498, -9.805721, 16.35686, 8.601498, -9.805721, 16.04426, 9.775716, -9.5235195, 16.95091, 9.782472, -8.250607, 16.35686, 8.601498, -9.805721, 16.95091, 9.782472, -8.250607, 17.182077, 8.916349, -8.795498, 16.48933, 8.916348, -10.894003, 16.040882, 9.78247, -11.005459, 16.04426, 9.775716, -9.5235195, 17.565601, 8.63377, -11.509069, 16.040882, 9.78247, -11.005459, 16.48933, 8.916348, -10.894003, 16.04426, 9.775716, -9.5235195, 17.147928, 10.65835, -8.697552, 16.95091, 9.782472, -8.250607, 17.565601, 8.63377, -11.509069, 17.329557, 9.801608, -11.519203, 16.040882, 9.78247, -11.005459, 18.827633, 8.956502, -11.19647, 17.329557, 9.801608, -11.519203, 17.565601, 8.63377, -11.509069, 16.040882, 9.78247, -11.005459, 16.634933, 10.963444, -9.450344, 16.04426, 9.775716, -9.5235195, 16.04426, 9.775716, -9.5235195, 16.634933, 10.963444, -9.450344, 17.147928, 10.65835, -8.697552, 17.329557, 9.801608, -11.519203, 16.568888, 10.658349, -10.450436, 16.040882, 9.78247, -11.005459, 16.040882, 9.78247, -11.005459, 16.568888, 10.658349, -10.450436, 16.634933, 10.963444, -9.450344, 18.827633, 8.956502, -11.19647, 18.87867, 9.821122, -11.244131, 17.329557, 9.801608, -11.519203, 18.87867, 9.821122, -11.244131, 18.827633, 8.956502, -11.19647, 19.683622, 8.666044, -10.457565, 17.329557, 9.801608, -11.519203, 17.354326, 10.969822, -10.740519, 16.568888, 10.658349, -10.450436, 18.87867, 9.821122, -11.244131, 17.354326, 10.969822, -10.740519, 17.329557, 9.801608, -11.519203, 16.568888, 10.658349, -10.450436, 17.62452, 11.358977, -9.687889, 16.634933, 10.963444, -9.450344, 16.568888, 10.658349, -10.450436, 17.354326, 10.969822, -10.740519, 17.62452, 11.358977, -9.687889, 18.87867, 9.821122, -11.244131, 18.359297, 10.6756115, -10.731136, 17.354326, 10.969822, -10.740519, 18.87867, 9.821122, -11.244131, 19.683622, 8.666044, -10.457565, 19.657354, 9.827502, -10.359245, 18.359297, 10.6756115, -10.731136, 18.87867, 9.821122, -11.244131, 19.657354, 9.827502, -10.359245, 19.657354, 9.827502, -10.359245, 19.683622, 8.666044, -10.457565, 19.406673, 8.956503, -9.443586, 17.354326, 10.969822, -10.740519, 18.359297, 10.6756115, -10.731136, 18.727436, 10.976202, -10.052275, 18.359297, 10.6756115, -10.731136, 19.657354, 9.827502, -10.359245, 18.727436, 10.976202, -10.052275, 17.354326, 10.969822, -10.740519, 18.727436, 10.976202, -10.052275, 17.62452, 11.358977, -9.687889, 19.657354, 9.827502, -10.359245, 19.406673, 8.956503, -9.443586, 19.532389, 9.821123, -9.265711, 18.727436, 10.976202, -10.052275, 19.657354, 9.827502, -10.359245, 19.532389, 9.821123, -9.265711, 18.47563, 8.633772, -8.754217, 19.532389, 9.821123, -9.265711, 19.406673, 8.956503, -9.443586, 17.62452, 11.358977, -9.687889, 18.727436, 10.976202, -10.052275, 18.824255, 10.6756115, -9.323126, 18.727436, 10.976202, -10.052275, 19.532389, 9.821123, -9.265711, 18.824255, 10.6756115, -9.323126, 18.47563, 8.633772, -8.754217, 18.371681, 9.80161, -8.363562, 19.532389, 9.821123, -9.265711, 18.371681, 9.80161, -8.363562, 18.824255, 10.6756115, -9.323126, 19.532389, 9.821123, -9.265711, 17.182077, 8.916349, -8.795498, 18.371681, 9.80161, -8.363562, 18.47563, 8.633772, -8.754217, 17.182077, 8.916349, -8.795498, 16.95091, 9.782472, -8.250607, 18.371681, 9.80161, -8.363562, 18.371681, 9.80161, -8.363562, 18.008045, 10.969824, -8.762098, 18.824255, 10.6756115, -9.323126, 17.62452, 11.358977, -9.687889, 18.824255, 10.6756115, -9.323126, 18.008045, 10.969824, -8.762098, 16.95091, 9.782472, -8.250607, 18.008045, 10.969824, -8.762098, 18.371681, 9.80161, -8.363562, 16.634933, 10.963444, -9.450344, 17.62452, 11.358977, -9.687889, 18.008045, 10.969824, -8.762098, 16.95091, 9.782472, -8.250607, 17.147928, 10.65835, -8.697552, 18.008045, 10.969824, -8.762098, 16.634933, 10.963444, -9.450344, 18.008045, 10.969824, -8.762098, 17.147928, 10.65835, -8.697552, 18.827633, 8.956502, -11.19647, 18.077095, 8.244242, -10.195251, 19.683622, 8.666044, -10.457565, 18.827633, 8.956502, -11.19647, 17.565601, 8.63377, -11.509069, 18.077095, 8.244242, -10.195251, 19.683622, 8.666044, -10.457565, 18.077095, 8.244242, -10.195251, 18.47563, 8.633772, -8.754217, 19.683622, 8.666044, -10.457565, 18.47563, 8.633772, -8.754217, 19.406673, 8.956503, -9.443586, 18.077095, 8.244242, -10.195251, 17.182077, 8.916349, -8.795498, 18.47563, 8.633772, -8.754217, 18.077095, 8.244242, -10.195251, 16.35686, 8.601498, -9.805721, 17.182077, 8.916349, -8.795498, 17.565601, 8.63377, -11.509069, 16.35686, 8.601498, -9.805721, 18.077095, 8.244242, -10.195251, 17.565601, 8.63377, -11.509069, 16.48933, 8.916348, -10.894003, 16.35686, 8.601498, -9.805721, 18.97211, 7.0576415, -6.386642, 16.75427, 7.642686, -6.783302, 18.871164, 6.7709355, -6.804692, 18.97211, 7.0576415, -6.386642, 16.92877, 7.871976, -6.386642, 16.75427, 7.642686, -6.783302, 16.92877, 7.871976, -6.386642, 14.656889, 9.864281, -6.5018516, 16.75427, 7.642686, -6.783302, 16.92877, 7.871976, -6.386642, 14.740574, 9.86353, -6.386643, 14.656889, 9.864281, -6.5018516, 16.75427, 7.642686, -6.783302, 14.656889, 9.864281, -6.5018516, 14.521418, 9.865407, -6.4579444, 18.871164, 6.7709355, -6.804692, 16.75427, 7.642686, -6.783302, 16.472067, 7.2715445, -6.6316934, 16.75427, 7.642686, -6.783302, 14.521418, 9.865407, -6.4579444, 16.472067, 7.2715445, -6.6316934, 18.871164, 6.7709355, -6.804692, 16.472067, 7.2715445, -6.6316934, 18.70717, 6.307478, -6.645202, 16.472067, 7.2715445, -6.6316934, 14.521418, 9.865407, -6.4579444, 14.521418, 9.865407, -6.3153424, 18.70717, 6.307478, -6.645202, 16.472067, 7.2715445, -6.6316934, 16.472067, 7.271545, -6.1415906, 16.472067, 7.2715445, -6.6316934, 14.521418, 9.865407, -6.3153424, 16.472067, 7.271545, -6.1415906, 18.70717, 6.307478, -6.645202, 16.472067, 7.271545, -6.1415906, 18.70717, 6.307478, -6.128081, 16.472067, 7.271545, -6.1415906, 14.521418, 9.865407, -6.3153424, 14.656889, 9.864282, -6.2714353, 18.70717, 6.307478, -6.128081, 16.472067, 7.271545, -6.1415906, 16.75427, 7.642687, -5.9899826, 16.472067, 7.271545, -6.1415906, 14.656889, 9.864282, -6.2714353, 16.75427, 7.642687, -5.9899826, 16.75427, 7.642687, -5.9899826, 14.656889, 9.864282, -6.2714353, 14.740574, 9.86353, -6.386643, 18.70717, 6.307478, -6.128081, 16.75427, 7.642687, -5.9899826, 18.871164, 6.770936, -5.9685917, 16.75427, 7.642687, -5.9899826, 14.740574, 9.86353, -6.386643, 16.92877, 7.871976, -6.386642, 18.871164, 6.770936, -5.9685917, 16.75427, 7.642687, -5.9899826, 16.92877, 7.871976, -6.386642, 18.871164, 6.770936, -5.9685917, 16.92877, 7.871976, -6.386642, 18.97211, 7.0576415, -6.386642, 16.917137, 3.1484604, -6.38664, 18.672272, 6.762305, -5.989982, 17.66805, 3.1484613, -5.289727, 16.917137, 3.1484604, -6.38664, 18.388943, 6.8137164, -6.386642, 18.672272, 6.762305, -5.989982, 18.388943, 6.8137164, -6.386642, 18.814873, 10.392285, -6.211393, 18.672272, 6.762305, -5.989982, 18.388943, 6.8137164, -6.386642, 18.688408, 10.405795, -6.386644, 18.814873, 10.392285, -6.211393, 18.672272, 6.762305, -5.989982, 18.814873, 10.392285, -6.211393, 19.01977, 10.370894, -6.278191, 17.66805, 3.1484613, -5.289727, 18.672272, 6.762305, -5.989982, 19.13085, 6.6789947, -6.1415906, 18.672272, 6.762305, -5.989982, 19.01977, 10.370894, -6.278191, 19.13085, 6.6789947, -6.1415906, 17.66805, 3.1484613, -5.289727, 19.13085, 6.6789947, -6.1415906, 18.883549, 3.148085, -5.708528, 19.13085, 6.6789947, -6.1415906, 19.01977, 10.370894, -6.278191, 19.01977, 10.370894, -6.4950967, 18.883549, 3.148085, -5.708528, 19.13085, 6.6789947, -6.1415906, 19.13085, 6.678994, -6.6316924, 19.13085, 6.6789947, -6.1415906, 19.01977, 10.370894, -6.4950967, 19.13085, 6.678994, -6.6316924, 18.883549, 3.148085, -5.708528, 19.13085, 6.678994, -6.6316924, 18.883549, 3.1480846, -7.064751, 19.13085, 6.678994, -6.6316924, 19.01977, 10.370894, -6.4950967, 18.814873, 10.392285, -6.561895, 18.883549, 3.1480846, -7.064751, 19.13085, 6.678994, -6.6316924, 18.672272, 6.7623043, -6.783302, 19.13085, 6.678994, -6.6316924, 18.814873, 10.392285, -6.561895, 18.672272, 6.7623043, -6.783302, 18.672272, 6.7623043, -6.783302, 18.814873, 10.392285, -6.561895, 18.688408, 10.405795, -6.386644, 18.883549, 3.1480846, -7.064751, 18.672272, 6.7623043, -6.783302, 17.66805, 3.14846, -7.4835525, 18.672272, 6.7623043, -6.783302, 18.688408, 10.405795, -6.386644, 18.388943, 6.8137164, -6.386642, 17.66805, 3.14846, -7.4835525, 18.672272, 6.7623043, -6.783302, 18.388943, 6.8137164, -6.386642, 17.66805, 3.14846, -7.4835525, 18.388943, 6.8137164, -6.386642, 16.917137, 3.1484604, -6.38664, 18.62724, 6.962323, -6.6530833, 18.414085, 7.3529778, -8.142153, 18.871164, 6.7709355, -6.804692, 18.62724, 6.962323, -6.6530833, 18.199057, 7.5060873, -7.9485135, 18.414085, 7.3529778, -8.142153, 18.199057, 7.5060873, -7.9485135, 17.796392, 8.836041, -9.412815, 18.414085, 7.3529778, -8.142153, 18.199057, 7.5060873, -7.9485135, 17.740852, 8.835667, -9.33551, 17.796392, 8.836041, -9.412815, 18.414085, 7.3529778, -8.142153, 17.796392, 8.836041, -9.412815, 17.740103, 8.836792, -9.48937, 18.871164, 6.7709355, -6.804692, 18.414085, 7.3529778, -8.142153, 18.258724, 7.1053, -8.289257, 18.414085, 7.3529778, -8.142153, 17.740103, 8.836792, -9.48937, 18.258724, 7.1053, -8.289257, 18.871164, 6.7709355, -6.804692, 18.258724, 7.1053, -8.289257, 18.735317, 6.461338, -6.8752427, 18.258724, 7.1053, -8.289257, 17.740103, 8.836792, -9.48937, 17.650038, 8.836792, -9.459724, 18.735317, 6.461338, -6.8752427, 18.258724, 7.1053, -8.289257, 17.948002, 7.1053, -8.18681, 18.258724, 7.1053, -8.289257, 17.650038, 8.836792, -9.459724, 17.948002, 7.1053, -8.18681, 18.735317, 6.461338, -6.8752427, 17.948002, 7.1053, -8.18681, 18.407707, 6.461338, -6.7671647, 17.948002, 7.1053, -8.18681, 17.650038, 8.836792, -9.459724, 17.650414, 8.836041, -9.36478, 18.407707, 6.461338, -6.7671647, 17.948002, 7.1053, -8.18681, 17.911226, 7.3529778, -7.975908, 17.948002, 7.1053, -8.18681, 17.650414, 8.836041, -9.36478, 17.911226, 7.3529778, -7.975908, 17.911226, 7.3529778, -7.975908, 17.650414, 8.836041, -9.36478, 17.740852, 8.835667, -9.33551, 18.407707, 6.461338, -6.7671647, 17.911226, 7.3529778, -7.975908, 18.340908, 6.7709355, -6.6298165, 17.911226, 7.3529778, -7.975908, 17.740852, 8.835667, -9.33551, 18.199057, 7.5060873, -7.9485135, 18.340908, 6.7709355, -6.6298165, 17.911226, 7.3529778, -7.975908, 18.199057, 7.5060873, -7.9485135, 18.340908, 6.7709355, -6.6298165, 18.199057, 7.5060873, -7.9485135, 18.62724, 6.962323, -6.6530833, -21.319002, 11.927798, 5.296257, -17.92721, 14.377124, 7.3704467, -18.167463, 11.003454, 6.8469477, -18.167463, 11.003454, 6.8469477, -17.92721, 14.377124, 7.3704467, -13.817183, 14.463108, 5.3506293, -18.167463, 11.003454, 6.8469477, -13.817183, 14.463108, 5.3506293, -14.551012, 11.882277, 4.945571, -21.319002, 11.927798, 5.296257, -21.828173, 14.324436, 6.480244, -17.92721, 14.377124, 7.3704467, -22.586025, 11.236962, 1.0783278, -21.828173, 14.324436, 6.480244, -21.319002, 11.927798, 5.296257, -17.92721, 14.377124, 7.3704467, -15.004122, 16.938564, 4.195724, -13.817183, 14.463108, 5.3506293, -22.586025, 11.236962, 1.0783278, -22.836817, 14.563845, 1.3805406, -21.828173, 14.324436, 6.480244, -20.660202, 12.173107, -2.8445535, -22.836817, 14.563845, 1.3805406, -22.586025, 11.236962, 1.0783278, -21.828173, 14.324436, 6.480244, -17.478313, 17.78409, 4.983925, -17.92721, 14.377124, 7.3704467, -17.92721, 14.377124, 7.3704467, -17.478313, 17.78409, 4.983925, -15.004122, 16.938564, 4.195724, -22.836817, 14.563845, 1.3805406, -20.092022, 16.877449, 4.697728, -21.828173, 14.324436, 6.480244, -21.828173, 14.324436, 6.480244, -20.092022, 16.877449, 4.697728, -17.478313, 17.78409, 4.983925, -20.660202, 12.173107, -2.8445535, -21.181595, 14.743402, -3.6340203, -22.836817, 14.563845, 1.3805406, -21.181595, 14.743402, -3.6340203, -20.660202, 12.173107, -2.8445535, -17.123413, 11.16952, -4.2650013, -22.836817, 14.563845, 1.3805406, -20.424162, 17.830454, 1.7678959, -20.092022, 16.877449, 4.697728, -21.181595, 14.743402, -3.6340203, -20.424162, 17.830454, 1.7678959, -22.836817, 14.563845, 1.3805406, -20.092022, 16.877449, 4.697728, -17.328682, 18.919605, 1.896874, -17.478313, 17.78409, 4.983925, -20.092022, 16.877449, 4.697728, -20.424162, 17.830454, 1.7678959, -17.328682, 18.919605, 1.896874, -21.181595, 14.743402, -3.6340203, -19.804983, 17.063326, -1.3250556, -20.424162, 17.830454, 1.7678959, -21.181595, 14.743402, -3.6340203, -17.123413, 11.16952, -4.2650013, -17.0918, 14.5098915, -4.268796, -19.804983, 17.063326, -1.3250556, -21.181595, 14.743402, -3.6340203, -17.0918, 14.5098915, -4.268796, -17.0918, 14.5098915, -4.268796, -17.123413, 11.16952, -4.2650013, -13.909912, 11.843496, -1.963623, -20.424162, 17.830454, 1.7678959, -19.804983, 17.063326, -1.3250556, -17.269672, 17.816965, -1.3629904, -19.804983, 17.063326, -1.3250556, -17.0918, 14.5098915, -4.268796, -17.269672, 17.816965, -1.3629904, -20.424162, 17.830454, 1.7678959, -17.269672, 17.816965, -1.3629904, -17.328682, 18.919605, 1.896874, -17.0918, 14.5098915, -4.268796, -13.909912, 11.843496, -1.963623, -13.211069, 14.243084, -1.9935503, -17.269672, 17.816965, -1.3629904, -17.0918, 14.5098915, -4.268796, -13.211069, 14.243084, -1.9935503, -12.704849, 10.935591, 1.5040407, -13.211069, 14.243084, -1.9935503, -13.909912, 11.843496, -1.963623, -17.328682, 18.919605, 1.896874, -17.269672, 17.816965, -1.3629904, -14.734785, 16.841198, -0.5958638, -17.269672, 17.816965, -1.3629904, -13.211069, 14.243084, -1.9935503, -14.734785, 16.841198, -0.5958638, -12.704849, 10.935591, 1.5040407, -12.182192, 14.322748, 1.7211109, -13.211069, 14.243084, -1.9935503, -12.182192, 14.322748, 1.7211109, -14.734785, 16.841198, -0.5958638, -13.211069, 14.243084, -1.9935503, -14.551012, 11.882277, 4.945571, -12.182192, 14.322748, 1.7211109, -12.704849, 10.935591, 1.5040407, -14.551012, 11.882277, 4.945571, -13.817183, 14.463108, 5.3506293, -12.182192, 14.322748, 1.7211109, -12.182192, 14.322748, 1.7211109, -14.323402, 17.77018, 1.8530385, -14.734785, 16.841198, -0.5958638, -17.328682, 18.919605, 1.896874, -14.734785, 16.841198, -0.5958638, -14.323402, 17.77018, 1.8530385, -13.817183, 14.463108, 5.3506293, -14.323402, 17.77018, 1.8530385, -12.182192, 14.322748, 1.7211109, -17.478313, 17.78409, 4.983925, -17.328682, 18.919605, 1.896874, -14.323402, 17.77018, 1.8530385, -13.817183, 14.463108, 5.3506293, -15.004122, 16.938564, 4.195724, -14.323402, 17.77018, 1.8530385, -17.478313, 17.78409, 4.983925, -14.323402, 17.77018, 1.8530385, -15.004122, 16.938564, 4.195724, -20.660202, 12.173107, -2.8445535, -17.690748, 9.96741, 1.2047776, -17.123413, 11.16952, -4.2650013, -20.660202, 12.173107, -2.8445535, -22.586025, 11.236962, 1.0783278, -17.690748, 9.96741, 1.2047776, -17.123413, 11.16952, -4.2650013, -17.690748, 9.96741, 1.2047776, -12.704849, 10.935591, 1.5040407, -17.123413, 11.16952, -4.2650013, -12.704849, 10.935591, 1.5040407, -13.909912, 11.843496, -1.963623, -17.690748, 9.96741, 1.2047776, -14.551012, 11.882277, 4.945571, -12.704849, 10.935591, 1.5040407, -17.690748, 9.96741, 1.2047776, -18.167463, 11.003454, 6.8469477, -14.551012, 11.882277, 4.945571, -22.586025, 11.236962, 1.0783278, -18.167463, 11.003454, 6.8469477, -17.690748, 9.96741, 1.2047776, -22.586025, 11.236962, 1.0783278, -21.319002, 11.927798, 5.296257, -18.167463, 11.003454, 6.8469477, -25.366648, 10.824738, 3.0151107, -23.412582, 12.270897, 4.4486246, -23.698359, 10.294915, 3.8003616, -23.698359, 10.294915, 3.8003616, -23.412582, 12.270897, 4.4486246, -20.900877, 12.281435, 3.6722255, -23.698359, 10.294915, 3.8003616, -20.900877, 12.281435, 3.6722255, -21.649036, 10.824738, 3.0151107, -25.366648, 10.824738, 3.0151107, -25.781822, 12.281435, 3.6722255, -23.412582, 12.270897, 4.4486246, -25.781822, 10.349287, 0.97084606, -25.781822, 12.281435, 3.6722255, -25.366648, 10.824738, 3.0151107, -23.412582, 12.270897, 4.4486246, -21.510363, 13.755413, 3.1213274, -20.900877, 12.281435, 3.6722255, -25.781822, 10.349287, 0.97084606, -25.922182, 12.314311, 1.342185, -25.781822, 12.281435, 3.6722255, -24.61638, 10.892176, -0.880373, -25.922182, 12.314311, 1.342185, -25.781822, 10.349287, 0.97084606, -25.781822, 12.281435, 3.6722255, -22.98392, 14.268375, 3.5440893, -23.412582, 12.270897, 4.4486246, -23.412582, 12.270897, 4.4486246, -22.98392, 14.268375, 3.5440893, -21.510363, 13.755413, 3.1213274, -25.922182, 12.314311, 1.342185, -24.61638, 13.755413, 3.1213274, -25.781822, 12.281435, 3.6722255, -25.781822, 12.281435, 3.6722255, -24.61638, 13.755413, 3.1213274, -22.98392, 14.268375, 3.5440893, -24.61638, 10.892176, -0.880373, -24.665274, 12.3467655, -0.9874339, -25.922182, 12.314311, 1.342185, -24.665274, 12.3467655, -0.9874339, -24.61638, 10.892176, -0.880373, -22.98392, 10.403238, -1.8582478, -25.922182, 12.314311, 1.342185, -24.665274, 14.278913, 1.713524, -24.61638, 13.755413, 3.1213274, -24.665274, 12.3467655, -0.9874339, -24.665274, 14.278913, 1.713524, -25.922182, 12.314311, 1.342185, -24.61638, 13.755413, 3.1213274, -22.841454, 14.934341, 1.8374442, -22.98392, 14.268375, 3.5440893, -24.61638, 13.755413, 3.1213274, -24.665274, 14.278913, 1.713524, -22.841454, 14.934341, 1.8374442, -24.665274, 12.3467655, -0.9874339, -24.120277, 13.784074, 0.11351812, -24.665274, 14.278913, 1.713524, -24.665274, 12.3467655, -0.9874339, -22.98392, 10.403238, -1.8582478, -22.841454, 12.357724, -1.763833, -24.120277, 13.784074, 0.11351812, -24.665274, 12.3467655, -0.9874339, -22.841454, 12.357724, -1.763833, -22.841454, 12.357724, -1.763833, -22.98392, 10.403238, -1.8582478, -21.510363, 10.892176, -0.880373, -24.665274, 14.278913, 1.713524, -24.120277, 13.784074, 0.11351812, -22.841454, 14.289871, -0.116620064, -24.120277, 13.784074, 0.11351812, -22.841454, 12.357724, -1.763833, -22.841454, 14.289871, -0.116620064, -24.665274, 14.278913, 1.713524, -22.841454, 14.289871, -0.116620064, -22.841454, 14.934341, 1.8374442, -22.841454, 12.357724, -1.763833, -21.510363, 10.892176, -0.880373, -21.160097, 12.3467655, -0.9874339, -22.841454, 14.289871, -0.116620064, -22.841454, 12.357724, -1.763833, -21.160097, 12.3467655, -0.9874339, -20.900877, 10.349287, 0.97084606, -21.160097, 12.3467655, -0.9874339, -21.510363, 10.892176, -0.880373, -22.841454, 14.934341, 1.8374442, -22.841454, 14.289871, -0.116620064, -21.625853, 13.784074, 0.11351812, -22.841454, 14.289871, -0.116620064, -21.160097, 12.3467655, -0.9874339, -21.625853, 13.784074, 0.11351812, -20.900877, 10.349287, 0.97084606, -20.331432, 12.314311, 1.342185, -21.160097, 12.3467655, -0.9874339, -20.331432, 12.314311, 1.342185, -21.625853, 13.784074, 0.11351812, -21.160097, 12.3467655, -0.9874339, -21.649036, 10.824738, 3.0151107, -20.331432, 12.314311, 1.342185, -20.900877, 10.349287, 0.97084606, -21.649036, 10.824738, 3.0151107, -20.900877, 12.281435, 3.6722255, -20.331432, 12.314311, 1.342185, -20.331432, 12.314311, 1.342185, -21.160097, 14.278913, 1.713524, -21.625853, 13.784074, 0.11351812, -22.841454, 14.934341, 1.8374442, -21.625853, 13.784074, 0.11351812, -21.160097, 14.278913, 1.713524, -20.900877, 12.281435, 3.6722255, -21.160097, 14.278913, 1.713524, -20.331432, 12.314311, 1.342185, -22.98392, 14.268375, 3.5440893, -22.841454, 14.934341, 1.8374442, -21.160097, 14.278913, 1.713524, -20.900877, 12.281435, 3.6722255, -21.510363, 13.755413, 3.1213274, -21.160097, 14.278913, 1.713524, -22.98392, 14.268375, 3.5440893, -21.160097, 14.278913, 1.713524, -21.510363, 13.755413, 3.1213274, -24.61638, 10.892176, -0.880373, -23.412582, 9.69428, 0.8473475, -22.98392, 10.403238, -1.8582478, -24.61638, 10.892176, -0.880373, -25.781822, 10.349287, 0.97084606, -23.412582, 9.69428, 0.8473475, -22.98392, 10.403238, -1.8582478, -23.412582, 9.69428, 0.8473475, -20.900877, 10.349287, 0.97084606, -22.98392, 10.403238, -1.8582478, -20.900877, 10.349287, 0.97084606, -21.510363, 10.892176, -0.880373, -23.412582, 9.69428, 0.8473475, -21.649036, 10.824738, 3.0151107, -20.900877, 10.349287, 0.97084606, -23.412582, 9.69428, 0.8473475, -23.698359, 10.294915, 3.8003616, -21.649036, 10.824738, 3.0151107, -25.781822, 10.349287, 0.97084606, -23.698359, 10.294915, 3.8003616, -23.412582, 9.69428, 0.8473475, -25.781822, 10.349287, 0.97084606, -25.366648, 10.824738, 3.0151107, -23.698359, 10.294915, 3.8003616, -20.253035, 9.625153, -3.2251654, -20.752932, 10.590384, -1.6858549, -20.401823, 9.271516, -2.002821, -20.401823, 9.271516, -2.002821, -20.752932, 10.590384, -1.6858549, -19.734592, 10.597971, -0.2561345, -20.401823, 9.271516, -2.002821, -19.734592, 10.597971, -0.2561345, -19.474949, 9.625154, -0.8681493, -20.253035, 9.625153, -3.2251654, -20.756725, 10.597969, -3.3503504, -20.752932, 10.590384, -1.6858549, -19.044178, 9.307764, -3.9160008, -20.756725, 10.597969, -3.3503504, -20.253035, 9.625153, -3.2251654, -20.752932, 10.590384, -1.6858549, -19.513306, 11.581747, -0.75813866, -19.734592, 10.597971, -0.2561345, -19.044178, 9.307764, -3.9160008, -19.309301, 10.619466, -3.9273815, -20.756725, 10.597969, -3.3503504, -17.62668, 9.670253, -3.5648928, -19.309301, 10.619466, -3.9273815, -19.044178, 9.307764, -3.9160008, -20.756725, 10.597969, -3.3503504, -20.089493, 11.924424, -1.6036634, -20.752932, 10.590384, -1.6858549, -20.752932, 10.590384, -1.6858549, -20.089493, 11.924424, -1.6036634, -19.513306, 11.581747, -0.75813866, -19.309301, 10.619466, -3.9273815, -20.163677, 11.581746, -2.7269554, -20.756725, 10.597969, -3.3503504, -20.756725, 10.597969, -3.3503504, -20.163677, 11.581746, -2.7269554, -20.089493, 11.924424, -1.6036634, -17.62668, 9.670253, -3.5648928, -17.569357, 10.641384, -3.6184235, -19.309301, 10.619466, -3.9273815, -17.569357, 10.641384, -3.6184235, -17.62668, 9.670253, -3.5648928, -16.665245, 9.344013, -2.7349634, -19.309301, 10.619466, -3.9273815, -19.281483, 11.931589, -3.052774, -20.163677, 11.581746, -2.7269554, -17.569357, 10.641384, -3.6184235, -19.281483, 11.931589, -3.052774, -19.309301, 10.619466, -3.9273815, -20.163677, 11.581746, -2.7269554, -18.978003, 12.368683, -1.870472, -20.089493, 11.924424, -1.6036634, -20.163677, 11.581746, -2.7269554, -19.281483, 11.931589, -3.052774, -18.978003, 12.368683, -1.870472, -17.569357, 10.641384, -3.6184235, -18.15271, 11.601134, -3.0422359, -19.281483, 11.931589, -3.052774, -17.569357, 10.641384, -3.6184235, -16.665245, 9.344013, -2.7349634, -16.69475, 10.64855, -2.6245313, -18.15271, 11.601134, -3.0422359, -17.569357, 10.641384, -3.6184235, -16.69475, 10.64855, -2.6245313, -16.69475, 10.64855, -2.6245313, -16.665245, 9.344013, -2.7349634, -16.976309, 9.670254, -1.596076, -19.281483, 11.931589, -3.052774, -18.15271, 11.601134, -3.0422359, -17.739222, 11.938755, -2.279746, -18.15271, 11.601134, -3.0422359, -16.69475, 10.64855, -2.6245313, -17.739222, 11.938755, -2.279746, -19.281483, 11.931589, -3.052774, -17.739222, 11.938755, -2.279746, -18.978003, 12.368683, -1.870472, -16.69475, 10.64855, -2.6245313, -16.976309, 9.670254, -1.596076, -16.835108, 10.641384, -1.396286, -17.739222, 11.938755, -2.279746, -16.69475, 10.64855, -2.6245313, -16.835108, 10.641384, -1.396286, -18.022047, 9.307765, -0.821784, -16.835108, 10.641384, -1.396286, -16.976309, 9.670254, -1.596076, -18.978003, 12.368683, -1.870472, -17.739222, 11.938755, -2.279746, -17.630474, 11.601135, -1.4607759, -17.739222, 11.938755, -2.279746, -16.835108, 10.641384, -1.396286, -17.630474, 11.601135, -1.4607759, -18.022047, 9.307765, -0.821784, -18.138802, 10.619467, -0.38300514, -16.835108, 10.641384, -1.396286, -18.138802, 10.619467, -0.38300514, -17.630474, 11.601135, -1.4607759, -16.835108, 10.641384, -1.396286, -19.474949, 9.625154, -0.8681493, -18.138802, 10.619467, -0.38300514, -18.022047, 9.307765, -0.821784, -19.474949, 9.625154, -0.8681493, -19.734592, 10.597971, -0.2561345, -18.138802, 10.619467, -0.38300514, -18.138802, 10.619467, -0.38300514, -18.547232, 11.93159, -0.8306365, -17.630474, 11.601135, -1.4607759, -18.978003, 12.368683, -1.870472, -17.630474, 11.601135, -1.4607759, -18.547232, 11.93159, -0.8306365, -19.734592, 10.597971, -0.2561345, -18.547232, 11.93159, -0.8306365, -18.138802, 10.619467, -0.38300514, -20.089493, 11.924424, -1.6036634, -18.978003, 12.368683, -1.870472, -18.547232, 11.93159, -0.8306365, -19.734592, 10.597971, -0.2561345, -19.513306, 11.581747, -0.75813866, -18.547232, 11.93159, -0.8306365, -20.089493, 11.924424, -1.6036634, -18.547232, 11.93159, -0.8306365, -19.513306, 11.581747, -0.75813866, -17.62668, 9.670253, -3.5648928, -18.469677, 8.87025, -2.4403362, -16.665245, 9.344013, -2.7349634, -17.62668, 9.670253, -3.5648928, -19.044178, 9.307764, -3.9160008, -18.469677, 8.87025, -2.4403362, -16.665245, 9.344013, -2.7349634, -18.469677, 8.87025, -2.4403362, -18.022047, 9.307765, -0.821784, -16.665245, 9.344013, -2.7349634, -18.022047, 9.307765, -0.821784, -16.976309, 9.670254, -1.596076, -18.469677, 8.87025, -2.4403362, -19.474949, 9.625154, -0.8681493, -18.022047, 9.307765, -0.821784, -18.469677, 8.87025, -2.4403362, -20.401823, 9.271516, -2.002821, -19.474949, 9.625154, -0.8681493, -19.044178, 9.307764, -3.9160008, -20.401823, 9.271516, -2.002821, -18.469677, 8.87025, -2.4403362, -19.044178, 9.307764, -3.9160008, -20.253035, 9.625153, -3.2251654, -20.401823, 9.271516, -2.002821, -17.464405, 7.537475, 1.8374468, -19.955458, 8.19459, 1.3919232, -17.577787, 7.2154503, 1.3678982, -17.464405, 7.537475, 1.8374468, -19.75946, 8.452126, 1.8374465, -19.955458, 8.19459, 1.3919232, -19.75946, 8.452126, 1.8374465, -22.311209, 10.6898575, 1.7080457, -19.955458, 8.19459, 1.3919232, -19.75946, 8.452126, 1.8374465, -22.217215, 10.689015, 1.8374457, -22.311209, 10.6898575, 1.7080457, -19.955458, 8.19459, 1.3919232, -22.311209, 10.6898575, 1.7080457, -22.46337, 10.691122, 1.757361, -17.577787, 7.2154503, 1.3678982, -19.955458, 8.19459, 1.3919232, -20.272423, 7.7777286, 1.5622085, -19.955458, 8.19459, 1.3919232, -22.46337, 10.691122, 1.757361, -20.272423, 7.7777286, 1.5622085, -17.577787, 7.2154503, 1.3678982, -20.272423, 7.7777286, 1.5622085, -17.761982, 6.6949005, 1.547035, -20.272423, 7.7777286, 1.5622085, -22.46337, 10.691122, 1.757361, -22.46337, 10.691122, 1.9175303, -17.761982, 6.6949005, 1.547035, -20.272423, 7.7777286, 1.5622085, -20.272423, 7.777729, 2.1126847, -20.272423, 7.7777286, 1.5622085, -22.46337, 10.691122, 1.9175303, -20.272423, 7.777729, 2.1126847, -17.761982, 6.6949005, 1.547035, -20.272423, 7.777729, 2.1126847, -17.761982, 6.694901, 2.127859, -20.272423, 7.777729, 2.1126847, -22.46337, 10.691122, 1.9175303, -22.311209, 10.689858, 1.9668455, -17.761982, 6.694901, 2.127859, -20.272423, 7.777729, 2.1126847, -19.955458, 8.194591, 2.28297, -20.272423, 7.777729, 2.1126847, -22.311209, 10.689858, 1.9668455, -19.955458, 8.194591, 2.28297, -19.955458, 8.194591, 2.28297, -22.311209, 10.689858, 1.9668455, -22.217215, 10.689015, 1.8374457, -17.761982, 6.694901, 2.127859, -19.955458, 8.194591, 2.28297, -17.577787, 7.215451, 2.3069956, -19.955458, 8.194591, 2.28297, -22.217215, 10.689015, 1.8374457, -19.75946, 8.452126, 1.8374465, -17.577787, 7.215451, 2.3069956, -19.955458, 8.194591, 2.28297, -19.75946, 8.452126, 1.8374465, -17.577787, 7.215451, 2.3069956, -19.75946, 8.452126, 1.8374465, -17.464405, 7.537475, 1.8374468, -19.772528, 3.1467314, 1.8374484, -17.801182, 7.205756, 2.2829702, -18.92911, 3.1467319, 3.0694866, -19.772528, 3.1467314, 1.8374484, -18.119411, 7.263501, 1.8374469, -17.801182, 7.205756, 2.2829702, -18.119411, 7.263501, 1.8374469, -17.641012, 11.282906, 2.034285, -17.801182, 7.205756, 2.2829702, -18.119411, 7.263501, 1.8374469, -17.783056, 11.2980795, 1.8374455, -17.641012, 11.282906, 2.034285, -17.801182, 7.205756, 2.2829702, -17.641012, 11.282906, 2.034285, -17.410873, 11.258881, 1.9592583, -18.92911, 3.1467319, 3.0694866, -17.801182, 7.205756, 2.2829702, -17.28611, 7.1121836, 2.1126852, -17.801182, 7.205756, 2.2829702, -17.410873, 11.258881, 1.9592583, -17.28611, 7.1121836, 2.1126852, -18.92911, 3.1467319, 3.0694866, -17.28611, 7.1121836, 2.1126852, -17.563877, 3.1463103, 2.599095, -17.28611, 7.1121836, 2.1126852, -17.410873, 11.258881, 1.9592583, -17.410873, 11.25888, 1.7156326, -17.563877, 3.1463103, 2.599095, -17.28611, 7.1121836, 2.1126852, -17.28611, 7.1121836, 1.5622088, -17.28611, 7.1121836, 2.1126852, -17.410873, 11.25888, 1.7156326, -17.28611, 7.1121836, 1.5622088, -17.563877, 3.1463103, 2.599095, -17.28611, 7.1121836, 1.5622088, -17.563877, 3.1463099, 1.0758016, -17.28611, 7.1121836, 1.5622088, -17.410873, 11.25888, 1.7156326, -17.641012, 11.282906, 1.6406059, -17.563877, 3.1463099, 1.0758016, -17.28611, 7.1121836, 1.5622088, -17.801182, 7.205756, 1.3919237, -17.28611, 7.1121836, 1.5622088, -17.641012, 11.282906, 1.6406059, -17.801182, 7.205756, 1.3919237, -17.801182, 7.205756, 1.3919237, -17.641012, 11.282906, 1.6406059, -17.783056, 11.2980795, 1.8374455, -17.563877, 3.1463099, 1.0758016, -17.801182, 7.205756, 1.3919237, -18.92911, 3.146731, 0.60541, -17.801182, 7.205756, 1.3919237, -17.783056, 11.2980795, 1.8374455, -18.119411, 7.263501, 1.8374469, -18.92911, 3.146731, 0.60541, -17.801182, 7.205756, 1.3919237, -18.119411, 7.263501, 1.8374469, -18.92911, 3.146731, 0.60541, -18.119411, 7.263501, 1.8374469, -19.772528, 3.1467314, 1.8374484, -17.85176, 7.430414, 1.5381833, -18.091171, 7.869193, -0.13432074, -17.577787, 7.2154503, 1.3678982, -17.85176, 7.430414, 1.5381833, -18.33269, 8.041164, 0.08317232, -18.091171, 7.869193, -0.13432074, -18.33269, 8.041164, 0.08317232, -18.784958, 9.534952, -1.5615134, -18.091171, 7.869193, -0.13432074, -18.33269, 8.041164, 0.08317232, -18.84734, 9.534531, -1.4746847, -18.784958, 9.534952, -1.5615134, -18.091171, 7.869193, -0.13432074, -18.784958, 9.534952, -1.5615134, -18.848183, 9.535795, -1.6474986, -17.577787, 7.2154503, 1.3678982, -18.091171, 7.869193, -0.13432074, -18.265673, 7.5910044, -0.29954767, -18.091171, 7.869193, -0.13432074, -18.848183, 9.535795, -1.6474986, -18.265673, 7.5910044, -0.29954767, -17.577787, 7.2154503, 1.3678982, -18.265673, 7.5910044, -0.29954767, -17.73037, 6.867715, 1.2886567, -18.265673, 7.5910044, -0.29954767, -18.848183, 9.535795, -1.6474986, -18.94934, 9.535795, -1.6142006, -17.73037, 6.867715, 1.2886567, -18.265673, 7.5910044, -0.29954767, -18.614672, 7.591005, -0.18447876, -18.265673, 7.5910044, -0.29954767, -18.94934, 9.535795, -1.6142006, -18.614672, 7.591005, -0.18447876, -17.73037, 6.867715, 1.2886567, -18.614672, 7.591005, -0.18447876, -18.098337, 6.867715, 1.4100481, -18.614672, 7.591005, -0.18447876, -18.94934, 9.535795, -1.6142006, -18.94892, 9.534952, -1.5075612, -18.098337, 6.867715, 1.4100481, -18.614672, 7.591005, -0.18447876, -18.65598, 7.869193, 0.052403092, -18.614672, 7.591005, -0.18447876, -18.94892, 9.534952, -1.5075612, -18.65598, 7.869193, 0.052403092, -18.65598, 7.869193, 0.052403092, -18.94892, 9.534952, -1.5075612, -18.84734, 9.534531, -1.4746847, -18.098337, 6.867715, 1.4100481, -18.65598, 7.869193, 0.052403092, -18.173365, 7.2154503, 1.5643163, -18.65598, 7.869193, 0.052403092, -18.84734, 9.534531, -1.4746847, -18.33269, 8.041164, 0.08317232, -18.173365, 7.2154503, 1.5643163, -18.65598, 7.869193, 0.052403092, -18.33269, 8.041164, 0.08317232, -18.173365, 7.2154503, 1.5643163, -18.33269, 8.041164, 0.08317232, -17.85176, 7.430414, 1.5381833, -18.021717, 16.363804, -17.998592, -21.997292, 20.020512, -13.591122, -21.163078, 14.983807, -13.799868, -21.163078, 14.983807, -13.799868, -21.997292, 20.020512, -13.591122, -20.16242, 20.148884, -7.004968, -21.163078, 14.983807, -13.799868, -20.16242, 20.148884, -7.004968, -19.366419, 16.295841, -7.970547, -18.021717, 16.363804, -17.998592, -19.619282, 19.941853, -19.07104, -21.997292, 20.020512, -13.591122, -11.484028, 15.332424, -18.699127, -19.619282, 19.941853, -19.07104, -18.021717, 16.363804, -17.998592, -21.997292, 20.020512, -13.591122, -18.141577, 23.844606, -8.429464, -20.16242, 20.148884, -7.004968, -11.484028, 15.332424, -18.699127, -11.858614, 20.29928, -19.150177, -19.619282, 19.941853, -19.07104, -6.256456, 16.73004, -14.795372, -11.858614, 20.29928, -19.150177, -11.484028, 15.332424, -18.699127, -19.619282, 19.941853, -19.07104, -18.61851, 25.10693, -12.27676, -21.997292, 20.020512, -13.591122, -21.997292, 20.020512, -13.591122, -18.61851, 25.10693, -12.27676, -18.141577, 23.844606, -8.429464, -11.858614, 20.29928, -19.150177, -17.480486, 23.753365, -16.033627, -19.619282, 19.941853, -19.07104, -19.619282, 19.941853, -19.07104, -17.480486, 23.753365, -16.033627, -18.61851, 25.10693, -12.27676, -6.256456, 16.73004, -14.795372, -4.954714, 20.567348, -15.343607, -11.858614, 20.29928, -19.150177, -4.954714, 20.567348, -15.343607, -6.256456, 16.73004, -14.795372, -5.1436496, 15.231738, -9.215076, -11.858614, 20.29928, -19.150177, -13.089849, 25.17615, -15.716137, -17.480486, 23.753365, -16.033627, -4.954714, 20.567348, -15.343607, -13.089849, 25.17615, -15.716137, -11.858614, 20.29928, -19.150177, -17.480486, 23.753365, -16.033627, -14.129517, 26.802193, -11.209101, -18.61851, 25.10693, -12.27676, -17.480486, 23.753365, -16.033627, -13.089849, 25.17615, -15.716137, -14.129517, 26.802193, -11.209101, -4.954714, 20.567348, -15.343607, -8.721195, 24.030872, -13.957819, -13.089849, 25.17615, -15.716137, -4.954714, 20.567348, -15.343607, -5.1436496, 15.231738, -9.215076, -5.146767, 20.21873, -9.1676445, -8.721195, 24.030872, -13.957819, -4.954714, 20.567348, -15.343607, -5.146767, 20.21873, -9.1676445, -5.146767, 20.21873, -9.1676445, -5.1436496, 15.231738, -9.215076, -9.403631, 16.237947, -5.131659, -13.089849, 25.17615, -15.716137, -8.721195, 24.030872, -13.957819, -9.362032, 25.156012, -10.226952, -8.721195, 24.030872, -13.957819, -5.146767, 20.21873, -9.1676445, -9.362032, 25.156012, -10.226952, -13.089849, 25.17615, -15.716137, -9.362032, 25.156012, -10.226952, -14.129517, 26.802193, -11.209101, -5.146767, 20.21873, -9.1676445, -9.403631, 16.237947, -5.131659, -9.5517025, 19.8204, -4.097917, -9.362032, 25.156012, -10.226952, -5.146767, 20.21873, -9.1676445, -9.5517025, 19.8204, -4.097917, -14.8233185, 14.882492, -4.315932, -9.5517025, 19.8204, -4.097917, -9.403631, 16.237947, -5.131659, -14.129517, 26.802193, -11.209101, -9.362032, 25.156012, -10.226952, -11.184143, 23.699244, -6.717871, -9.362032, 25.156012, -10.226952, -9.5517025, 19.8204, -4.097917, -11.184143, 23.699244, -6.717871, -14.8233185, 14.882492, -4.315932, -15.285444, 19.939335, -3.6085896, -9.5517025, 19.8204, -4.097917, -15.285444, 19.939335, -3.6085896, -11.184143, 23.699244, -6.717871, -9.5517025, 19.8204, -4.097917, -19.366419, 16.295841, -7.970547, -15.285444, 19.939335, -3.6085896, -14.8233185, 14.882492, -4.315932, -19.366419, 16.295841, -7.970547, -20.16242, 20.148884, -7.004968, -15.285444, 19.939335, -3.6085896, -15.285444, 19.939335, -3.6085896, -14.890806, 25.086163, -6.7869554, -11.184143, 23.699244, -6.717871, -14.129517, 26.802193, -11.209101, -11.184143, 23.699244, -6.717871, -14.890806, 25.086163, -6.7869554, -20.16242, 20.148884, -7.004968, -14.890806, 25.086163, -6.7869554, -15.285444, 19.939335, -3.6085896, -18.61851, 25.10693, -12.27676, -14.129517, 26.802193, -11.209101, -14.890806, 25.086163, -6.7869554, -20.16242, 20.148884, -7.004968, -18.141577, 23.844606, -8.429464, -14.890806, 25.086163, -6.7869554, -18.61851, 25.10693, -12.27676, -14.890806, 25.086163, -6.7869554, -18.141577, 23.844606, -8.429464, -6.256456, 16.73004, -14.795372, -13.014427, 13.437051, -11.550283, -5.1436496, 15.231738, -9.215076, -6.256456, 16.73004, -14.795372, -11.484028, 15.332424, -18.699127, -13.014427, 13.437051, -11.550283, -5.1436496, 15.231738, -9.215076, -13.014427, 13.437051, -11.550283, -14.8233185, 14.882492, -4.315932, -5.1436496, 15.231738, -9.215076, -14.8233185, 14.882492, -4.315932, -9.403631, 16.237947, -5.131659, -13.014427, 13.437051, -11.550283, -19.366419, 16.295841, -7.970547, -14.8233185, 14.882492, -4.315932, -13.014427, 13.437051, -11.550283, -21.163078, 14.983807, -13.799868, -19.366419, 16.295841, -7.970547, -11.484028, 15.332424, -18.699127, -21.163078, 14.983807, -13.799868, -13.014427, 13.437051, -11.550283, -11.484028, 15.332424, -18.699127, -18.021717, 16.363804, -17.998592, -21.163078, 14.983807, -13.799868, -13.562267, 14.716995, -23.311634, -16.202703, 16.876032, -20.837955, -15.172899, 13.925998, -21.079226, -15.172899, 13.925998, -21.079226, -16.202703, 16.876032, -20.837955, -15.753397, 16.891764, -16.938854, -15.172899, 13.925998, -21.079226, -15.753397, 16.891764, -16.938854, -14.583576, 14.716994, -17.856216, -13.562267, 14.716995, -23.311634, -14.412493, 16.891764, -24.101406, -16.202703, 16.876032, -20.837955, -10.448349, 14.0071745, -23.35928, -14.412493, 16.891764, -24.101406, -13.562267, 14.716995, -23.311634, -16.202703, 16.876032, -20.837955, -14.777542, 19.092333, -17.681902, -15.753397, 16.891764, -16.938854, -10.448349, 14.0071745, -23.35928, -10.954712, 16.940847, -23.667263, -14.412493, 16.891764, -24.101406, -8.051947, 14.8176775, -21.140478, -10.954712, 16.940847, -23.667263, -10.448349, 14.0071745, -23.35928, -14.412493, 16.891764, -24.101406, -14.993107, 19.85816, -19.960417, -16.202703, 16.876032, -20.837955, -16.202703, 16.876032, -20.837955, -14.993107, 19.85816, -19.960417, -14.777542, 19.092333, -17.681902, -10.954712, 16.940847, -23.667263, -13.92425, 19.092335, -22.239834, -14.412493, 16.891764, -24.101406, -14.412493, 16.891764, -24.101406, -13.92425, 19.092335, -22.239834, -14.993107, 19.85816, -19.960417, -8.051947, 14.8176775, -21.140478, -7.8814073, 16.989302, -21.182816, -10.954712, 16.940847, -23.667263, -7.8814073, 16.989302, -21.182816, -8.051947, 14.8176775, -21.140478, -7.065434, 14.08772, -18.476276, -10.954712, 16.940847, -23.667263, -11.844934, 19.873892, -21.924828, -13.92425, 19.092335, -22.239834, -7.8814073, 16.989302, -21.182816, -11.844934, 19.873892, -21.924828, -10.954712, 16.940847, -23.667263, -13.92425, 19.092335, -22.239834, -12.527824, 20.852411, -19.282501, -14.993107, 19.85816, -19.960417, -13.92425, 19.092335, -22.239834, -11.844934, 19.873892, -21.924828, -12.527824, 20.852411, -19.282501, -7.8814073, 16.989302, -21.182816, -9.646725, 19.135124, -20.685514, -11.844934, 19.873892, -21.924828, -7.8814073, 16.989302, -21.182816, -7.065434, 14.08772, -18.476276, -7.2431235, 17.005661, -18.293152, -9.646725, 19.135124, -20.685514, -7.8814073, 16.989302, -21.182816, -7.2431235, 17.005661, -18.293152, -7.2431235, 17.005661, -18.293152, -7.065434, 14.08772, -18.476276, -8.905237, 14.8176775, -16.582546, -11.844934, 19.873892, -21.924828, -9.646725, 19.135124, -20.685514, -9.66033, 19.890251, -18.745678, -9.646725, 19.135124, -20.685514, -7.2431235, 17.005661, -18.293152, -9.66033, 19.890251, -18.745678, -11.844934, 19.873892, -21.924828, -9.66033, 19.890251, -18.745678, -12.527824, 20.852411, -19.282501, -7.2431235, 17.005661, -18.293152, -8.905237, 14.8176775, -16.582546, -8.844357, 16.9893, -16.039139, -9.66033, 19.890251, -18.745678, -7.2431235, 17.005661, -18.293152, -8.844357, 16.9893, -16.039139, -11.789251, 14.007174, -16.196726, -8.844357, 16.9893, -16.039139, -8.905237, 14.8176775, -16.582546, -12.527824, 20.852411, -19.282501, -9.66033, 19.890251, -18.745678, -10.331999, 19.135124, -17.025066, -9.66033, 19.890251, -18.745678, -8.844357, 16.9893, -16.039139, -10.331999, 19.135124, -17.025066, -11.789251, 14.007174, -16.196726, -12.490612, 16.940847, -15.46311, -8.844357, 16.9893, -16.039139, -12.490612, 16.940847, -15.46311, -10.331999, 19.135124, -17.025066, -8.844357, 16.9893, -16.039139, -14.583576, 14.716994, -17.856216, -12.490612, 16.940847, -15.46311, -11.789251, 14.007174, -16.196726, -14.583576, 14.716994, -17.856216, -15.753397, 16.891764, -16.938854, -12.490612, 16.940847, -15.46311, -12.490612, 16.940847, -15.46311, -12.807882, 19.87389, -16.78115, -10.331999, 19.135124, -17.025066, -12.527824, 20.852411, -19.282501, -10.331999, 19.135124, -17.025066, -12.807882, 19.87389, -16.78115, -15.753397, 16.891764, -16.938854, -12.807882, 19.87389, -16.78115, -12.490612, 16.940847, -15.46311, -14.993107, 19.85816, -19.960417, -12.527824, 20.852411, -19.282501, -12.807882, 19.87389, -16.78115, -15.753397, 16.891764, -16.938854, -14.777542, 19.092333, -17.681902, -12.807882, 19.87389, -16.78115, -14.993107, 19.85816, -19.960417, -12.807882, 19.87389, -16.78115, -14.777542, 19.092333, -17.681902, -8.051947, 14.8176775, -21.140478, -10.918001, 13.029283, -19.848606, -7.065434, 14.08772, -18.476276, -8.051947, 14.8176775, -21.140478, -10.448349, 14.0071745, -23.35928, -10.918001, 13.029283, -19.848606, -7.065434, 14.08772, -18.476276, -10.918001, 13.029283, -19.848606, -11.789251, 14.007174, -16.196726, -7.065434, 14.08772, -18.476276, -11.789251, 14.007174, -16.196726, -8.905237, 14.8176775, -16.582546, -10.918001, 13.029283, -19.848606, -14.583576, 14.716994, -17.856216, -11.789251, 14.007174, -16.196726, -10.918001, 13.029283, -19.848606, -15.172899, 13.925998, -21.079226, -14.583576, 14.716994, -17.856216, -10.448349, 14.0071745, -23.35928, -15.172899, 13.925998, -21.079226, -10.918001, 13.029283, -19.848606, -10.448349, 14.0071745, -23.35928, -13.562267, 14.716995, -23.311634, -15.172899, 13.925998, -21.079226, -5.8097816, 12.926081, -14.093311, -7.931314, 14.367117, -15.249769, -7.562638, 12.39812, -14.647457, -7.562638, 12.39812, -14.647457, -7.931314, 14.367117, -15.249769, -10.30912, 14.378445, -14.14818, -7.562638, 12.39812, -14.647457, -10.30912, 14.378445, -14.14818, -9.4823475, 12.926081, -13.599033, -5.8097816, 12.926081, -14.093311, -5.4877048, 14.378444, -14.798061, -7.931314, 14.367117, -15.249769, -5.128114, 12.452237, -12.129587, -5.4877048, 14.378444, -14.798061, -5.8097816, 12.926081, -14.093311, -7.931314, 14.367117, -15.249769, -9.633245, 15.847169, -13.685543, -10.30912, 14.378445, -14.14818, -5.128114, 12.452237, -12.129587, -5.038579, 14.410537, -12.515514, -5.4877048, 14.378444, -14.798061, -6.032766, 12.993412, -10.145935, -5.038579, 14.410537, -12.515514, -5.128114, 12.452237, -12.129587, -5.4877048, 14.378444, -14.798061, -8.234188, 16.358768, -14.298784, -7.931314, 14.367117, -15.249769, -7.931314, 14.367117, -15.249769, -8.234188, 16.358768, -14.298784, -9.633245, 15.847169, -13.685543, -5.038579, 14.410537, -12.515514, -6.565431, 15.847169, -14.099052, -5.4877048, 14.378444, -14.798061, -5.4877048, 14.378444, -14.798061, -6.565431, 15.847169, -14.099052, -8.234188, 16.358768, -14.298784, -6.032766, 12.993412, -10.145935, -5.9699607, 14.443258, -10.047108, -5.038579, 14.410537, -12.515514, -5.9699607, 14.443258, -10.047108, -6.032766, 12.993412, -10.145935, -7.5147758, 12.506355, -8.963072, -5.038579, 14.410537, -12.515514, -6.3296666, 16.369465, -12.714966, -6.565431, 15.847169, -14.099052, -5.9699607, 14.443258, -10.047108, -6.3296666, 16.369465, -12.714966, -5.038579, 14.410537, -12.515514, -6.565431, 15.847169, -14.099052, -8.14801, 17.022022, -12.594429, -8.234188, 16.358768, -14.298784, -6.565431, 15.847169, -14.099052, -6.3296666, 16.369465, -12.714966, -8.14801, 17.022022, -12.594429, -5.9699607, 14.443258, -10.047108, -6.655229, 15.876115, -11.061441, -6.3296666, 16.369465, -12.714966, -5.9699607, 14.443258, -10.047108, -7.5147758, 12.506355, -8.963072, -7.6687236, 14.453957, -9.036707, -6.655229, 15.876115, -11.061441, -5.9699607, 14.443258, -10.047108, -7.6687236, 14.453957, -9.036707, -7.6687236, 14.453957, -9.036707, -7.5147758, 12.506355, -8.963072, -9.10058, 12.993413, -9.732424, -6.3296666, 16.369465, -12.714966, -6.655229, 15.876115, -11.061441, -7.8877406, 16.380163, -10.664139, -6.655229, 15.876115, -11.061441, -7.6687236, 14.453957, -9.036707, -7.8877406, 16.380163, -10.664139, -6.3296666, 16.369465, -12.714966, -7.8877406, 16.380163, -10.664139, -8.14801, 17.022022, -12.594429, -7.6687236, 14.453957, -9.036707, -9.10058, 12.993413, -9.732424, -9.432554, 14.443259, -9.580103, -7.8877406, 16.380163, -10.664139, -7.6687236, 14.453957, -9.036707, -9.432554, 14.443259, -9.580103, -9.94953, 12.452238, -11.479706, -9.432554, 14.443259, -9.580103, -9.10058, 12.993413, -9.732424, -8.14801, 17.022022, -12.594429, -7.8877406, 16.380163, -10.664139, -9.119415, 15.876115, -10.7295475, -7.8877406, 16.380163, -10.664139, -9.432554, 14.443259, -9.580103, -9.119415, 15.876115, -10.7295475, -9.94953, 12.452238, -11.479706, -10.561342, 14.410537, -11.77158, -9.432554, 14.443259, -9.580103, -10.561342, 14.410537, -11.77158, -9.119415, 15.876115, -10.7295475, -9.432554, 14.443259, -9.580103, -9.4823475, 12.926081, -13.599033, -10.561342, 14.410537, -11.77158, -9.94953, 12.452238, -11.479706, -9.4823475, 12.926081, -13.599033, -10.30912, 14.378445, -14.14818, -10.561342, 14.410537, -11.77158, -10.561342, 14.410537, -11.77158, -9.792261, 16.369465, -12.247959, -9.119415, 15.876115, -10.7295475, -8.14801, 17.022022, -12.594429, -9.119415, 15.876115, -10.7295475, -9.792261, 16.369465, -12.247959, -10.30912, 14.378445, -14.14818, -9.792261, 16.369465, -12.247959, -10.561342, 14.410537, -11.77158, -8.234188, 16.358768, -14.298784, -8.14801, 17.022022, -12.594429, -9.792261, 16.369465, -12.247959, -10.30912, 14.378445, -14.14818, -9.633245, 15.847169, -13.685543, -9.792261, 16.369465, -12.247959, -8.234188, 16.358768, -14.298784, -9.792261, 16.369465, -12.247959, -9.633245, 15.847169, -13.685543, -6.032766, 12.993412, -10.145935, -7.45141, 11.799051, -11.691931, -7.5147758, 12.506355, -8.963072, -6.032766, 12.993412, -10.145935, -5.128114, 12.452237, -12.129587, -7.45141, 11.799051, -11.691931, -7.5147758, 12.506355, -8.963072, -7.45141, 11.799051, -11.691931, -9.94953, 12.452238, -11.479706, -7.5147758, 12.506355, -8.963072, -9.94953, 12.452238, -11.479706, -9.10058, 12.993413, -9.732424, -7.45141, 11.799051, -11.691931, -9.4823475, 12.926081, -13.599033, -9.94953, 12.452238, -11.479706, -7.45141, 11.799051, -11.691931, -7.562638, 12.39812, -14.647457, -9.4823475, 12.926081, -13.599033, -5.128114, 12.452237, -12.129587, -7.562638, 12.39812, -14.647457, -7.45141, 11.799051, -11.691931, -5.128114, 12.452237, -12.129587, -5.8097816, 12.926081, -14.093311, -7.562638, 12.39812, -14.647457, -14.00502, 9.80929, -11.391943, -12.666889, 10.790327, -14.925047, -13.28483, 9.328525, -11.429331, -14.00502, 9.80929, -11.391943, -13.374517, 11.174813, -14.759827, -12.666889, 10.790327, -14.925047, -13.374517, 11.174813, -14.759827, -12.483606, 14.515627, -18.468845, -12.666889, 10.790327, -14.925047, -13.374517, 11.174813, -14.759827, -12.699318, 14.514368, -18.366463, -12.483606, 14.515627, -18.468845, -12.666889, 10.790327, -14.925047, -12.483606, 14.515627, -18.468845, -12.5141735, 14.517514, -18.70568, -13.28483, 9.328525, -11.429331, -12.666889, 10.790327, -14.925047, -12.829697, 10.167975, -15.43696, -12.666889, 10.790327, -14.925047, -12.5141735, 14.517514, -18.70568, -12.829697, 10.167975, -15.43696, -13.28483, 9.328525, -11.429331, -12.829697, 10.167975, -15.43696, -13.497103, 8.551372, -11.74884, -12.829697, 10.167975, -15.43696, -12.5141735, 14.517514, -18.70568, -12.749214, 14.517514, -18.749683, -13.497103, 8.551372, -11.74884, -12.829697, 10.167975, -15.43696, -13.637495, 10.167976, -15.588188, -12.829697, 10.167975, -15.43696, -12.749214, 14.517514, -18.749683, -13.637495, 10.167976, -15.588188, -13.497103, 8.551372, -11.74884, -13.637495, 10.167976, -15.588188, -14.349434, 8.551372, -11.908405, -13.637495, 10.167976, -15.588188, -12.749214, 14.517514, -18.749683, -12.863384, 14.515627, -18.539944, -14.349434, 8.551372, -11.908405, -13.637495, 10.167976, -15.588188, -13.974457, 10.790327, -15.169838, -13.637495, 10.167976, -15.588188, -12.863384, 14.515627, -18.539944, -13.974457, 10.790327, -15.169838, -13.974457, 10.790327, -15.169838, -12.863384, 14.515627, -18.539944, -12.699318, 14.514368, -18.366463, -14.349434, 8.551372, -11.908405, -13.974457, 10.790327, -15.169838, -14.662912, 9.328525, -11.687323, -13.974457, 10.790327, -15.169838, -12.699318, 14.514368, -18.366463, -13.374517, 11.174813, -14.759827, -14.662912, 9.328525, -11.687323, -13.974457, 10.790327, -15.169838, -13.374517, 11.174813, -14.759827, -14.662912, 9.328525, -11.687323, -13.374517, 11.174813, -14.759827, -14.00502, 9.80929, -11.391943, -13.370928, 3.2541475, -14.779001, -14.566284, 9.314051, -12.008541, -15.410591, 3.2541475, -13.879795, -13.370928, 3.2541475, -14.779001, -13.825075, 9.400261, -12.353134, -14.566284, 9.314051, -12.008541, -13.825075, 9.400261, -12.353134, -14.245354, 15.401014, -11.705181, -14.566284, 9.314051, -12.008541, -13.825075, 9.400261, -12.353134, -13.917479, 15.423668, -11.859549, -14.245354, 15.401014, -11.705181, -14.566284, 9.314051, -12.008541, -14.245354, 15.401014, -11.705181, -14.198481, 15.365146, -11.346853, -15.410591, 3.2541475, -13.879795, -14.566284, 9.314051, -12.008541, -14.457899, 9.174353, -11.205919, -14.566284, 9.314051, -12.008541, -14.198481, 15.365146, -11.346853, -14.457899, 9.174353, -11.205919, -15.410591, 3.2541475, -13.879795, -14.457899, 9.174353, -11.205919, -15.095373, 3.253518, -11.747156, -14.457899, 9.174353, -11.205919, -14.198481, 15.365146, -11.346853, -13.840971, 15.365146, -11.279924, -15.095373, 3.253518, -11.747156, -14.457899, 9.174353, -11.205919, -13.650103, 9.174353, -11.054691, -14.457899, 9.174353, -11.205919, -13.840971, 15.365146, -11.279924, -13.650103, 9.174353, -11.054691, -15.095373, 3.253518, -11.747156, -13.650103, 9.174353, -11.054691, -12.860012, 3.253518, -11.328674, -13.650103, 9.174353, -11.054691, -13.840971, 15.365146, -11.279924, -13.667648, 15.401014, -11.597029, -12.860012, 3.253518, -11.328674, -13.650103, 9.174353, -11.054691, -13.258715, 9.314051, -11.763752, -13.650103, 9.174353, -11.054691, -13.667648, 15.401014, -11.597029, -13.258715, 9.314051, -11.763752, -13.258715, 9.314051, -11.763752, -13.667648, 15.401014, -11.597029, -13.917479, 15.423668, -11.859549, -12.860012, 3.253518, -11.328674, -13.258715, 9.314051, -11.763752, -11.794676, 3.2541475, -13.20286, -13.258715, 9.314051, -11.763752, -13.917479, 15.423668, -11.859549, -13.825075, 9.400261, -12.353134, -11.794676, 3.2541475, -13.20286, -13.258715, 9.314051, -11.763752, -13.825075, 9.400261, -12.353134, -11.794676, 3.2541475, -13.20286, -13.825075, 9.400261, -12.353134, -13.370928, 3.2541475, -14.779001, -13.45945, 9.649454, -11.878155, -10.93936, 10.304527, -11.770006, -13.28483, 9.328525, -11.429331, -13.45945, 9.649454, -11.878155, -11.192169, 10.561271, -12.184172, -10.93936, 10.304527, -11.770006, -11.192169, 10.561271, -12.184172, -8.654425, 12.791416, -12.396021, -10.93936, 10.304527, -11.770006, -11.192169, 10.561271, -12.184172, -8.764704, 12.790787, -12.511417, -8.654425, 12.791416, -12.396021, -10.93936, 10.304527, -11.770006, -8.654425, 12.791416, -12.396021, -8.510876, 12.792675, -12.465178, -13.28483, 9.328525, -11.429331, -10.93936, 10.304527, -11.770006, -10.648958, 9.889207, -11.980684, -10.93936, 10.304527, -11.770006, -8.510876, 12.792675, -12.465178, -10.648958, 9.889207, -11.980684, -13.28483, 9.328525, -11.429331, -10.648958, 9.889207, -11.980684, -13.126629, 8.809374, -11.63147, -10.648958, 9.889207, -11.980684, -8.510876, 12.792675, -12.465178, -8.53195, 12.792675, -12.622772, -13.126629, 8.809374, -11.63147, -10.648958, 9.889207, -11.980684, -10.721937, 9.889207, -12.524437, -10.648958, 9.889207, -11.980684, -8.53195, 12.792675, -12.622772, -10.721937, 9.889207, -12.524437, -13.126629, 8.809374, -11.63147, -10.721937, 9.889207, -12.524437, -13.203676, 8.809374, -12.204792, -10.721937, 9.889207, -12.524437, -8.53195, 12.792675, -12.622772, -8.688553, 12.791416, -12.651451, -13.203676, 8.809374, -12.204792, -10.721937, 9.889207, -12.524437, -11.058203, 10.304527, -12.650129, -10.721937, 9.889207, -12.524437, -8.688553, 12.791416, -12.651451, -11.058203, 10.304527, -12.650129, -11.058203, 10.304527, -12.650129, -8.688553, 12.791416, -12.651451, -8.764704, 12.790787, -12.511417, -13.203676, 8.809374, -12.204792, -11.058203, 10.304527, -12.650129, -13.409447, 9.328525, -12.357271, -11.058203, 10.304527, -12.650129, -8.764704, 12.790787, -12.511417, -11.192169, 10.561271, -12.184172, -13.409447, 9.328525, -12.357271, -11.058203, 10.304527, -12.650129, -11.192169, 10.561271, -12.184172, -13.409447, 9.328525, -12.357271, -11.192169, 10.561271, -12.184172, -13.45945, 9.649454, -11.878155, 1.2086613, -17.361609, 9.414333, 2.631641, -16.33421, 10.284525, 2.5310369, -17.7495, 10.064947, 2.5310369, -17.7495, 10.064947, 2.631641, -16.33421, 10.284525, 4.3560047, -16.298113, 9.437187, 2.5310369, -17.7495, 10.064947, 4.3560047, -16.298113, 9.437187, 4.0482125, -17.380833, 9.267164, 1.2086613, -17.361609, 9.414333, 0.99485135, -16.35621, 9.910944, 2.631641, -16.33421, 10.284525, 0.67701983, -17.65146, 7.644686, 0.99485135, -16.35621, 9.910944, 1.2086613, -17.361609, 9.414333, 2.631641, -16.33421, 10.284525, 3.8578982, -15.259179, 8.952537, 4.3560047, -16.298113, 9.437187, 0.67701983, -17.65146, 7.644686, 0.57171667, -16.255608, 7.7713485, 0.99485135, -16.35621, 9.910944, 1.4848416, -17.258871, 5.9989257, 0.57171667, -16.255608, 7.7713485, 0.67701983, -17.65146, 7.644686, 0.99485135, -16.35621, 9.910944, 2.820033, -14.904823, 9.283184, 2.631641, -16.33421, 10.284525, 2.631641, -16.33421, 10.284525, 2.820033, -14.904823, 9.283184, 3.8578982, -15.259179, 8.952537, 0.57171667, -16.255608, 7.7713485, 1.7232151, -15.285238, 9.163143, 0.99485135, -16.35621, 9.910944, 0.99485135, -16.35621, 9.910944, 1.7232151, -15.285238, 9.163143, 2.820033, -14.904823, 9.283184, 1.4848416, -17.258871, 5.9989257, 1.2661188, -16.180422, 5.6674232, 0.57171667, -16.255608, 7.7713485, 1.2661188, -16.180422, 5.6674232, 1.4848416, -17.258871, 5.9989257, 2.9689097, -17.679869, 5.4027777, 0.57171667, -16.255608, 7.7713485, 1.5839503, -14.885172, 7.933895, 1.7232151, -15.285238, 9.163143, 1.2661188, -16.180422, 5.6674232, 1.5839503, -14.885172, 7.933895, 0.57171667, -16.255608, 7.7713485, 1.7232151, -15.285238, 9.163143, 2.8828304, -14.428289, 7.988148, 2.820033, -14.904823, 9.283184, 1.7232151, -15.285238, 9.163143, 1.5839503, -14.885172, 7.933895, 2.8828304, -14.428289, 7.988148, 1.2661188, -16.180422, 5.6674232, 1.8436836, -15.206849, 6.6362967, 1.5839503, -14.885172, 7.933895, 1.2661188, -16.180422, 5.6674232, 2.9689097, -17.679869, 5.4027777, 2.9821527, -16.278463, 5.4012823, 1.8436836, -15.206849, 6.6362967, 1.2661188, -16.180422, 5.6674232, 2.9821527, -16.278463, 5.4012823, 2.9821527, -16.278463, 5.4012823, 2.9689097, -17.679869, 5.4027777, 4.3171306, -17.397068, 6.368234, 1.5839503, -14.885172, 7.933895, 1.8436836, -15.206849, 6.6362967, 2.9076076, -14.890726, 6.620277, 1.8436836, -15.206849, 6.6362967, 2.9821527, -16.278463, 5.4012823, 2.9076076, -14.890726, 6.620277, 1.5839503, -14.885172, 7.933895, 2.9076076, -14.890726, 6.620277, 2.8828304, -14.428289, 7.988148, 2.9821527, -16.278463, 5.4012823, 4.3171306, -17.397068, 6.368234, 4.6103983, -16.390173, 6.355845, 2.9076076, -14.890726, 6.620277, 2.9821527, -16.278463, 5.4012823, 4.6103983, -16.390173, 6.355845, 4.8227134, -17.777908, 7.823253, 4.6103983, -16.390173, 6.355845, 4.3171306, -17.397068, 6.368234, 2.8828304, -14.428289, 7.988148, 2.9076076, -14.890726, 6.620277, 3.9711041, -15.30019, 6.942167, 2.9076076, -14.890726, 6.620277, 4.6103983, -16.390173, 6.355845, 3.9711041, -15.30019, 6.942167, 4.8227134, -17.777908, 7.823253, 5.042077, -16.356638, 7.9142447, 4.6103983, -16.390173, 6.355845, 5.042077, -16.356638, 7.9142447, 3.9711041, -15.30019, 6.942167, 4.6103983, -16.390173, 6.355845, 4.0482125, -17.380833, 9.267164, 5.042077, -16.356638, 7.9142447, 4.8227134, -17.777908, 7.823253, 4.0482125, -17.380833, 9.267164, 4.3560047, -16.298113, 9.437187, 5.042077, -16.356638, 7.9142447, 5.042077, -16.356638, 7.9142447, 4.1434765, -14.910377, 7.969779, 3.9711041, -15.30019, 6.942167, 2.8828304, -14.428289, 7.988148, 3.9711041, -15.30019, 6.942167, 4.1434765, -14.910377, 7.969779, 4.3560047, -16.298113, 9.437187, 4.1434765, -14.910377, 7.969779, 5.042077, -16.356638, 7.9142447, 2.820033, -14.904823, 9.283184, 2.8828304, -14.428289, 7.988148, 4.1434765, -14.910377, 7.969779, 4.3560047, -16.298113, 9.437187, 3.8578982, -15.259179, 8.952537, 4.1434765, -14.910377, 7.969779, 2.820033, -14.904823, 9.283184, 4.1434765, -14.910377, 7.969779, 3.8578982, -15.259179, 8.952537, 1.4848416, -17.258871, 5.9989257, 2.7309632, -18.18417, 7.6976585, 2.9689097, -17.679869, 5.4027777, 1.4848416, -17.258871, 5.9989257, 0.67701983, -17.65146, 7.644686, 2.7309632, -18.18417, 7.6976585, 2.9689097, -17.679869, 5.4027777, 2.7309632, -18.18417, 7.6976585, 4.8227134, -17.777908, 7.823253, 2.9689097, -17.679869, 5.4027777, 4.8227134, -17.777908, 7.823253, 4.3171306, -17.397068, 6.368234, 2.7309632, -18.18417, 7.6976585, 4.0482125, -17.380833, 9.267164, 4.8227134, -17.777908, 7.823253, 2.7309632, -18.18417, 7.6976585, 2.5310369, -17.7495, 10.064947, 4.0482125, -17.380833, 9.267164, 0.67701983, -17.65146, 7.644686, 2.5310369, -17.7495, 10.064947, 2.7309632, -18.18417, 7.6976585, 0.67701983, -17.65146, 7.644686, 1.2086613, -17.361609, 9.414333, 2.5310369, -17.7495, 10.064947, -1.1748617, -18.139956, 8.584938, -0.18462849, -17.407104, 9.311379, -0.3294468, -18.408445, 8.982868, -0.3294468, -18.408445, 8.982868, -0.18462849, -17.407104, 9.311379, 1.0884066, -17.401766, 8.917934, -0.3294468, -18.408445, 8.982868, 1.0884066, -17.401766, 8.917934, 0.70905924, -18.139956, 8.584938, -1.1748617, -18.139956, 8.584938, -1.3850405, -17.401766, 8.917934, -0.18462849, -17.407104, 9.311379, -1.3850405, -18.381105, 7.5489955, -1.3850405, -17.401766, 8.917934, -1.1748617, -18.139956, 8.584938, -0.18462849, -17.407104, 9.311379, 0.7793324, -16.655033, 8.638763, 1.0884066, -17.401766, 8.917934, -1.3850405, -18.381105, 7.5489955, -1.4563816, -17.385319, 7.7371736, -1.3850405, -17.401766, 8.917934, -0.79465985, -18.10578, 6.6108794, -1.4563816, -17.385319, 7.7371736, -1.3850405, -18.381105, 7.5489955, -1.3850405, -17.401766, 8.917934, 0.03259921, -16.395086, 8.853001, -0.18462849, -17.407104, 9.311379, -0.18462849, -17.407104, 9.311379, 0.03259921, -16.395086, 8.853001, 0.7793324, -16.655033, 8.638763, -1.4563816, -17.385319, 7.7371736, -0.79465985, -16.655033, 8.638763, -1.3850405, -17.401766, 8.917934, -1.3850405, -17.401766, 8.917934, -0.79465985, -16.655033, 8.638763, 0.03259921, -16.395086, 8.853001, -0.79465985, -18.10578, 6.6108794, -0.8192234, -17.368872, 6.556626, -1.4563816, -17.385319, 7.7371736, -0.8192234, -17.368872, 6.556626, -0.79465985, -18.10578, 6.6108794, 0.03259921, -18.353552, 6.115336, -1.4563816, -17.385319, 7.7371736, -0.8192234, -16.389532, 7.9253516, -0.79465985, -16.655033, 8.638763, -0.8192234, -17.368872, 6.556626, -0.8192234, -16.389532, 7.9253516, -1.4563816, -17.385319, 7.7371736, -0.79465985, -16.655033, 8.638763, 0.10500836, -16.057604, 7.9881487, 0.03259921, -16.395086, 8.853001, -0.79465985, -16.655033, 8.638763, -0.8192234, -16.389532, 7.9253516, 0.10500836, -16.057604, 7.9881487, -0.8192234, -17.368872, 6.556626, -0.54325676, -16.640295, 7.1145396, -0.8192234, -16.389532, 7.9253516, -0.8192234, -17.368872, 6.556626, 0.03259921, -18.353552, 6.115336, 0.10500836, -17.36332, 6.1631813, -0.54325676, -16.640295, 7.1145396, -0.8192234, -17.368872, 6.556626, 0.10500836, -17.36332, 6.1631813, 0.10500836, -17.36332, 6.1631813, 0.03259921, -18.353552, 6.115336, 0.7793324, -18.10578, 6.6108794, -0.8192234, -16.389532, 7.9253516, -0.54325676, -16.640295, 7.1145396, 0.10500836, -16.38398, 6.9979157, -0.54325676, -16.640295, 7.1145396, 0.10500836, -17.36332, 6.1631813, 0.10500836, -16.38398, 6.9979157, -0.8192234, -16.389532, 7.9253516, 0.10500836, -16.38398, 6.9979157, 0.10500836, -16.057604, 7.9881487, 0.10500836, -17.36332, 6.1631813, 0.7793324, -18.10578, 6.6108794, 0.9570446, -17.368872, 6.556626, 0.10500836, -16.38398, 6.9979157, 0.10500836, -17.36332, 6.1631813, 0.9570446, -17.368872, 6.556626, 1.0884066, -18.381105, 7.5489955, 0.9570446, -17.368872, 6.556626, 0.7793324, -18.10578, 6.6108794, 0.10500836, -16.057604, 7.9881487, 0.10500836, -16.38398, 6.9979157, 0.7210206, -16.640295, 7.1145396, 0.10500836, -16.38398, 6.9979157, 0.9570446, -17.368872, 6.556626, 0.7210206, -16.640295, 7.1145396, 1.0884066, -18.381105, 7.5489955, 1.3767617, -17.385319, 7.7371736, 0.9570446, -17.368872, 6.556626, 1.3767617, -17.385319, 7.7371736, 0.7210206, -16.640295, 7.1145396, 0.9570446, -17.368872, 6.556626, 0.70905924, -18.139956, 8.584938, 1.3767617, -17.385319, 7.7371736, 1.0884066, -18.381105, 7.5489955, 0.70905924, -18.139956, 8.584938, 1.0884066, -17.401766, 8.917934, 1.3767617, -17.385319, 7.7371736, 1.3767617, -17.385319, 7.7371736, 0.9570446, -16.389532, 7.9253516, 0.7210206, -16.640295, 7.1145396, 0.10500836, -16.057604, 7.9881487, 0.7210206, -16.640295, 7.1145396, 0.9570446, -16.389532, 7.9253516, 1.0884066, -17.401766, 8.917934, 0.9570446, -16.389532, 7.9253516, 1.3767617, -17.385319, 7.7371736, 0.03259921, -16.395086, 8.853001, 0.10500836, -16.057604, 7.9881487, 0.9570446, -16.389532, 7.9253516, 1.0884066, -17.401766, 8.917934, 0.7793324, -16.655033, 8.638763, 0.9570446, -16.389532, 7.9253516, 0.03259921, -16.395086, 8.853001, 0.9570446, -16.389532, 7.9253516, 0.7793324, -16.655033, 8.638763, -0.79465985, -18.10578, 6.6108794, -0.18462849, -18.712822, 7.4864116, 0.03259921, -18.353552, 6.115336, -0.79465985, -18.10578, 6.6108794, -1.3850405, -18.381105, 7.5489955, -0.18462849, -18.712822, 7.4864116, 0.03259921, -18.353552, 6.115336, -0.18462849, -18.712822, 7.4864116, 1.0884066, -18.381105, 7.5489955, 0.03259921, -18.353552, 6.115336, 1.0884066, -18.381105, 7.5489955, 0.7793324, -18.10578, 6.6108794, -0.18462849, -18.712822, 7.4864116, 0.70905924, -18.139956, 8.584938, 1.0884066, -18.381105, 7.5489955, -0.18462849, -18.712822, 7.4864116, -0.3294468, -18.408445, 8.982868, 0.70905924, -18.139956, 8.584938, -1.3850405, -18.381105, 7.5489955, -0.3294468, -18.408445, 8.982868, -0.18462849, -18.712822, 7.4864116, -1.3850405, -18.381105, 7.5489955, -1.1748617, -18.139956, 8.584938, -0.3294468, -18.408445, 8.982868, 2.6649618, -19.729753, 7.98815, 1.5668626, -19.439903, 7.791855, 2.6147668, -19.871582, 7.781175, 2.6649618, -19.729753, 7.98815, 1.6531556, -19.32627, 7.98815, 1.5668626, -19.439903, 7.791855, 1.6531556, -19.32627, 7.98815, 0.52835655, -18.340096, 7.9311194, 1.5668626, -19.439903, 7.791855, 1.6531556, -19.32627, 7.98815, 0.5697943, -18.34031, 7.9881496, 0.52835655, -18.340096, 7.9311194, 1.5668626, -19.439903, 7.791855, 0.52835655, -18.340096, 7.9311194, 0.46128738, -18.339455, 7.952906, 2.6147668, -19.871582, 7.781175, 1.5668626, -19.439903, 7.791855, 1.4271705, -19.62381, 7.8668275, 1.5668626, -19.439903, 7.791855, 0.46128738, -18.339455, 7.952906, 1.4271705, -19.62381, 7.8668275, 2.6147668, -19.871582, 7.781175, 1.4271705, -19.62381, 7.8668275, 2.5338137, -20.101198, 7.860206, 1.4271705, -19.62381, 7.8668275, 0.46128738, -18.339455, 7.952906, 0.46128738, -18.339455, 8.023394, 2.5338137, -20.101198, 7.860206, 1.4271705, -19.62381, 7.8668275, 1.4271705, -19.62381, 8.109472, 1.4271705, -19.62381, 7.8668275, 0.46128738, -18.339455, 8.023394, 1.4271705, -19.62381, 8.109472, 2.5338137, -20.101198, 7.860206, 1.4271705, -19.62381, 8.109472, 2.5338137, -20.101198, 8.116095, 1.4271705, -19.62381, 8.109472, 0.46128738, -18.339455, 8.023394, 0.52835655, -18.340096, 8.045179, 2.5338137, -20.101198, 8.116095, 1.4271705, -19.62381, 8.109472, 1.5668626, -19.439903, 8.184445, 1.4271705, -19.62381, 8.109472, 0.52835655, -18.340096, 8.045179, 1.5668626, -19.439903, 8.184445, 1.5668626, -19.439903, 8.184445, 0.52835655, -18.340096, 8.045179, 0.5697943, -18.34031, 7.9881496, 2.5338137, -20.101198, 8.116095, 1.5668626, -19.439903, 8.184445, 2.6147668, -19.871582, 8.195126, 1.5668626, -19.439903, 8.184445, 0.5697943, -18.34031, 7.9881496, 1.6531556, -19.32627, 7.98815, 2.6147668, -19.871582, 8.195126, 1.5668626, -19.439903, 8.184445, 1.6531556, -19.32627, 7.98815, 2.6147668, -19.871582, 8.195126, 1.6531556, -19.32627, 7.98815, 2.6649618, -19.729753, 7.98815, 1.5335416, -22.031042, 7.988151, 2.5325322, -19.973894, 8.213709, 1.9611616, -22.031042, 8.6124935, 1.5335416, -22.031042, 7.988151, 2.3712668, -19.944632, 7.98815, 2.5325322, -19.973894, 8.213709, 2.3712668, -19.944632, 7.98815, 2.613699, -17.907776, 8.087899, 2.5325322, -19.973894, 8.213709, 2.3712668, -19.944632, 7.98815, 2.5417168, -17.900085, 7.9881496, 2.613699, -17.907776, 8.087899, 2.5325322, -19.973894, 8.213709, 2.613699, -17.907776, 8.087899, 2.730536, -17.920164, 8.049879, 1.9611616, -22.031042, 8.6124935, 2.5325322, -19.973894, 8.213709, 2.7935472, -20.021313, 8.127628, 2.5325322, -19.973894, 8.213709, 2.730536, -17.920164, 8.049879, 2.7935472, -20.021313, 8.127628, 1.9611616, -22.031042, 8.6124935, 2.7935472, -20.021313, 8.127628, 2.652787, -22.031042, 8.373906, 2.7935472, -20.021313, 8.127628, 2.730536, -17.920164, 8.049879, 2.730536, -17.920164, 7.92642, 2.652787, -22.031042, 8.373906, 2.7935472, -20.021313, 8.127628, 2.7935472, -20.021313, 7.848672, 2.7935472, -20.021313, 8.127628, 2.730536, -17.920164, 7.92642, 2.7935472, -20.021313, 7.848672, 2.652787, -22.031042, 8.373906, 2.7935472, -20.021313, 7.848672, 2.652787, -22.031042, 7.6021824, 2.7935472, -20.021313, 7.848672, 2.730536, -17.920164, 7.92642, 2.613699, -17.907776, 7.8884, 2.652787, -22.031042, 7.6021824, 2.7935472, -20.021313, 7.848672, 2.5325322, -19.973894, 7.7623787, 2.7935472, -20.021313, 7.848672, 2.613699, -17.907776, 7.8884, 2.5325322, -19.973894, 7.7623787, 2.5325322, -19.973894, 7.7623787, 2.613699, -17.907776, 7.8884, 2.5417168, -17.900085, 7.9881496, 2.652787, -22.031042, 7.6021824, 2.5325322, -19.973894, 7.7623787, 1.9611616, -22.031042, 7.3638086, 2.5325322, -19.973894, 7.7623787, 2.5417168, -17.900085, 7.9881496, 2.3712668, -19.944632, 7.98815, 1.9611616, -22.031042, 7.3638086, 2.5325322, -19.973894, 7.7623787, 2.3712668, -19.944632, 7.98815, 1.9611616, -22.031042, 7.3638086, 2.3712668, -19.944632, 7.98815, 1.5335416, -22.031042, 7.988151, 7.7761884, 7.8527765, 15.352116, 9.199167, 8.88039, 16.222307, 9.098564, 7.4648857, 16.002731, 9.098564, 7.4648857, 16.002731, 9.199167, 8.88039, 16.222307, 10.9235325, 8.916273, 15.3749695, 9.098564, 7.4648857, 16.002731, 10.9235325, 8.916273, 15.3749695, 10.61574, 7.8335533, 15.204948, 7.7761884, 7.8527765, 15.352116, 7.562378, 8.858175, 15.848726, 9.199167, 8.88039, 16.222307, 7.2445464, 7.562926, 13.58247, 7.562378, 8.858175, 15.848726, 7.7761884, 7.8527765, 15.352116, 9.199167, 8.88039, 16.222307, 10.425425, 9.955207, 14.89032, 10.9235325, 8.916273, 15.3749695, 7.2445464, 7.562926, 13.58247, 7.1392436, 8.958778, 13.709131, 7.562378, 8.858175, 15.848726, 8.052368, 7.9555154, 11.936708, 7.1392436, 8.958778, 13.709131, 7.2445464, 7.562926, 13.58247, 7.562378, 8.858175, 15.848726, 9.38756, 10.309776, 15.220966, 9.199167, 8.88039, 16.222307, 9.199167, 8.88039, 16.222307, 9.38756, 10.309776, 15.220966, 10.425425, 9.955207, 14.89032, 7.1392436, 8.958778, 13.709131, 8.290742, 9.929361, 15.100925, 7.562378, 8.858175, 15.848726, 7.562378, 8.858175, 15.848726, 8.290742, 9.929361, 15.100925, 9.38756, 10.309776, 15.220966, 8.052368, 7.9555154, 11.936708, 7.8336453, 9.033964, 11.605206, 7.1392436, 8.958778, 13.709131, 7.8336453, 9.033964, 11.605206, 8.052368, 7.9555154, 11.936708, 9.536436, 7.534303, 11.340561, 7.1392436, 8.958778, 13.709131, 8.151478, 10.329214, 13.871677, 8.290742, 9.929361, 15.100925, 7.8336453, 9.033964, 11.605206, 8.151478, 10.329214, 13.871677, 7.1392436, 8.958778, 13.709131, 8.290742, 9.929361, 15.100925, 9.450357, 10.786097, 13.925932, 9.38756, 10.309776, 15.220966, 8.290742, 9.929361, 15.100925, 8.151478, 10.329214, 13.871677, 9.450357, 10.786097, 13.925932, 7.8336453, 9.033964, 11.605206, 8.411211, 10.007323, 12.5740795, 8.151478, 10.329214, 13.871677, 7.8336453, 9.033964, 11.605206, 9.536436, 7.534303, 11.340561, 9.549679, 8.935923, 11.339066, 8.411211, 10.007323, 12.5740795, 7.8336453, 9.033964, 11.605206, 9.549679, 8.935923, 11.339066, 9.549679, 8.935923, 11.339066, 9.536436, 7.534303, 11.340561, 10.884657, 7.817319, 12.306017, 8.151478, 10.329214, 13.871677, 8.411211, 10.007323, 12.5740795, 9.475134, 10.32366, 12.55806, 8.411211, 10.007323, 12.5740795, 9.549679, 8.935923, 11.339066, 9.475134, 10.32366, 12.55806, 8.151478, 10.329214, 13.871677, 9.475134, 10.32366, 12.55806, 9.450357, 10.786097, 13.925932, 9.549679, 8.935923, 11.339066, 10.884657, 7.817319, 12.306017, 11.177925, 8.823998, 12.293627, 9.475134, 10.32366, 12.55806, 9.549679, 8.935923, 11.339066, 11.177925, 8.823998, 12.293627, 11.39024, 7.4364767, 13.761036, 11.177925, 8.823998, 12.293627, 10.884657, 7.817319, 12.306017, 9.450357, 10.786097, 13.925932, 9.475134, 10.32366, 12.55806, 10.538631, 9.914196, 12.87995, 9.475134, 10.32366, 12.55806, 11.177925, 8.823998, 12.293627, 10.538631, 9.914196, 12.87995, 11.39024, 7.4364767, 13.761036, 11.609604, 8.857534, 13.852028, 11.177925, 8.823998, 12.293627, 11.609604, 8.857534, 13.852028, 10.538631, 9.914196, 12.87995, 11.177925, 8.823998, 12.293627, 10.61574, 7.8335533, 15.204948, 11.609604, 8.857534, 13.852028, 11.39024, 7.4364767, 13.761036, 10.61574, 7.8335533, 15.204948, 10.9235325, 8.916273, 15.3749695, 11.609604, 8.857534, 13.852028, 11.609604, 8.857534, 13.852028, 10.711004, 10.304009, 13.907562, 10.538631, 9.914196, 12.87995, 9.450357, 10.786097, 13.925932, 10.538631, 9.914196, 12.87995, 10.711004, 10.304009, 13.907562, 10.9235325, 8.916273, 15.3749695, 10.711004, 10.304009, 13.907562, 11.609604, 8.857534, 13.852028, 9.38756, 10.309776, 15.220966, 9.450357, 10.786097, 13.925932, 10.711004, 10.304009, 13.907562, 10.9235325, 8.916273, 15.3749695, 10.425425, 9.955207, 14.89032, 10.711004, 10.304009, 13.907562, 9.38756, 10.309776, 15.220966, 10.711004, 10.304009, 13.907562, 10.425425, 9.955207, 14.89032, 8.052368, 7.9555154, 11.936708, 9.298491, 7.030216, 13.635441, 9.536436, 7.534303, 11.340561, 8.052368, 7.9555154, 11.936708, 7.2445464, 7.562926, 13.58247, 9.298491, 7.030216, 13.635441, 9.536436, 7.534303, 11.340561, 9.298491, 7.030216, 13.635441, 11.39024, 7.4364767, 13.761036, 9.536436, 7.534303, 11.340561, 11.39024, 7.4364767, 13.761036, 10.884657, 7.817319, 12.306017, 9.298491, 7.030216, 13.635441, 10.61574, 7.8335533, 15.204948, 11.39024, 7.4364767, 13.761036, 9.298491, 7.030216, 13.635441, 9.098564, 7.4648857, 16.002731, 10.61574, 7.8335533, 15.204948, 7.2445464, 7.562926, 13.58247, 9.098564, 7.4648857, 16.002731, 9.298491, 7.030216, 13.635441, 7.2445464, 7.562926, 13.58247, 7.7761884, 7.8527765, 15.352116, 9.098564, 7.4648857, 16.002731, 5.392665, 7.074431, 14.52272, 6.3828983, 7.807067, 15.249163, 6.23808, 6.805727, 14.9206505, 6.23808, 6.805727, 14.9206505, 6.3828983, 7.807067, 15.249163, 7.6559334, 7.8126206, 14.855719, 6.23808, 6.805727, 14.9206505, 7.6559334, 7.8126206, 14.855719, 7.276586, 7.074431, 14.52272, 5.392665, 7.074431, 14.52272, 5.1824865, 7.8126206, 14.855719, 6.3828983, 7.807067, 15.249163, 5.1824865, 6.83328, 13.486778, 5.1824865, 7.8126206, 14.855719, 5.392665, 7.074431, 14.52272, 6.3828983, 7.807067, 15.249163, 7.3468595, 8.559354, 14.576548, 7.6559334, 7.8126206, 14.855719, 5.1824865, 6.83328, 13.486778, 5.111145, 7.829067, 13.674955, 5.1824865, 7.8126206, 14.855719, 5.772867, 7.108606, 12.548662, 5.111145, 7.829067, 13.674955, 5.1824865, 6.83328, 13.486778, 5.1824865, 7.8126206, 14.855719, 6.600126, 8.819301, 14.790783, 6.3828983, 7.807067, 15.249163, 6.3828983, 7.807067, 15.249163, 6.600126, 8.819301, 14.790783, 7.3468595, 8.559354, 14.576548, 5.111145, 7.829067, 13.674955, 5.772867, 8.559354, 14.576548, 5.1824865, 7.8126206, 14.855719, 5.1824865, 7.8126206, 14.855719, 5.772867, 8.559354, 14.576548, 6.600126, 8.819301, 14.790783, 5.772867, 7.108606, 12.548662, 5.7483034, 7.8455133, 12.49441, 5.111145, 7.829067, 13.674955, 5.7483034, 7.8455133, 12.49441, 5.772867, 7.108606, 12.548662, 6.600126, 6.8608336, 12.05312, 5.111145, 7.829067, 13.674955, 5.7483034, 8.824854, 13.863134, 5.772867, 8.559354, 14.576548, 5.7483034, 7.8455133, 12.49441, 5.7483034, 8.824854, 13.863134, 5.111145, 7.829067, 13.674955, 5.772867, 8.559354, 14.576548, 6.6725354, 9.156782, 13.925932, 6.600126, 8.819301, 14.790783, 5.772867, 8.559354, 14.576548, 5.7483034, 8.824854, 13.863134, 6.6725354, 9.156782, 13.925932, 5.7483034, 7.8455133, 12.49441, 6.02427, 8.574091, 13.052322, 5.7483034, 8.824854, 13.863134, 5.7483034, 7.8455133, 12.49441, 6.600126, 6.8608336, 12.05312, 6.6725354, 7.8510666, 12.100964, 6.02427, 8.574091, 13.052322, 5.7483034, 7.8455133, 12.49441, 6.6725354, 7.8510666, 12.100964, 6.6725354, 7.8510666, 12.100964, 6.600126, 6.8608336, 12.05312, 7.3468595, 7.108606, 12.548662, 5.7483034, 8.824854, 13.863134, 6.02427, 8.574091, 13.052322, 6.6725354, 8.830407, 12.935699, 6.02427, 8.574091, 13.052322, 6.6725354, 7.8510666, 12.100964, 6.6725354, 8.830407, 12.935699, 5.7483034, 8.824854, 13.863134, 6.6725354, 8.830407, 12.935699, 6.6725354, 9.156782, 13.925932, 6.6725354, 7.8510666, 12.100964, 7.3468595, 7.108606, 12.548662, 7.5245714, 7.8455133, 12.49441, 6.6725354, 8.830407, 12.935699, 6.6725354, 7.8510666, 12.100964, 7.5245714, 7.8455133, 12.49441, 7.6559334, 6.83328, 13.486778, 7.5245714, 7.8455133, 12.49441, 7.3468595, 7.108606, 12.548662, 6.6725354, 9.156782, 13.925932, 6.6725354, 8.830407, 12.935699, 7.2885475, 8.574091, 13.052322, 6.6725354, 8.830407, 12.935699, 7.5245714, 7.8455133, 12.49441, 7.2885475, 8.574091, 13.052322, 7.6559334, 6.83328, 13.486778, 7.9442887, 7.829067, 13.674955, 7.5245714, 7.8455133, 12.49441, 7.9442887, 7.829067, 13.674955, 7.2885475, 8.574091, 13.052322, 7.5245714, 7.8455133, 12.49441, 7.276586, 7.074431, 14.52272, 7.9442887, 7.829067, 13.674955, 7.6559334, 6.83328, 13.486778, 7.276586, 7.074431, 14.52272, 7.6559334, 7.8126206, 14.855719, 7.9442887, 7.829067, 13.674955, 7.9442887, 7.829067, 13.674955, 7.5245714, 8.824854, 13.863134, 7.2885475, 8.574091, 13.052322, 6.6725354, 9.156782, 13.925932, 7.2885475, 8.574091, 13.052322, 7.5245714, 8.824854, 13.863134, 7.6559334, 7.8126206, 14.855719, 7.5245714, 8.824854, 13.863134, 7.9442887, 7.829067, 13.674955, 6.600126, 8.819301, 14.790783, 6.6725354, 9.156782, 13.925932, 7.5245714, 8.824854, 13.863134, 7.6559334, 7.8126206, 14.855719, 7.3468595, 8.559354, 14.576548, 7.5245714, 8.824854, 13.863134, 6.600126, 8.819301, 14.790783, 7.5245714, 8.824854, 13.863134, 7.3468595, 8.559354, 14.576548, 5.772867, 7.108606, 12.548662, 6.3828983, 6.5013514, 13.424194, 6.600126, 6.8608336, 12.05312, 5.772867, 7.108606, 12.548662, 5.1824865, 6.83328, 13.486778, 6.3828983, 6.5013514, 13.424194, 6.600126, 6.8608336, 12.05312, 6.3828983, 6.5013514, 13.424194, 7.6559334, 6.83328, 13.486778, 6.600126, 6.8608336, 12.05312, 7.6559334, 6.83328, 13.486778, 7.3468595, 7.108606, 12.548662, 6.3828983, 6.5013514, 13.424194, 7.276586, 7.074431, 14.52272, 7.6559334, 6.83328, 13.486778, 6.3828983, 6.5013514, 13.424194, 6.23808, 6.805727, 14.9206505, 7.276586, 7.074431, 14.52272, 5.1824865, 6.83328, 13.486778, 6.23808, 6.805727, 14.9206505, 6.3828983, 6.5013514, 13.424194, 5.1824865, 6.83328, 13.486778, 5.392665, 7.074431, 14.52272, 6.23808, 6.805727, 14.9206505, 9.232489, 5.4846325, 13.925934, 8.13439, 5.7744827, 13.729637, 9.182293, 5.342804, 13.718958, 9.232489, 5.4846325, 13.925934, 8.220682, 5.8879023, 13.925932, 8.13439, 5.7744827, 13.729637, 8.220682, 5.8879023, 13.925932, 7.0958834, 6.874291, 13.868902, 8.13439, 5.7744827, 13.729637, 8.220682, 5.8879023, 13.925932, 7.137321, 6.8738637, 13.925932, 7.0958834, 6.874291, 13.868902, 8.13439, 5.7744827, 13.729637, 7.0958834, 6.874291, 13.868902, 7.0288143, 6.874932, 13.89069, 9.182293, 5.342804, 13.718958, 8.13439, 5.7744827, 13.729637, 7.994697, 5.590576, 13.804609, 8.13439, 5.7744827, 13.729637, 7.0288143, 6.874932, 13.89069, 7.994697, 5.590576, 13.804609, 9.182293, 5.342804, 13.718958, 7.994697, 5.590576, 13.804609, 9.101341, 5.1134014, 13.797989, 7.994697, 5.590576, 13.804609, 7.0288143, 6.874932, 13.89069, 7.0288143, 6.874932, 13.961176, 9.101341, 5.1134014, 13.797989, 7.994697, 5.590576, 13.804609, 7.994697, 5.590576, 14.047256, 7.994697, 5.590576, 13.804609, 7.0288143, 6.874932, 13.961176, 7.994697, 5.590576, 14.047256, 9.101341, 5.1134014, 13.797989, 7.994697, 5.590576, 14.047256, 9.101341, 5.113402, 14.053877, 7.994697, 5.590576, 14.047256, 7.0288143, 6.874932, 13.961176, 7.0958834, 6.874291, 13.982964, 9.101341, 5.113402, 14.053877, 7.994697, 5.590576, 14.047256, 8.13439, 5.7744827, 14.122229, 7.994697, 5.590576, 14.047256, 7.0958834, 6.874291, 13.982964, 8.13439, 5.7744827, 14.122229, 8.13439, 5.7744827, 14.122229, 7.0958834, 6.874291, 13.982964, 7.137321, 6.8738637, 13.925932, 9.101341, 5.113402, 14.053877, 8.13439, 5.7744827, 14.122229, 9.182293, 5.3428044, 14.132908, 8.13439, 5.7744827, 14.122229, 7.137321, 6.8738637, 13.925932, 8.220682, 5.8879023, 13.925932, 9.182293, 5.3428044, 14.132908, 8.13439, 5.7744827, 14.122229, 8.220682, 5.8879023, 13.925932, 9.182293, 5.3428044, 14.132908, 8.220682, 5.8879023, 13.925932, 9.232489, 5.4846325, 13.925934, 8.1010685, 3.1833441, 13.925934, 9.1000595, 5.240492, 14.151705, 8.528688, 3.1833444, 14.550276, 8.1010685, 3.1833441, 13.925934, 8.938793, 5.2697544, 13.925934, 9.1000595, 5.240492, 14.151705, 8.938793, 5.2697544, 13.925934, 9.181226, 7.30661, 14.025682, 9.1000595, 5.240492, 14.151705, 8.938793, 5.2697544, 13.925934, 9.109243, 7.314086, 13.925932, 9.181226, 7.30661, 14.025682, 9.1000595, 5.240492, 14.151705, 9.181226, 7.30661, 14.025682, 9.298063, 7.2942214, 13.987661, 8.528688, 3.1833444, 14.550276, 9.1000595, 5.240492, 14.151705, 9.361288, 5.1928596, 14.0654125, 9.1000595, 5.240492, 14.151705, 9.298063, 7.2942214, 13.987661, 9.361288, 5.1928596, 14.0654125, 8.528688, 3.1833444, 14.550276, 9.361288, 5.1928596, 14.0654125, 9.220314, 3.1833444, 14.311903, 9.361288, 5.1928596, 14.0654125, 9.298063, 7.2942214, 13.987661, 9.298063, 7.2942214, 13.8642025, 9.220314, 3.1833444, 14.311903, 9.361288, 5.1928596, 14.0654125, 9.361074, 5.1928596, 13.786455, 9.361288, 5.1928596, 14.0654125, 9.298063, 7.2942214, 13.8642025, 9.361074, 5.1928596, 13.786455, 9.220314, 3.1833444, 14.311903, 9.361074, 5.1928596, 13.786455, 9.220314, 3.1833441, 13.539965, 9.361074, 5.1928596, 13.786455, 9.298063, 7.2942214, 13.8642025, 9.181226, 7.30661, 13.826183, 9.220314, 3.1833441, 13.539965, 9.361074, 5.1928596, 13.786455, 9.1000595, 5.240492, 13.700161, 9.361074, 5.1928596, 13.786455, 9.181226, 7.30661, 13.826183, 9.1000595, 5.240492, 13.700161, 9.1000595, 5.240492, 13.700161, 9.181226, 7.30661, 13.826183, 9.109243, 7.314086, 13.925932, 9.220314, 3.1833441, 13.539965, 9.1000595, 5.240492, 13.700161, 8.528688, 3.183344, 13.301592, 9.1000595, 5.240492, 13.700161, 9.109243, 7.314086, 13.925932, 8.938793, 5.2697544, 13.925934, 8.528688, 3.183344, 13.301592, 9.1000595, 5.240492, 13.700161, 8.938793, 5.2697544, 13.925934, 8.528688, 3.183344, 13.301592, 8.938793, 5.2697544, 13.925934, 8.1010685, 3.1833441, 13.925934, -21.010672, 7.8103647, 2.1272998, -19.587692, 8.837978, 2.9974916, -19.688295, 7.422474, 2.7779148, -19.688295, 7.422474, 2.7779148, -19.587692, 8.837978, 2.9974916, -17.863327, 8.873861, 2.1501544, -19.688295, 7.422474, 2.7779148, -17.863327, 8.873861, 2.1501544, -18.17112, 7.7911415, 1.9801317, -21.010672, 7.8103647, 2.1272998, -21.224482, 8.815763, 2.6239111, -19.587692, 8.837978, 2.9974916, -21.542313, 7.520514, 0.35765326, -21.224482, 8.815763, 2.6239111, -21.010672, 7.8103647, 2.1272998, -19.587692, 8.837978, 2.9974916, -18.361435, 9.912795, 1.6655035, -17.863327, 8.873861, 2.1501544, -21.542313, 7.520514, 0.35765326, -21.647615, 8.916367, 0.4843155, -21.224482, 8.815763, 2.6239111, -20.734491, 7.9131036, -1.2881074, -21.647615, 8.916367, 0.4843155, -21.542313, 7.520514, 0.35765326, -21.224482, 8.815763, 2.6239111, -19.3993, 10.2673645, 1.9961507, -19.587692, 8.837978, 2.9974916, -19.587692, 8.837978, 2.9974916, -19.3993, 10.2673645, 1.9961507, -18.361435, 9.912795, 1.6655035, -21.647615, 8.916367, 0.4843155, -20.496117, 9.88695, 1.8761096, -21.224482, 8.815763, 2.6239111, -21.224482, 8.815763, 2.6239111, -20.496117, 9.88695, 1.8761096, -19.3993, 10.2673645, 1.9961507, -20.734491, 7.9131036, -1.2881074, -20.953215, 8.991552, -1.6196096, -21.647615, 8.916367, 0.4843155, -20.953215, 8.991552, -1.6196096, -20.734491, 7.9131036, -1.2881074, -19.250423, 7.4918914, -1.8842552, -21.647615, 8.916367, 0.4843155, -20.635382, 10.286802, 0.6468619, -20.496117, 9.88695, 1.8761096, -20.953215, 8.991552, -1.6196096, -20.635382, 10.286802, 0.6468619, -21.647615, 8.916367, 0.4843155, -20.496117, 9.88695, 1.8761096, -19.336502, 10.743685, 0.70111525, -19.3993, 10.2673645, 1.9961507, -20.496117, 9.88695, 1.8761096, -20.635382, 10.286802, 0.6468619, -19.336502, 10.743685, 0.70111525, -20.953215, 8.991552, -1.6196096, -20.375648, 9.964911, -0.65073633, -20.635382, 10.286802, 0.6468619, -20.953215, 8.991552, -1.6196096, -19.250423, 7.4918914, -1.8842552, -19.23718, 8.893511, -1.885751, -20.375648, 9.964911, -0.65073633, -20.953215, 8.991552, -1.6196096, -19.23718, 8.893511, -1.885751, -19.23718, 8.893511, -1.885751, -19.250423, 7.4918914, -1.8842552, -17.902203, 7.774907, -0.91879916, -20.635382, 10.286802, 0.6468619, -20.375648, 9.964911, -0.65073633, -19.311726, 10.281248, -0.66675615, -20.375648, 9.964911, -0.65073633, -19.23718, 8.893511, -1.885751, -19.311726, 10.281248, -0.66675615, -20.635382, 10.286802, 0.6468619, -19.311726, 10.281248, -0.66675615, -19.336502, 10.743685, 0.70111525, -19.23718, 8.893511, -1.885751, -17.902203, 7.774907, -0.91879916, -17.608934, 8.781587, -0.9311881, -19.311726, 10.281248, -0.66675615, -19.23718, 8.893511, -1.885751, -17.608934, 8.781587, -0.9311881, -17.39662, 7.394065, 0.53621995, -17.608934, 8.781587, -0.9311881, -17.902203, 7.774907, -0.91879916, -19.336502, 10.743685, 0.70111525, -19.311726, 10.281248, -0.66675615, -18.248228, 9.871784, -0.34486628, -19.311726, 10.281248, -0.66675615, -17.608934, 8.781587, -0.9311881, -18.248228, 9.871784, -0.34486628, -17.39662, 7.394065, 0.53621995, -17.177256, 8.815123, 0.6272116, -17.608934, 8.781587, -0.9311881, -17.177256, 8.815123, 0.6272116, -18.248228, 9.871784, -0.34486628, -17.608934, 8.781587, -0.9311881, -18.17112, 7.7911415, 1.9801317, -17.177256, 8.815123, 0.6272116, -17.39662, 7.394065, 0.53621995, -18.17112, 7.7911415, 1.9801317, -17.863327, 8.873861, 2.1501544, -17.177256, 8.815123, 0.6272116, -17.177256, 8.815123, 0.6272116, -18.075855, 10.261598, 0.6827462, -18.248228, 9.871784, -0.34486628, -19.336502, 10.743685, 0.70111525, -18.248228, 9.871784, -0.34486628, -18.075855, 10.261598, 0.6827462, -17.863327, 8.873861, 2.1501544, -18.075855, 10.261598, 0.6827462, -17.177256, 8.815123, 0.6272116, -19.3993, 10.2673645, 1.9961507, -19.336502, 10.743685, 0.70111525, -18.075855, 10.261598, 0.6827462, -17.863327, 8.873861, 2.1501544, -18.361435, 9.912795, 1.6655035, -18.075855, 10.261598, 0.6827462, -19.3993, 10.2673645, 1.9961507, -18.075855, 10.261598, 0.6827462, -18.361435, 9.912795, 1.6655035, -20.734491, 7.9131036, -1.2881074, -19.488369, 6.9878044, 0.41062546, -19.250423, 7.4918914, -1.8842552, -20.734491, 7.9131036, -1.2881074, -21.542313, 7.520514, 0.35765326, -19.488369, 6.9878044, 0.41062546, -19.250423, 7.4918914, -1.8842552, -19.488369, 6.9878044, 0.41062546, -17.39662, 7.394065, 0.53621995, -19.250423, 7.4918914, -1.8842552, -17.39662, 7.394065, 0.53621995, -17.902203, 7.774907, -0.91879916, -19.488369, 6.9878044, 0.41062546, -18.17112, 7.7911415, 1.9801317, -17.39662, 7.394065, 0.53621995, -19.488369, 6.9878044, 0.41062546, -19.688295, 7.422474, 2.7779148, -18.17112, 7.7911415, 1.9801317, -21.542313, 7.520514, 0.35765326, -19.688295, 7.422474, 2.7779148, -19.488369, 6.9878044, 0.41062546, -21.542313, 7.520514, 0.35765326, -21.010672, 7.8103647, 2.1272998, -19.688295, 7.422474, 2.7779148, -23.394194, 7.032019, 1.297905, -22.403961, 7.764655, 2.0243464, -22.54878, 6.763315, 1.6958355, -22.54878, 6.763315, 1.6958355, -22.403961, 7.764655, 2.0243464, -21.130926, 7.770209, 1.6309018, -22.54878, 6.763315, 1.6958355, -21.130926, 7.770209, 1.6309018, -21.510273, 7.032019, 1.297905, -23.394194, 7.032019, 1.297905, -23.604374, 7.770209, 1.6309018, -22.403961, 7.764655, 2.0243464, -23.604374, 6.7908683, 0.2619624, -23.604374, 7.770209, 1.6309018, -23.394194, 7.032019, 1.297905, -22.403961, 7.764655, 2.0243464, -21.44, 8.516942, 1.3517308, -21.130926, 7.770209, 1.6309018, -23.604374, 6.7908683, 0.2619624, -23.675714, 7.7866554, 0.45014048, -23.604374, 7.770209, 1.6309018, -23.013992, 7.066194, -0.6761534, -23.675714, 7.7866554, 0.45014048, -23.604374, 6.7908683, 0.2619624, -23.604374, 7.770209, 1.6309018, -22.186733, 8.776889, 1.5659679, -22.403961, 7.764655, 2.0243464, -22.403961, 7.764655, 2.0243464, -22.186733, 8.776889, 1.5659679, -21.44, 8.516942, 1.3517308, -23.675714, 7.7866554, 0.45014048, -23.013992, 8.516942, 1.3517308, -23.604374, 7.770209, 1.6309018, -23.604374, 7.770209, 1.6309018, -23.013992, 8.516942, 1.3517308, -22.186733, 8.776889, 1.5659679, -23.013992, 7.066194, -0.6761534, -23.038557, 7.8031015, -0.73040724, -23.675714, 7.7866554, 0.45014048, -23.038557, 7.8031015, -0.73040724, -23.013992, 7.066194, -0.6761534, -22.186733, 6.818422, -1.1716971, -23.675714, 7.7866554, 0.45014048, -23.038557, 8.782442, 0.63831854, -23.013992, 8.516942, 1.3517308, -23.038557, 7.8031015, -0.73040724, -23.038557, 8.782442, 0.63831854, -23.675714, 7.7866554, 0.45014048, -23.013992, 8.516942, 1.3517308, -22.114325, 9.11437, 0.70111585, -22.186733, 8.776889, 1.5659679, -23.013992, 8.516942, 1.3517308, -23.038557, 8.782442, 0.63831854, -22.114325, 9.11437, 0.70111585, -23.038557, 7.8031015, -0.73040724, -22.76259, 8.531679, -0.17249346, -23.038557, 8.782442, 0.63831854, -23.038557, 7.8031015, -0.73040724, -22.186733, 6.818422, -1.1716971, -22.114325, 7.808655, -1.1238518, -22.76259, 8.531679, -0.17249346, -23.038557, 7.8031015, -0.73040724, -22.114325, 7.808655, -1.1238518, -22.114325, 7.808655, -1.1238518, -22.186733, 6.818422, -1.1716971, -21.44, 7.066194, -0.6761534, -23.038557, 8.782442, 0.63831854, -22.76259, 8.531679, -0.17249346, -22.114325, 8.787995, -0.28911734, -22.76259, 8.531679, -0.17249346, -22.114325, 7.808655, -1.1238518, -22.114325, 8.787995, -0.28911734, -23.038557, 8.782442, 0.63831854, -22.114325, 8.787995, -0.28911734, -22.114325, 9.11437, 0.70111585, -22.114325, 7.808655, -1.1238518, -21.44, 7.066194, -0.6761534, -21.262287, 7.8031015, -0.73040724, -22.114325, 8.787995, -0.28911734, -22.114325, 7.808655, -1.1238518, -21.262287, 7.8031015, -0.73040724, -21.130926, 6.7908683, 0.2619624, -21.262287, 7.8031015, -0.73040724, -21.44, 7.066194, -0.6761534, -22.114325, 9.11437, 0.70111585, -22.114325, 8.787995, -0.28911734, -21.498312, 8.531679, -0.17249346, -22.114325, 8.787995, -0.28911734, -21.262287, 7.8031015, -0.73040724, -21.498312, 8.531679, -0.17249346, -21.130926, 6.7908683, 0.2619624, -20.842571, 7.7866554, 0.45014048, -21.262287, 7.8031015, -0.73040724, -20.842571, 7.7866554, 0.45014048, -21.498312, 8.531679, -0.17249346, -21.262287, 7.8031015, -0.73040724, -21.510273, 7.032019, 1.297905, -20.842571, 7.7866554, 0.45014048, -21.130926, 6.7908683, 0.2619624, -21.510273, 7.032019, 1.297905, -21.130926, 7.770209, 1.6309018, -20.842571, 7.7866554, 0.45014048, -20.842571, 7.7866554, 0.45014048, -21.262287, 8.782442, 0.63831854, -21.498312, 8.531679, -0.17249346, -22.114325, 9.11437, 0.70111585, -21.498312, 8.531679, -0.17249346, -21.262287, 8.782442, 0.63831854, -21.130926, 7.770209, 1.6309018, -21.262287, 8.782442, 0.63831854, -20.842571, 7.7866554, 0.45014048, -22.186733, 8.776889, 1.5659679, -22.114325, 9.11437, 0.70111585, -21.262287, 8.782442, 0.63831854, -21.130926, 7.770209, 1.6309018, -21.44, 8.516942, 1.3517308, -21.262287, 8.782442, 0.63831854, -22.186733, 8.776889, 1.5659679, -21.262287, 8.782442, 0.63831854, -21.44, 8.516942, 1.3517308, -23.013992, 7.066194, -0.6761534, -22.403961, 6.4589396, 0.19937873, -22.186733, 6.818422, -1.1716971, -23.013992, 7.066194, -0.6761534, -23.604374, 6.7908683, 0.2619624, -22.403961, 6.4589396, 0.19937873, -22.186733, 6.818422, -1.1716971, -22.403961, 6.4589396, 0.19937873, -21.130926, 6.7908683, 0.2619624, -22.186733, 6.818422, -1.1716971, -21.130926, 6.7908683, 0.2619624, -21.44, 7.066194, -0.6761534, -22.403961, 6.4589396, 0.19937873, -21.510273, 7.032019, 1.297905, -21.130926, 6.7908683, 0.2619624, -22.403961, 6.4589396, 0.19937873, -22.54878, 6.763315, 1.6958355, -21.510273, 7.032019, 1.297905, -23.604374, 6.7908683, 0.2619624, -22.54878, 6.763315, 1.6958355, -22.403961, 6.4589396, 0.19937873, -23.604374, 6.7908683, 0.2619624, -23.394194, 7.032019, 1.297905, -22.54878, 6.763315, 1.6958355, -19.55437, 5.4422207, 0.70111716, -20.65247, 5.732071, 0.5048219, -19.604567, 5.300392, 0.49414217, -19.55437, 5.4422207, 0.70111716, -20.566177, 5.8454905, 0.70111704, -20.65247, 5.732071, 0.5048219, -20.566177, 5.8454905, 0.70111704, -21.690977, 6.831879, 0.64408636, -20.65247, 5.732071, 0.5048219, -20.566177, 5.8454905, 0.70111704, -21.649538, 6.831452, 0.7011167, -21.690977, 6.831879, 0.64408636, -20.65247, 5.732071, 0.5048219, -21.690977, 6.831879, 0.64408636, -21.758045, 6.83252, 0.66587317, -19.604567, 5.300392, 0.49414217, -20.65247, 5.732071, 0.5048219, -20.792162, 5.5481644, 0.5797943, -20.65247, 5.732071, 0.5048219, -21.758045, 6.83252, 0.66587317, -20.792162, 5.5481644, 0.5797943, -19.604567, 5.300392, 0.49414217, -20.792162, 5.5481644, 0.5797943, -19.685518, 5.0709896, 0.57317305, -20.792162, 5.5481644, 0.5797943, -21.758045, 6.83252, 0.66587317, -21.758045, 6.83252, 0.7363601, -19.685518, 5.0709896, 0.57317305, -20.792162, 5.5481644, 0.5797943, -20.792162, 5.5481644, 0.8224399, -20.792162, 5.5481644, 0.5797943, -21.758045, 6.83252, 0.7363601, -20.792162, 5.5481644, 0.8224399, -19.685518, 5.0709896, 0.57317305, -20.792162, 5.5481644, 0.8224399, -19.685518, 5.07099, 0.8290615, -20.792162, 5.5481644, 0.8224399, -21.758045, 6.83252, 0.7363601, -21.690977, 6.831879, 0.7581469, -19.685518, 5.07099, 0.8290615, -20.792162, 5.5481644, 0.8224399, -20.65247, 5.732071, 0.8974122, -20.792162, 5.5481644, 0.8224399, -21.690977, 6.831879, 0.7581469, -20.65247, 5.732071, 0.8974122, -20.65247, 5.732071, 0.8974122, -21.690977, 6.831879, 0.7581469, -21.649538, 6.831452, 0.7011167, -19.685518, 5.07099, 0.8290615, -20.65247, 5.732071, 0.8974122, -19.604567, 5.3003926, 0.90809214, -20.65247, 5.732071, 0.8974122, -21.649538, 6.831452, 0.7011167, -20.566177, 5.8454905, 0.70111704, -19.604567, 5.3003926, 0.90809214, -20.65247, 5.732071, 0.8974122, -20.566177, 5.8454905, 0.70111704, -19.604567, 5.3003926, 0.90809214, -20.566177, 5.8454905, 0.70111704, -19.55437, 5.4422207, 0.70111716, -20.685791, 3.1409323, 0.701118, -19.6868, 5.19808, 0.9268887, -20.258171, 3.1409326, 1.3254604, -20.685791, 3.1409323, 0.701118, -19.848066, 5.2273426, 0.70111716, -19.6868, 5.19808, 0.9268887, -19.848066, 5.2273426, 0.70111716, -19.605635, 7.2641983, 0.800866, -19.6868, 5.19808, 0.9268887, -19.848066, 5.2273426, 0.70111716, -19.677616, 7.271674, 0.70111644, -19.605635, 7.2641983, 0.800866, -19.6868, 5.19808, 0.9268887, -19.605635, 7.2641983, 0.800866, -19.488796, 7.2518096, 0.7628459, -20.258171, 3.1409326, 1.3254604, -19.6868, 5.19808, 0.9268887, -19.425571, 5.150448, 0.8405957, -19.6868, 5.19808, 0.9268887, -19.488796, 7.2518096, 0.7628459, -19.425571, 5.150448, 0.8405957, -20.258171, 3.1409326, 1.3254604, -19.425571, 5.150448, 0.8405957, -19.566545, 3.1409326, 1.0870868, -19.425571, 5.150448, 0.8405957, -19.488796, 7.2518096, 0.7628459, -19.488796, 7.2518096, 0.63938713, -19.566545, 3.1409326, 1.0870868, -19.425571, 5.150448, 0.8405957, -19.425785, 5.150448, 0.5616387, -19.425571, 5.150448, 0.8405957, -19.488796, 7.2518096, 0.63938713, -19.425785, 5.150448, 0.5616387, -19.566545, 3.1409326, 1.0870868, -19.425785, 5.150448, 0.5616387, -19.566545, 3.1409323, 0.3151492, -19.425785, 5.150448, 0.5616387, -19.488796, 7.2518096, 0.63938713, -19.605635, 7.2641983, 0.601367, -19.566545, 3.1409323, 0.3151492, -19.425785, 5.150448, 0.5616387, -19.6868, 5.19808, 0.47534573, -19.425785, 5.150448, 0.5616387, -19.605635, 7.2641983, 0.601367, -19.6868, 5.19808, 0.47534573, -19.6868, 5.19808, 0.47534573, -19.605635, 7.2641983, 0.601367, -19.677616, 7.271674, 0.70111644, -19.566545, 3.1409323, 0.3151492, -19.6868, 5.19808, 0.47534573, -20.258171, 3.140932, 0.07677555, -19.6868, 5.19808, 0.47534573, -19.677616, 7.271674, 0.70111644, -19.848066, 5.2273426, 0.70111716, -20.258171, 3.140932, 0.07677555, -19.6868, 5.19808, 0.47534573, -19.848066, 5.2273426, 0.70111716, -20.258171, 3.140932, 0.07677555, -19.848066, 5.2273426, 0.70111716, -20.685791, 3.1409323, 0.701118, -9.178474, 7.896264, -9.667162, -7.7554946, 8.923878, -8.79697, -7.8560977, 7.5083733, -9.016546, -7.8560977, 7.5083733, -9.016546, -7.7554946, 8.923878, -8.79697, -6.0311303, 8.959761, -9.644308, -7.8560977, 7.5083733, -9.016546, -6.0311303, 8.959761, -9.644308, -6.338922, 7.877041, -9.814329, -9.178474, 7.896264, -9.667162, -9.392284, 8.901663, -9.170551, -7.7554946, 8.923878, -8.79697, -9.710115, 7.6064134, -11.436808, -9.392284, 8.901663, -9.170551, -9.178474, 7.896264, -9.667162, -7.7554946, 8.923878, -8.79697, -6.5292373, 9.998694, -10.128958, -6.0311303, 8.959761, -9.644308, -9.710115, 7.6064134, -11.436808, -9.815418, 9.002266, -11.310146, -9.392284, 8.901663, -9.170551, -8.902294, 7.999003, -13.082569, -9.815418, 9.002266, -11.310146, -9.710115, 7.6064134, -11.436808, -9.392284, 8.901663, -9.170551, -7.567102, 10.353264, -9.798311, -7.7554946, 8.923878, -8.79697, -7.7554946, 8.923878, -8.79697, -7.567102, 10.353264, -9.798311, -6.5292373, 9.998694, -10.128958, -9.815418, 9.002266, -11.310146, -8.663919, 9.972849, -9.918352, -9.392284, 8.901663, -9.170551, -9.392284, 8.901663, -9.170551, -8.663919, 9.972849, -9.918352, -7.567102, 10.353264, -9.798311, -8.902294, 7.999003, -13.082569, -9.121016, 9.077452, -13.414072, -9.815418, 9.002266, -11.310146, -9.121016, 9.077452, -13.414072, -8.902294, 7.999003, -13.082569, -7.418226, 7.5777907, -13.678717, -9.815418, 9.002266, -11.310146, -8.8031845, 10.372702, -11.1476, -8.663919, 9.972849, -9.918352, -9.121016, 9.077452, -13.414072, -8.8031845, 10.372702, -11.1476, -9.815418, 9.002266, -11.310146, -8.663919, 9.972849, -9.918352, -7.5043044, 10.829584, -11.093346, -7.567102, 10.353264, -9.798311, -8.663919, 9.972849, -9.918352, -8.8031845, 10.372702, -11.1476, -7.5043044, 10.829584, -11.093346, -9.121016, 9.077452, -13.414072, -8.543451, 10.050811, -12.445198, -8.8031845, 10.372702, -11.1476, -9.121016, 9.077452, -13.414072, -7.418226, 7.5777907, -13.678717, -7.404982, 8.97941, -13.680212, -8.543451, 10.050811, -12.445198, -9.121016, 9.077452, -13.414072, -7.404982, 8.97941, -13.680212, -7.404982, 8.97941, -13.680212, -7.418226, 7.5777907, -13.678717, -6.070005, 7.8608065, -12.713261, -8.8031845, 10.372702, -11.1476, -8.543451, 10.050811, -12.445198, -7.479528, 10.367147, -12.461218, -8.543451, 10.050811, -12.445198, -7.404982, 8.97941, -13.680212, -7.479528, 10.367147, -12.461218, -8.8031845, 10.372702, -11.1476, -7.479528, 10.367147, -12.461218, -7.5043044, 10.829584, -11.093346, -7.404982, 8.97941, -13.680212, -6.070005, 7.8608065, -12.713261, -5.7767367, 8.867486, -12.725651, -7.479528, 10.367147, -12.461218, -7.404982, 8.97941, -13.680212, -5.7767367, 8.867486, -12.725651, -5.564422, 7.4799643, -11.258242, -5.7767367, 8.867486, -12.725651, -6.070005, 7.8608065, -12.713261, -7.5043044, 10.829584, -11.093346, -7.479528, 10.367147, -12.461218, -6.4160304, 9.957684, -12.139328, -7.479528, 10.367147, -12.461218, -5.7767367, 8.867486, -12.725651, -6.4160304, 9.957684, -12.139328, -5.564422, 7.4799643, -11.258242, -5.345058, 8.901022, -11.16725, -5.7767367, 8.867486, -12.725651, -5.345058, 8.901022, -11.16725, -6.4160304, 9.957684, -12.139328, -5.7767367, 8.867486, -12.725651, -6.338922, 7.877041, -9.814329, -5.345058, 8.901022, -11.16725, -5.564422, 7.4799643, -11.258242, -6.338922, 7.877041, -9.814329, -6.0311303, 8.959761, -9.644308, -5.345058, 8.901022, -11.16725, -5.345058, 8.901022, -11.16725, -6.2436585, 10.347497, -11.111715, -6.4160304, 9.957684, -12.139328, -7.5043044, 10.829584, -11.093346, -6.4160304, 9.957684, -12.139328, -6.2436585, 10.347497, -11.111715, -6.0311303, 8.959761, -9.644308, -6.2436585, 10.347497, -11.111715, -5.345058, 8.901022, -11.16725, -7.567102, 10.353264, -9.798311, -7.5043044, 10.829584, -11.093346, -6.2436585, 10.347497, -11.111715, -6.0311303, 8.959761, -9.644308, -6.5292373, 9.998694, -10.128958, -6.2436585, 10.347497, -11.111715, -7.567102, 10.353264, -9.798311, -6.2436585, 10.347497, -11.111715, -6.5292373, 9.998694, -10.128958, -8.902294, 7.999003, -13.082569, -7.6561713, 7.073704, -11.383837, -7.418226, 7.5777907, -13.678717, -8.902294, 7.999003, -13.082569, -9.710115, 7.6064134, -11.436808, -7.6561713, 7.073704, -11.383837, -7.418226, 7.5777907, -13.678717, -7.6561713, 7.073704, -11.383837, -5.564422, 7.4799643, -11.258242, -7.418226, 7.5777907, -13.678717, -5.564422, 7.4799643, -11.258242, -6.070005, 7.8608065, -12.713261, -7.6561713, 7.073704, -11.383837, -6.338922, 7.877041, -9.814329, -5.564422, 7.4799643, -11.258242, -7.6561713, 7.073704, -11.383837, -7.8560977, 7.5083733, -9.016546, -6.338922, 7.877041, -9.814329, -9.710115, 7.6064134, -11.436808, -7.8560977, 7.5083733, -9.016546, -7.6561713, 7.073704, -11.383837, -9.710115, 7.6064134, -11.436808, -9.178474, 7.896264, -9.667162, -7.8560977, 7.5083733, -9.016546, -11.561996, 7.1179185, -10.496557, -10.571764, 7.8505545, -9.770115, -10.716581, 6.8492146, -10.098627, -10.716581, 6.8492146, -10.098627, -10.571764, 7.8505545, -9.770115, -9.298729, 7.856108, -10.163559, -10.716581, 6.8492146, -10.098627, -9.298729, 7.856108, -10.163559, -9.678076, 7.1179185, -10.496557, -11.561996, 7.1179185, -10.496557, -11.772175, 7.856108, -10.163559, -10.571764, 7.8505545, -9.770115, -11.772175, 6.8767676, -11.532499, -11.772175, 7.856108, -10.163559, -11.561996, 7.1179185, -10.496557, -10.571764, 7.8505545, -9.770115, -9.607803, 8.602841, -10.44273, -9.298729, 7.856108, -10.163559, -11.772175, 6.8767676, -11.532499, -11.843517, 7.872555, -11.344322, -11.772175, 7.856108, -10.163559, -11.181795, 7.1520934, -12.470615, -11.843517, 7.872555, -11.344322, -11.772175, 6.8767676, -11.532499, -11.772175, 7.856108, -10.163559, -10.354536, 8.862788, -10.228495, -10.571764, 7.8505545, -9.770115, -10.571764, 7.8505545, -9.770115, -10.354536, 8.862788, -10.228495, -9.607803, 8.602841, -10.44273, -11.843517, 7.872555, -11.344322, -11.181795, 8.602841, -10.44273, -11.772175, 7.856108, -10.163559, -11.772175, 7.856108, -10.163559, -11.181795, 8.602841, -10.44273, -10.354536, 8.862788, -10.228495, -11.181795, 7.1520934, -12.470615, -11.206358, 7.889001, -12.524868, -11.843517, 7.872555, -11.344322, -11.206358, 7.889001, -12.524868, -11.181795, 7.1520934, -12.470615, -10.354536, 6.904321, -12.966158, -11.843517, 7.872555, -11.344322, -11.206358, 8.868341, -11.156143, -11.181795, 8.602841, -10.44273, -11.206358, 7.889001, -12.524868, -11.206358, 8.868341, -11.156143, -11.843517, 7.872555, -11.344322, -11.181795, 8.602841, -10.44273, -10.282127, 9.20027, -11.093346, -10.354536, 8.862788, -10.228495, -11.181795, 8.602841, -10.44273, -11.206358, 8.868341, -11.156143, -10.282127, 9.20027, -11.093346, -11.206358, 7.889001, -12.524868, -10.930391, 8.6175785, -11.966955, -11.206358, 8.868341, -11.156143, -11.206358, 7.889001, -12.524868, -10.354536, 6.904321, -12.966158, -10.282127, 7.894554, -12.918314, -10.930391, 8.6175785, -11.966955, -11.206358, 7.889001, -12.524868, -10.282127, 7.894554, -12.918314, -10.282127, 7.894554, -12.918314, -10.354536, 6.904321, -12.966158, -9.607803, 7.1520934, -12.470615, -11.206358, 8.868341, -11.156143, -10.930391, 8.6175785, -11.966955, -10.282127, 8.873895, -12.083578, -10.930391, 8.6175785, -11.966955, -10.282127, 7.894554, -12.918314, -10.282127, 8.873895, -12.083578, -11.206358, 8.868341, -11.156143, -10.282127, 8.873895, -12.083578, -10.282127, 9.20027, -11.093346, -10.282127, 7.894554, -12.918314, -9.607803, 7.1520934, -12.470615, -9.43009, 7.889001, -12.524868, -10.282127, 8.873895, -12.083578, -10.282127, 7.894554, -12.918314, -9.43009, 7.889001, -12.524868, -9.298729, 6.8767676, -11.532499, -9.43009, 7.889001, -12.524868, -9.607803, 7.1520934, -12.470615, -10.282127, 9.20027, -11.093346, -10.282127, 8.873895, -12.083578, -9.666115, 8.6175785, -11.966955, -10.282127, 8.873895, -12.083578, -9.43009, 7.889001, -12.524868, -9.666115, 8.6175785, -11.966955, -9.298729, 6.8767676, -11.532499, -9.010374, 7.872555, -11.344322, -9.43009, 7.889001, -12.524868, -9.010374, 7.872555, -11.344322, -9.666115, 8.6175785, -11.966955, -9.43009, 7.889001, -12.524868, -9.678076, 7.1179185, -10.496557, -9.010374, 7.872555, -11.344322, -9.298729, 6.8767676, -11.532499, -9.678076, 7.1179185, -10.496557, -9.298729, 7.856108, -10.163559, -9.010374, 7.872555, -11.344322, -9.010374, 7.872555, -11.344322, -9.43009, 8.868341, -11.156143, -9.666115, 8.6175785, -11.966955, -10.282127, 9.20027, -11.093346, -9.666115, 8.6175785, -11.966955, -9.43009, 8.868341, -11.156143, -9.298729, 7.856108, -10.163559, -9.43009, 8.868341, -11.156143, -9.010374, 7.872555, -11.344322, -10.354536, 8.862788, -10.228495, -10.282127, 9.20027, -11.093346, -9.43009, 8.868341, -11.156143, -9.298729, 7.856108, -10.163559, -9.607803, 8.602841, -10.44273, -9.43009, 8.868341, -11.156143, -10.354536, 8.862788, -10.228495, -9.43009, 8.868341, -11.156143, -9.607803, 8.602841, -10.44273, -11.181795, 7.1520934, -12.470615, -10.571764, 6.544839, -11.595083, -10.354536, 6.904321, -12.966158, -11.181795, 7.1520934, -12.470615, -11.772175, 6.8767676, -11.532499, -10.571764, 6.544839, -11.595083, -10.354536, 6.904321, -12.966158, -10.571764, 6.544839, -11.595083, -9.298729, 6.8767676, -11.532499, -10.354536, 6.904321, -12.966158, -9.298729, 6.8767676, -11.532499, -9.607803, 7.1520934, -12.470615, -10.571764, 6.544839, -11.595083, -9.678076, 7.1179185, -10.496557, -9.298729, 6.8767676, -11.532499, -10.571764, 6.544839, -11.595083, -10.716581, 6.8492146, -10.098627, -9.678076, 7.1179185, -10.496557, -11.772175, 6.8767676, -11.532499, -10.716581, 6.8492146, -10.098627, -10.571764, 6.544839, -11.595083, -11.772175, 6.8767676, -11.532499, -11.561996, 7.1179185, -10.496557, -10.716581, 6.8492146, -10.098627, -7.722173, 5.52812, -11.093344, -8.820272, 5.8179703, -11.28964, -7.772368, 5.3862915, -11.30032, -7.722173, 5.52812, -11.093344, -8.73398, 5.93139, -11.093346, -8.820272, 5.8179703, -11.28964, -8.73398, 5.93139, -11.093346, -9.858778, 6.9177785, -11.150375, -8.820272, 5.8179703, -11.28964, -8.73398, 5.93139, -11.093346, -9.817341, 6.9173512, -11.093346, -9.858778, 6.9177785, -11.150375, -8.820272, 5.8179703, -11.28964, -9.858778, 6.9177785, -11.150375, -9.925848, 6.9184194, -11.128588, -7.772368, 5.3862915, -11.30032, -8.820272, 5.8179703, -11.28964, -8.959965, 5.6340637, -11.214668, -8.820272, 5.8179703, -11.28964, -9.925848, 6.9184194, -11.128588, -8.959965, 5.6340637, -11.214668, -7.772368, 5.3862915, -11.30032, -8.959965, 5.6340637, -11.214668, -7.8533216, 5.156889, -11.221289, -8.959965, 5.6340637, -11.214668, -9.925848, 6.9184194, -11.128588, -9.925848, 6.9184194, -11.058102, -7.8533216, 5.156889, -11.221289, -8.959965, 5.6340637, -11.214668, -8.959965, 5.6340637, -10.972021, -8.959965, 5.6340637, -11.214668, -9.925848, 6.9184194, -11.058102, -8.959965, 5.6340637, -10.972021, -7.8533216, 5.156889, -11.221289, -8.959965, 5.6340637, -10.972021, -7.8533216, 5.1568894, -10.965401, -8.959965, 5.6340637, -10.972021, -9.925848, 6.9184194, -11.058102, -9.858778, 6.9177785, -11.036314, -7.8533216, 5.1568894, -10.965401, -8.959965, 5.6340637, -10.972021, -8.820272, 5.8179703, -10.897049, -8.959965, 5.6340637, -10.972021, -9.858778, 6.9177785, -11.036314, -8.820272, 5.8179703, -10.897049, -8.820272, 5.8179703, -10.897049, -9.858778, 6.9177785, -11.036314, -9.817341, 6.9173512, -11.093346, -7.8533216, 5.1568894, -10.965401, -8.820272, 5.8179703, -10.897049, -7.772368, 5.386292, -10.88637, -8.820272, 5.8179703, -10.897049, -9.817341, 6.9173512, -11.093346, -8.73398, 5.93139, -11.093346, -7.772368, 5.386292, -10.88637, -8.820272, 5.8179703, -10.897049, -8.73398, 5.93139, -11.093346, -7.772368, 5.386292, -10.88637, -8.73398, 5.93139, -11.093346, -7.722173, 5.52812, -11.093344, -8.853594, 3.2268317, -11.093344, -7.8546023, 5.2839794, -10.867573, -8.425974, 3.226832, -10.469002, -8.853594, 3.2268317, -11.093344, -8.015869, 5.313242, -11.093344, -7.8546023, 5.2839794, -10.867573, -8.015869, 5.313242, -11.093344, -7.773436, 7.3500977, -10.993595, -7.8546023, 5.2839794, -10.867573, -8.015869, 5.313242, -11.093344, -7.8454185, 7.3575735, -11.093346, -7.773436, 7.3500977, -10.993595, -7.8546023, 5.2839794, -10.867573, -7.773436, 7.3500977, -10.993595, -7.6565986, 7.337709, -11.031616, -8.425974, 3.226832, -10.469002, -7.8546023, 5.2839794, -10.867573, -7.5933747, 5.236347, -10.953865, -7.8546023, 5.2839794, -10.867573, -7.6565986, 7.337709, -11.031616, -7.5933747, 5.236347, -10.953865, -8.425974, 3.226832, -10.469002, -7.5933747, 5.236347, -10.953865, -7.734348, 3.226832, -10.707375, -7.5933747, 5.236347, -10.953865, -7.6565986, 7.337709, -11.031616, -7.6565986, 7.337709, -11.155075, -7.734348, 3.226832, -10.707375, -7.5933747, 5.236347, -10.953865, -7.5935884, 5.236347, -11.232822, -7.5933747, 5.236347, -10.953865, -7.6565986, 7.337709, -11.155075, -7.5935884, 5.236347, -11.232822, -7.734348, 3.226832, -10.707375, -7.5935884, 5.236347, -11.232822, -7.734348, 3.2268317, -11.479313, -7.5935884, 5.236347, -11.232822, -7.6565986, 7.337709, -11.155075, -7.773436, 7.3500977, -11.193094, -7.734348, 3.2268317, -11.479313, -7.5935884, 5.236347, -11.232822, -7.8546023, 5.2839794, -11.319117, -7.5935884, 5.236347, -11.232822, -7.773436, 7.3500977, -11.193094, -7.8546023, 5.2839794, -11.319117, -7.8546023, 5.2839794, -11.319117, -7.773436, 7.3500977, -11.193094, -7.8454185, 7.3575735, -11.093346, -7.734348, 3.2268317, -11.479313, -7.8546023, 5.2839794, -11.319117, -8.425974, 3.2268314, -11.717686, -7.8546023, 5.2839794, -11.319117, -7.8454185, 7.3575735, -11.093346, -8.015869, 5.313242, -11.093344, -8.425974, 3.2268314, -11.717686, -7.8546023, 5.2839794, -11.319117, -8.015869, 5.313242, -11.093344, -8.425974, 3.2268314, -11.717686, -8.015869, 5.313242, -11.093344, -8.853594, 3.2268317, -11.093344)
-
-[sub_resource type="PlaneMesh" id="PlaneMesh_7ny4s"]
-resource_local_to_scene = true
-size = Vector2(16.1, 12.3675)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_agj3o"]
-resource_local_to_scene = true
-size = Vector3(16.1, 4, 12.3675)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_l6210"]
-resource_local_to_scene = true
-size = Vector3(16.1, 4.8, 12.3675)
-
-[sub_resource type="PlaneMesh" id="PlaneMesh_6upbg"]
-resource_local_to_scene = true
-size = Vector2(10000, 10000)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_umo15"]
-resource_local_to_scene = true
-size = Vector3(10000, 2, 10000)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_1fdkm"]
-resource_local_to_scene = true
-size = Vector3(17, 5, 80)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_y2c0j"]
-resource_local_to_scene = true
-size = Vector3(42, 5, 80)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_uwwqy"]
-resource_local_to_scene = true
-size = Vector3(48, 5, 18)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_hf28p"]
-resource_local_to_scene = true
-size = Vector3(48, 5, 18)
-
-[node name="Main" type="Node3D" unique_id=704196381]
+[node name="Main" type="Node3D"]
script = ExtResource("3_main")
fish_catalog = ExtResource("6_pool")
pelican_buyer_profile = ExtResource("7_pelicans")
main_shop_buyer_profile = ExtResource("12_main_shop")
item_catalog = ExtResource("11_items")
-gatherable_catalog = ExtResource("58_gatherable_catalog")
-[node name="TitleBackgroundLayer" type="CanvasLayer" parent="." unique_id=1690754702]
+[node name="TestWorld" parent="." instance=ExtResource("1_world")]
+
+[node name="Players" type="Node3D" parent="."]
+
+[node name="Player" parent="Players" instance=ExtResource("2_player")]
unique_name_in_owner = true
-layer = -1
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 16)
-[node name="TitleBackground" type="ColorRect" parent="TitleBackgroundLayer" unique_id=1768697365]
+[node name="FishingSpot" parent="." instance=ExtResource("4_spot")]
unique_name_in_owner = true
-material = SubResource("ShaderMaterial_title_grass_native")
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-mouse_filter = 2
+position = Vector3(0, 1.02, -18)
-[node name="PlayerMenuBackdrop" type="ColorRect" parent="." unique_id=2055423661]
-unique_name_in_owner = true
-visible = false
-z_index = 10
-material = SubResource("ShaderMaterial_player_menu_pattern")
-mouse_filter = 2
-
-[node name="ShopBackdrop" type="ColorRect" parent="." unique_id=1404109502]
-unique_name_in_owner = true
-visible = false
-z_index = 10
-material = SubResource("ShaderMaterial_shop_pattern")
-mouse_filter = 2
-
-[node name="InterfaceFontController" type="Node" parent="." unique_id=384169781]
-unique_name_in_owner = true
-script = ExtResource("39_fonts")
-
-[node name="WorldTimeService" type="Node" parent="." unique_id=1774610576]
-unique_name_in_owner = true
-script = ExtResource("47_world_time")
-
-[node name="WorldTimeVisualController" type="Node" parent="." unique_id=1861087008]
-unique_name_in_owner = true
-script = ExtResource("48_world_time_visuals")
-
-[node name="NetworkWorldTimeService" type="Node" parent="." unique_id=1136962511]
-unique_name_in_owner = true
-script = ExtResource("49_network_world_time")
-
-[node name="WorldWeatherService" type="Node" parent="." unique_id=1812156280]
-unique_name_in_owner = true
-script = ExtResource("50_world_weather")
-
-[node name="NetworkWorldWeatherService" type="Node" parent="." unique_id=266238117]
-unique_name_in_owner = true
-script = ExtResource("51_network_world_weather")
-
-[node name="PlayerJobService" type="Node" parent="." unique_id=2062127535]
-unique_name_in_owner = true
-script = ExtResource("52_player_jobs")
-
-[node name="NetworkJobService" type="Node" parent="." unique_id=556458360]
-unique_name_in_owner = true
-script = ExtResource("53_network_jobs")
-
-[node name="PlayerDataRoot" type="Node" parent="." unique_id=281868360]
-unique_name_in_owner = true
-script = ExtResource("37_data_root")
-
-[node name="IdentityBackupService" type="Node" parent="." unique_id=1606277266]
-unique_name_in_owner = true
-script = ExtResource("38_identity_backup")
-
-[node name="NetworkSession" type="Node" parent="." unique_id=88578570]
-unique_name_in_owner = true
-script = ExtResource("17_network_session")
-
-[node name="DiscoveryClient" type="Node" parent="." unique_id=401971194]
-unique_name_in_owner = true
-script = ExtResource("57_discovery")
-
-[node name="NetworkProfilePreferences" type="Node" parent="." unique_id=2119445545]
-unique_name_in_owner = true
-script = ExtResource("18_network_profile")
-
-[node name="PlayerIdentityStore" type="Node" parent="." unique_id=737958453]
-unique_name_in_owner = true
-script = ExtResource("30_player_identity")
-
-[node name="HostIdentityStore" type="Node" parent="." unique_id=351659562]
-unique_name_in_owner = true
-script = ExtResource("31_host_identity")
-
-[node name="KnownPlayerStore" type="Node" parent="." unique_id=390068772]
-unique_name_in_owner = true
-script = ExtResource("32_known_players")
-
-[node name="ServerTrustStore" type="Node" parent="." unique_id=1565692831]
-unique_name_in_owner = true
-script = ExtResource("33_server_trust")
-
-[node name="PlayerRelationshipStore" type="Node" parent="." unique_id=612731662]
-unique_name_in_owner = true
-script = ExtResource("34_relationships")
-
-[node name="HostBanStore" type="Node" parent="." unique_id=1672077885]
-unique_name_in_owner = true
-script = ExtResource("35_bans")
-
-[node name="PlayerAppearanceStore" type="Node" parent="." unique_id=1740017030]
-unique_name_in_owner = true
-script = ExtResource("28_appearance")
-
-[node name="NetworkProfileService" type="Node" parent="." unique_id=1074818740]
-unique_name_in_owner = true
-script = ExtResource("29_profile_service")
-
-[node name="SavedServerStore" type="Node" parent="." unique_id=660213994]
-unique_name_in_owner = true
-script = ExtResource("19_saved_servers")
-
-[node name="PlayerSpawnService" type="Node" parent="." unique_id=2097451957]
-unique_name_in_owner = true
-script = ExtResource("20_spawn_service")
-
-[node name="NetworkFishingService" type="Node" parent="." unique_id=692209374]
-unique_name_in_owner = true
-script = ExtResource("21_network_fishing")
-
-[node name="NetworkSaleService" type="Node" parent="." unique_id=1445041024]
-unique_name_in_owner = true
-script = ExtResource("22_network_sale")
-
-[node name="NetworkShopService" type="Node" parent="." unique_id=1493674695]
-unique_name_in_owner = true
-script = ExtResource("23_network_shop")
-
-[node name="NetworkHomeService" type="Node" parent="."]
-unique_name_in_owner = true
-script = ExtResource("63_network_home")
-
-[node name="HomeDecorController" type="Node" parent="."]
-unique_name_in_owner = true
-script = ExtResource("64_home_decor_controller")
-
-[node name="NetworkItemUseService" type="Node" parent="." unique_id=255154670]
-unique_name_in_owner = true
-script = ExtResource("24_network_item")
-
-[node name="NetworkFishShowcaseService" type="Node" parent="." unique_id=148288598]
-unique_name_in_owner = true
-script = ExtResource("45_fish_showcase")
-
-[node name="NetworkWorldSpawnService" type="Node" parent="."]
-unique_name_in_owner = true
-script = ExtResource("59_world_spawns")
-
-[node name="NetworkSurfaceDrawingService" type="Node" parent="." unique_id=80122665]
-unique_name_in_owner = true
-script = ExtResource("46_surface_drawing")
-
-[node name="NetworkChatService" type="Node" parent="." unique_id=1956959711]
-unique_name_in_owner = true
-script = ExtResource("25_network_chat")
-
-[node name="PlayerAssetReservationService" type="Node" parent="." unique_id=217588893]
-unique_name_in_owner = true
-script = ExtResource("27_reservations")
-
-[node name="NetworkMailService" type="Node" parent="." unique_id=1415330316]
-unique_name_in_owner = true
-script = ExtResource("26_network_mail")
-
-[node name="NetworkPlayerListService" type="Node" parent="." unique_id=1638035935]
-unique_name_in_owner = true
-script = ExtResource("36_player_list")
-
-[node name="TestWorld" parent="." unique_id=1334932296 instance=ExtResource("1_world")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-
-[node name="Sun" parent="TestWorld/Environment" index="1"]
-transform = Transform3D(0.8480481, 0.4287137, -0.31147876, 0, 0.5877853, 0.809017, 0.52991927, -0.68608534, 0.49847022, 0, 0, 0)
-
-[node name="BelowWorldFailsafe" parent="TestWorld/Safety" index="0"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12, -7, 0)
-
-[node name="North" parent="TestWorld/WorldBounds" index="0"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12, 5, 63.225395)
-
-[node name="South" parent="TestWorld/WorldBounds" index="1"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12, 5, -63.225395)
-
-[node name="West" parent="TestWorld/WorldBounds" index="2"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -72.4386, 5, 0)
-
-[node name="East" parent="TestWorld/WorldBounds" index="3"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 96.4386, 5, 0)
-
-[node name="Players" type="Node3D" parent="." unique_id=550630144]
-
-[node name="Player" parent="Players" unique_id=485429332 instance=ExtResource("2_player")]
-unique_name_in_owner = true
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3.3999481, 12.07081)
-
-[node name="SurfaceDrawings" type="Node3D" parent="." unique_id=234671307]
-
-[node name="WorldGatherables" type="Node3D" parent="."]
-
-[node name="HomeWorldService" type="Node3D" parent="."]
-unique_name_in_owner = true
-script = ExtResource("62_home_world")
-
-[node name="GatheringController" type="Node3D" parent="."]
-unique_name_in_owner = true
-script = ExtResource("60_gathering_controller")
-
-[node name="FishingSpot" parent="." unique_id=1461328864 instance=ExtResource("4_spot")]
-unique_name_in_owner = true
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.02, -18)
-
-[node name="WaterRecovery" type="Node" parent="." unique_id=12981798]
+[node name="WaterRecovery" type="Node" parent="."]
unique_name_in_owner = true
script = ExtResource("8_recovery")
-[node name="PlayerSaveManager" type="Node" parent="." unique_id=223569593]
+[node name="PlayerSaveManager" type="Node" parent="."]
unique_name_in_owner = true
script = ExtResource("9_save")
-[node name="PlayerHomeState" type="Node" parent="."]
-unique_name_in_owner = true
-script = ExtResource("61_home_state")
-
-[node name="PlayerSettingsManager" type="Node" parent="." unique_id=906991991]
+[node name="PlayerSettingsManager" type="Node" parent="."]
unique_name_in_owner = true
script = ExtResource("10_settings")
-[node name="ControllerMappingManager" type="Node" parent="." unique_id=1198390067]
-unique_name_in_owner = true
-script = ExtResource("56_controller_mapping")
-
-[node name="KeyboardMouseMappingManager" type="Node" parent="."]
-unique_name_in_owner = true
-script = ExtResource("keyboard_mouse_mapping")
-
-[node name="TitleMusic" type="AudioStreamPlayer" parent="." unique_id=1292939956]
+[node name="TitleMusic" type="AudioStreamPlayer" parent="."]
unique_name_in_owner = true
+stream = ExtResource("13_title_music")
volume_db = -80.0
bus = &"Music"
-[node name="NewGameMusic" type="AudioStreamPlayer" parent="."]
-unique_name_in_owner = true
-bus = &"Music"
-
-[node name="DuskMusic" type="AudioStreamPlayer" parent="." unique_id=992744241]
-unique_name_in_owner = true
-bus = &"Music"
-
-[node name="ShorelineAmbience" type="Node" parent="." unique_id=1948037177]
-unique_name_in_owner = true
-script = ExtResource("54_shoreline_ambience")
-
-[node name="WavesAudio" type="AudioStreamPlayer" parent="ShorelineAmbience" unique_id=193530658]
-unique_name_in_owner = true
-bus = &"Environment"
-
-[node name="WorldPixelationPostprocess" parent="." unique_id=344378000 instance=ExtResource("16_world_pixelation")]
+[node name="WorldPixelationPostprocess" parent="." instance=ExtResource("16_world_pixelation")]
unique_name_in_owner = true
-[node name="UIPresentation" type="SubViewportContainer" parent="." unique_id=1104419395]
+[node name="UIPresentation" type="SubViewportContainer" parent="."]
unique_name_in_owner = true
-z_index = 20
texture_filter = 1
offset_right = 1152.0
offset_bottom = 648.0
@@ -404,16 +66,14 @@ mouse_filter = 0
stretch = true
script = ExtResource("14_pixelation")
-[node name="UIViewport" type="SubViewport" parent="UIPresentation" unique_id=461155312]
+[node name="UIViewport" type="SubViewport" parent="UIPresentation"]
transparent_bg = true
handle_input_locally = false
size = Vector2i(1152, 648)
render_target_update_mode = 4
-[node name="GameUI" parent="UIPresentation/UIViewport" unique_id=954136124 instance=ExtResource("5_ui")]
+[node name="GameUI" parent="UIPresentation/UIViewport" instance=ExtResource("5_ui")]
unique_name_in_owner = true
-[node name="PixelationResetOverlay" parent="." unique_id=291636041 instance=ExtResource("15_reset")]
+[node name="PixelationResetOverlay" parent="." instance=ExtResource("15_reset")]
unique_name_in_owner = true
-
-[editable path="TestWorld"]
diff --git a/main/runtime_performance_profile.gd b/main/runtime_performance_profile.gd
deleted file mode 100644
index f18e63a..0000000
--- a/main/runtime_performance_profile.gd
+++ /dev/null
@@ -1,64 +0,0 @@
-class_name RuntimePerformanceProfile
-extends RefCounted
-
-const PROFILE_ENVIRONMENT_VARIABLE: String = (
- "straywild_PERFORMANCE_PROFILE"
-)
-const LEGACY_LIGHT_ENVIRONMENT_VARIABLE: String = "straywild_LOW_END"
-const NETFISHING_PROFILE_ENVIRONMENT_VARIABLE := (
- "NETFISHING_PERFORMANCE_PROFILE"
-)
-const NETFISHING_LIGHT_ENVIRONMENT_VARIABLE := "NETFISHING_LOW_END"
-const NORMAL_PROFILE: StringName = &"normal"
-const LIGHT_PROFILE: StringName = &"light"
-const NORMAL_WORLD_RENDER_SCALE: float = 1.0
-const LIGHT_WORLD_RENDER_SCALE: float = 0.375
-
-var _profile_name: StringName = NORMAL_PROFILE
-
-
-static func from_environment() -> RuntimePerformanceProfile:
- var profile_name: String = OS.get_environment(
- PROFILE_ENVIRONMENT_VARIABLE
- )
- if profile_name.is_empty():
- profile_name = OS.get_environment(
- NETFISHING_PROFILE_ENVIRONMENT_VARIABLE
- )
- return from_name(
- StringName(profile_name),
- (
- OS.get_environment(LEGACY_LIGHT_ENVIRONMENT_VARIABLE) == "1"
- or OS.get_environment(NETFISHING_LIGHT_ENVIRONMENT_VARIABLE) == "1"
- ),
- )
-
-
-static func from_name(
- profile_name: StringName,
- legacy_light_requested: bool = false,
-) -> RuntimePerformanceProfile:
- var profile := RuntimePerformanceProfile.new()
- if profile_name == LIGHT_PROFILE or (
- profile_name.is_empty() and legacy_light_requested
- ):
- profile._profile_name = LIGHT_PROFILE
- else:
- profile._profile_name = NORMAL_PROFILE
- return profile
-
-
-func get_profile_name() -> StringName:
- return _profile_name
-
-
-func is_light() -> bool:
- return _profile_name == LIGHT_PROFILE
-
-
-func get_world_render_scale() -> float:
- return (
- LIGHT_WORLD_RENDER_SCALE
- if is_light()
- else NORMAL_WORLD_RENDER_SCALE
- )
diff --git a/main/runtime_performance_profile.gd.uid b/main/runtime_performance_profile.gd.uid
deleted file mode 100644
index 3f9e338..0000000
--- a/main/runtime_performance_profile.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://8vm104r2264h
diff --git a/main/texture_sampling_policy.gd b/main/texture_sampling_policy.gd
deleted file mode 100644
index 35a92a0..0000000
--- a/main/texture_sampling_policy.gd
+++ /dev/null
@@ -1,87 +0,0 @@
-extends Node
-
-## Enforces straywild's nearest-neighbor-only artwork policy at runtime.
-##
-## Godot uses separate texture-filter enums for CanvasItem and 3D materials.
-## Applying both here prevents imported models, dynamically created controls,
-## and Sprite3D presentations from silently falling back to linear sampling.
-
-
-func _enter_tree() -> void:
- get_tree().node_added.connect(_on_node_added)
- call_deferred("_enforce_existing_tree")
-
-
-func _on_node_added(node: Node) -> void:
- enforce_node(node)
- # Some runtime presenters assign their mesh or material immediately after
- # add_child(). Recheck those 3D resource owners once deferred so the newly
- # assigned resources are covered too. CanvasItems keep their filter setting
- # when their texture changes, so deferring every dynamic UI node only floods
- # the message queue during large inventory and catalog refreshes.
- if _needs_deferred_resource_check(node):
- call_deferred("_enforce_deferred_node", weakref(node))
-
-
-func _enforce_existing_tree() -> void:
- enforce_subtree(get_tree().root)
-
-
-func _enforce_deferred_node(node_ref: WeakRef) -> void:
- var node := node_ref.get_ref() as Node
- if node != null:
- enforce_node(node)
-
-
-static func _needs_deferred_resource_check(node: Node) -> bool:
- return (
- node is GeometryInstance3D
- or node is MultiMeshInstance3D
- or node is GPUParticles3D
- or node is CPUParticles3D
- )
-
-
-static func enforce_subtree(node: Node) -> void:
- enforce_node(node)
- for child: Node in node.get_children():
- enforce_subtree(child)
-
-
-static func enforce_node(node: Node) -> void:
- if node is CanvasItem:
- (node as CanvasItem).texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
- if node is SpriteBase3D:
- (node as SpriteBase3D).texture_filter = (
- BaseMaterial3D.TEXTURE_FILTER_NEAREST
- )
- if node is GeometryInstance3D:
- var geometry := node as GeometryInstance3D
- enforce_material(geometry.material_override)
- enforce_material(geometry.material_overlay)
- if node is MeshInstance3D:
- enforce_mesh((node as MeshInstance3D).mesh)
- if node is MultiMeshInstance3D:
- var multimesh := (node as MultiMeshInstance3D).multimesh
- if multimesh != null:
- enforce_mesh(multimesh.mesh)
- if node is GPUParticles3D:
- var particles := node as GPUParticles3D
- for pass_index: int in particles.draw_passes:
- enforce_mesh(particles.get_draw_pass_mesh(pass_index))
- if node is CPUParticles3D:
- enforce_mesh((node as CPUParticles3D).mesh)
-
-
-static func enforce_mesh(mesh: Mesh) -> void:
- if mesh == null:
- return
- for surface_index: int in mesh.get_surface_count():
- enforce_material(mesh.surface_get_material(surface_index))
-
-
-static func enforce_material(material: Material) -> void:
- if material is BaseMaterial3D:
- (material as BaseMaterial3D).texture_filter = (
- BaseMaterial3D.TEXTURE_FILTER_NEAREST
- )
diff --git a/main/texture_sampling_policy.gd.uid b/main/texture_sampling_policy.gd.uid
deleted file mode 100644
index 820783e..0000000
--- a/main/texture_sampling_policy.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dwd4wfn8bwk5i
diff --git a/main/world_pixelation_postprocess.gd b/main/world_pixelation_postprocess.gd
index 06b6e1b..303fa3e 100644
--- a/main/world_pixelation_postprocess.gd
+++ b/main/world_pixelation_postprocess.gd
@@ -5,7 +5,6 @@ extends CanvasLayer
var _pixel_size: int = PlayerSettings.DEFAULT_WORLD_PIXEL_SIZE
var _gameplay_active: bool = false
-var _light_performance_profile: bool = false
func _ready() -> void:
@@ -27,15 +26,6 @@ func set_gameplay_active(active: bool) -> void:
_refresh_grid()
-func set_light_performance_profile(enabled: bool) -> void:
- _light_performance_profile = enabled
- _refresh_grid()
-
-
-func is_light_performance_profile() -> bool:
- return _light_performance_profile
-
-
func get_grid_size() -> Vector2i:
return PlayerSettings.get_world_grid_size(
_pixel_size,
@@ -48,7 +38,6 @@ func _refresh_grid() -> void:
return
var effect_enabled: bool = (
_gameplay_active
- and not _light_performance_profile
and _pixel_size != PlayerSettings.MIN_WORLD_PIXEL_SIZE
)
_screen_grid.visible = effect_enabled
diff --git a/network/connection_endpoint.gd b/network/connection_endpoint.gd
deleted file mode 100644
index a11eb36..0000000
--- a/network/connection_endpoint.gd
+++ /dev/null
@@ -1,12 +0,0 @@
-class_name ConnectionEndpoint
-extends RefCounted
-
-var host: String = ""
-var port: int = 7777
-var normalized_display: String = ""
-var original_display: String = ""
-var error_message: String = ""
-
-
-func is_valid() -> bool:
- return error_message.is_empty() and not host.is_empty() and port in range(1, 65536)
diff --git a/network/connection_endpoint.gd.uid b/network/connection_endpoint.gd.uid
deleted file mode 100644
index c0103c2..0000000
--- a/network/connection_endpoint.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c0ssdkdd35qgf
diff --git a/network/connection_route.gd b/network/connection_route.gd
deleted file mode 100644
index af9e625..0000000
--- a/network/connection_route.gd
+++ /dev/null
@@ -1,24 +0,0 @@
-class_name ConnectionRoute
-extends RefCounted
-
-enum Kind {
- DIRECT,
-}
-
-var kind: Kind = Kind.DIRECT
-var direct_endpoint: ConnectionEndpoint
-var display_description: String = ""
-
-
-static func direct(endpoint: ConnectionEndpoint) -> ConnectionRoute:
- var route := ConnectionRoute.new()
- route.kind = Kind.DIRECT
- route.direct_endpoint = endpoint
- route.display_description = (
- endpoint.normalized_display if endpoint != null else ""
- )
- return route
-
-
-func is_valid() -> bool:
- return kind == Kind.DIRECT and direct_endpoint != null and direct_endpoint.is_valid()
diff --git a/network/connection_route.gd.uid b/network/connection_route.gd.uid
deleted file mode 100644
index 2f2ca91..0000000
--- a/network/connection_route.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dquhcebc57bg8
diff --git a/network/direct_enet_transport.gd b/network/direct_enet_transport.gd
deleted file mode 100644
index 067b86c..0000000
--- a/network/direct_enet_transport.gd
+++ /dev/null
@@ -1,61 +0,0 @@
-class_name DirectEnetTransport
-extends NetworkTransport
-
-
-func start_host(
- port: int,
- max_clients: int,
- bind_address: String = "*",
-) -> Error:
- disconnect_transport()
- var enet_peer := ENetMultiplayerPeer.new()
- if not bind_address.is_empty() and bind_address != "*":
- enet_peer.set_bind_ip(bind_address)
- var error: Error = enet_peer.create_server(
- port, max_clients, NetworkProtocol.ENET_CHANNEL_COUNT
- )
- if error != OK:
- transport_error.emit("Unable to host UDP port %d." % port)
- return error
- _peer = enet_peer
- _route_description = "UDP *:%d" % port
- return OK
-
-
-func connect_to_route(route: ConnectionRoute) -> Error:
- disconnect_transport()
- if route == null or not route.is_valid():
- transport_error.emit("The direct connection route is invalid.")
- return ERR_INVALID_PARAMETER
- var endpoint: ConnectionEndpoint = route.direct_endpoint
- var enet_peer := ENetMultiplayerPeer.new()
- var error: Error = enet_peer.create_client(
- endpoint.host, endpoint.port, NetworkProtocol.ENET_CHANNEL_COUNT
- )
- if error != OK:
- transport_error.emit(
- "Unable to connect to %s." % endpoint.normalized_display
- )
- return error
- _peer = enet_peer
- _route_description = endpoint.normalized_display
- return OK
-
-
-func send_raw_packet(
- destination_address: String,
- destination_port: int,
- packet: PackedByteArray,
-) -> bool:
- var enet_peer := _peer as ENetMultiplayerPeer
- if (
- enet_peer == null
- or enet_peer.host == null
- or destination_address.is_empty()
- or destination_port < 1
- or destination_port > 65535
- or packet.is_empty()
- ):
- return false
- enet_peer.host.socket_send(destination_address, destination_port, packet)
- return true
diff --git a/network/direct_enet_transport.gd.uid b/network/direct_enet_transport.gd.uid
deleted file mode 100644
index 09f5d1b..0000000
--- a/network/direct_enet_transport.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://d4jn8ubxqkqa4
diff --git a/network/discovery_client.gd b/network/discovery_client.gd
deleted file mode 100644
index d8e602a..0000000
--- a/network/discovery_client.gd
+++ /dev/null
@@ -1,1585 +0,0 @@
-class_name DiscoveryClient
-extends Node
-
-signal rooms_updated(rooms: Array[Dictionary])
-signal browse_status_changed(message: String, is_error: bool)
-signal host_settings_changed(room_name: String, discoverable: bool)
-signal host_status_changed(message: String, is_error: bool)
-signal host_state_changed(state: int)
-signal public_join_prepared(endpoint: String)
-signal public_join_status_changed(message: String, is_error: bool)
-signal public_join_state_changed(state: int)
-signal friend_presence_updated(friends: Array[Dictionary])
-signal presence_sharing_changed(enabled: bool)
-signal social_status_changed(message: String, is_error: bool)
-signal friend_invite_received(
- fingerprint: String,
- display_name: String,
- room: Dictionary,
-)
-signal friend_invite_finished(success: bool, message: String)
-
-const BASE_URL_SETTING: String = "network/discovery/base_url"
-const BASE_URL_ENVIRONMENT: String = "straywild_DISCOVERY_URL"
-const LEGACY_BASE_URL_ENVIRONMENT: String = "NETFISHING_DISCOVERY_URL"
-const SETTINGS_PATH: String = "user://network_discovery.cfg"
-const DEFAULT_ROOM_NAME: String = "Player's Server"
-const LEGACY_DEFAULT_ROOM_NAME: String = "straywild Room"
-const LEGACY_DEDICATED_DEFAULT_ROOM_NAME: String = (
- "straywild Dedicated Server"
-)
-const ROOM_NAME_SUFFIX: String = "'s Server"
-const MAX_ROOM_NAME_LENGTH: int = 48
-const HEARTBEAT_INTERVAL_SECONDS: float = 15.0
-const REQUEST_TIMEOUT_SECONDS: float = 8.0
-const TRAVERSAL_POLL_INTERVAL_SECONDS: float = 1.0
-const JOIN_PROBE_INTERVAL_SECONDS: float = 0.35
-const TRAVERSAL_PACKET_PREFIX: String = "straywild_TRAVERSAL_V1 "
-const UPNP_MAPPING_DURATION_SECONDS: int = 3600
-const UPNP_RENEW_INTERVAL_SECONDS: float = 2700.0
-const UPNP_RETRY_INTERVAL_SECONDS: float = 300.0
-const SOCIAL_POLL_INTERVAL_SECONDS: float = 5.0
-
-enum HostState {
- UNAVAILABLE,
- CLOSED,
- PRIVATE,
- OPENING_PORT,
- REGISTERING,
- VERIFYING,
- PUBLIC,
- ERROR,
-}
-
-enum PublicJoinState {
- IDLE,
- REQUESTING_ROUTE,
- CONNECTING,
- ERROR,
-}
-
-enum HostRequestKind {
- NONE,
- CREATE,
- UPDATE,
- DELETE,
-}
-
-var _session: NetworkSession
-var _relationships: PlayerRelationshipStore
-var _base_url: String = ""
-var _room_name: String = DEFAULT_ROOM_NAME
-var _room_name_uses_default: bool = true
-var _host_settings_persistence_enabled: bool = true
-var _discoverable: bool = false
-var _host_status_message: String = ""
-var _host_status_is_error: bool = false
-var _host_state: HostState = HostState.CLOSED
-var _public_join_state: PublicJoinState = PublicJoinState.IDLE
-var _lease_room_id: String = ""
-var _lease_token: String = ""
-var _host_request_kind: HostRequestKind = HostRequestKind.NONE
-var _host_request_in_flight: bool = false
-var _host_sync_queued: bool = false
-var _browse_request_in_flight: bool = false
-var _host_request: HTTPRequest
-var _browse_request: HTTPRequest
-var _heartbeat: Timer
-var _traversal_request: HTTPRequest
-var _join_request: HTTPRequest
-var _traversal_timer: Timer
-var _join_probe_timer: Timer
-var _host_verified: bool = false
-var _traversal_host: String = ""
-var _traversal_port: int = 0
-var _host_verification_token: String = ""
-var _join_request_in_flight: bool = false
-var _pending_join_endpoint: String = ""
-var _pending_join_token: String = ""
-var _pending_join_room_id: String = ""
-var _preserve_pending_join_on_inactive: bool = false
-var _upnp_thread: Thread
-var _upnp_mapping_in_progress: bool = false
-var _upnp_operation_is_renewal: bool = false
-var _upnp_renew_timer: Timer
-var _upnp: UPNP
-var _upnp_mapped_port: int = 0
-var _presence_sharing: bool = false
-var _social_timer: Timer
-var _presence_request: HTTPRequest
-var _presence_query_request: HTTPRequest
-var _invitation_poll_request: HTTPRequest
-var _invitation_send_request: HTTPRequest
-var _friend_presence: Dictionary[String, Dictionary] = {}
-var _published_presence_tokens: PackedStringArray = PackedStringArray()
-var _pending_presence_online: bool = false
-var _pending_presence_tokens: PackedStringArray = PackedStringArray()
-var _pending_invite_fingerprint: String = ""
-var _joined_public_room_id: String = ""
-
-
-func _ready() -> void:
- _host_request = HTTPRequest.new()
- _host_request.name = "HostLeaseRequest"
- _host_request.timeout = REQUEST_TIMEOUT_SECONDS
- add_child(_host_request)
- _host_request.request_completed.connect(_on_host_request_completed)
-
- _browse_request = HTTPRequest.new()
- _browse_request.name = "RoomBrowseRequest"
- _browse_request.timeout = REQUEST_TIMEOUT_SECONDS
- add_child(_browse_request)
- _browse_request.request_completed.connect(_on_browse_request_completed)
-
- _traversal_request = HTTPRequest.new()
- _traversal_request.name = "HostTraversalRequest"
- _traversal_request.timeout = REQUEST_TIMEOUT_SECONDS
- add_child(_traversal_request)
- _traversal_request.request_completed.connect(
- _on_traversal_request_completed
- )
-
- _join_request = HTTPRequest.new()
- _join_request.name = "PublicJoinPreparationRequest"
- _join_request.timeout = REQUEST_TIMEOUT_SECONDS
- add_child(_join_request)
- _join_request.request_completed.connect(_on_join_request_completed)
-
- _heartbeat = Timer.new()
- _heartbeat.name = "HostLeaseHeartbeat"
- _heartbeat.wait_time = HEARTBEAT_INTERVAL_SECONDS
- _heartbeat.one_shot = false
- add_child(_heartbeat)
- _heartbeat.timeout.connect(_on_heartbeat_timeout)
-
- _upnp_renew_timer = Timer.new()
- _upnp_renew_timer.name = "HostUPnPRenewal"
- _upnp_renew_timer.one_shot = true
- add_child(_upnp_renew_timer)
- _upnp_renew_timer.timeout.connect(_on_upnp_renew_timer_timeout)
-
- _traversal_timer = Timer.new()
- _traversal_timer.name = "HostTraversalPoll"
- _traversal_timer.wait_time = TRAVERSAL_POLL_INTERVAL_SECONDS
- _traversal_timer.one_shot = false
- add_child(_traversal_timer)
- _traversal_timer.timeout.connect(_on_traversal_timer_timeout)
-
- _join_probe_timer = Timer.new()
- _join_probe_timer.name = "PublicJoinTraversalProbe"
- _join_probe_timer.wait_time = JOIN_PROBE_INTERVAL_SECONDS
- _join_probe_timer.one_shot = false
- add_child(_join_probe_timer)
- _join_probe_timer.timeout.connect(_send_pending_join_probe)
-
- _presence_request = _make_social_request(
- "FriendPresencePublish", _on_presence_request_completed
- )
- _presence_query_request = _make_social_request(
- "FriendPresenceQuery", _on_presence_query_completed
- )
- _invitation_poll_request = _make_social_request(
- "FriendInvitationPoll", _on_invitation_poll_completed
- )
- _invitation_send_request = _make_social_request(
- "FriendInvitationSend", _on_invitation_send_completed
- )
- _social_timer = Timer.new()
- _social_timer.name = "FriendPresencePoll"
- _social_timer.wait_time = SOCIAL_POLL_INTERVAL_SECONDS
- _social_timer.one_shot = false
- add_child(_social_timer)
- _social_timer.timeout.connect(_refresh_social_state)
- _load_settings()
- _base_url = _configured_base_url()
-
-
-func setup(
- session: NetworkSession,
- relationships: PlayerRelationshipStore = null,
-) -> void:
- _session = session
- _relationships = relationships
- if _room_name_uses_default:
- _room_name = _default_room_name(_session.get_local_display_name())
- _save_settings()
- _session.set_session_display_name(_room_name)
- if (
- _relationships != null
- and not _relationships.relationship_changed.is_connected(
- _on_social_relationship_changed
- )
- ):
- _relationships.relationship_changed.connect(
- _on_social_relationship_changed
- )
- if not _session.state_changed.is_connected(_on_session_state_changed):
- _session.state_changed.connect(_on_session_state_changed)
- if not _session.peer_count_changed.is_connected(_on_peer_count_changed):
- _session.peer_count_changed.connect(_on_peer_count_changed)
- if not _session.host_openness_changed.is_connected(_on_host_openness_changed):
- _session.host_openness_changed.connect(_on_host_openness_changed)
- host_settings_changed.emit(_room_name, _discoverable)
- if not is_configured():
- _set_host_state(HostState.UNAVAILABLE)
- _set_host_status("Room discovery is not configured in this build.", true)
- elif _session.is_open_host():
- _set_host_state(HostState.PRIVATE)
- _set_host_status("Room is open but unlisted.", false)
- else:
- _set_host_state(HostState.CLOSED)
- _set_host_status("Open the game before listing it publicly.", false)
- if is_configured() and _relationships != null:
- _social_timer.start()
- call_deferred("_refresh_social_state")
-
-
-func is_configured() -> bool:
- return not _base_url.is_empty()
-
-
-func get_base_url() -> String:
- return _base_url
-
-
-func is_presence_sharing() -> bool:
- return _presence_sharing
-
-
-func set_presence_sharing(enabled: bool) -> bool:
- if enabled and (not is_configured() or _relationships == null):
- social_status_changed.emit(
- "Friend presence is not configured in this build.", true
- )
- return false
- if _presence_sharing == enabled:
- return true
- _presence_sharing = enabled
- _save_settings()
- presence_sharing_changed.emit(enabled)
- _refresh_social_state()
- return true
-
-
-func get_friend_presence() -> Array[Dictionary]:
- var result: Array[Dictionary] = []
- if _relationships == null:
- return result
- for friend: Dictionary in _relationships.get_friends():
- var fingerprint := str(friend.get("fingerprint", ""))
- var live: Dictionary = _friend_presence.get(fingerprint, {})
- result.append({
- "fingerprint": fingerprint,
- "display_name": str(
- friend.get("last_known_display_name", "Player")
- ),
- "online": bool(live.get("online", false)),
- "room": (
- live.get("room", {}).duplicate(true)
- if typeof(live.get("room", {})) == TYPE_DICTIONARY
- else {}
- ),
- })
- result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
- if bool(a["online"]) != bool(b["online"]):
- return bool(a["online"])
- return str(a["display_name"]).naturalnocasecmp_to(
- str(b["display_name"])
- ) < 0
- )
- return result
-
-
-func request_friend_presence() -> bool:
- if not is_configured() or _relationships == null:
- friend_presence_updated.emit(get_friend_presence())
- return false
- _refresh_social_state()
- return true
-
-
-func send_friend_invite(fingerprint: String) -> bool:
- if (
- _relationships == null
- or not _relationships.is_friend(fingerprint)
- or _relationships.is_blocked(fingerprint)
- or not is_configured()
- ):
- friend_invite_finished.emit(
- false, "This person needs to be online to do this."
- )
- return false
- if (
- _session == null
- or not _session.is_host()
- or not _host_verified
- or _lease_room_id.is_empty()
- ):
- friend_invite_finished.emit(
- false,
- "List your open room in discovery before inviting friends.",
- )
- return false
- if (
- not _pending_invite_fingerprint.is_empty()
- or _invitation_send_request.get_http_client_status()
- != HTTPClient.STATUS_DISCONNECTED
- ):
- return false
- var friend: Dictionary = _relationships.get_friend_record(fingerprint)
- var inbox_token := str(friend.get("remote_invite_token", ""))
- if inbox_token.is_empty():
- friend_invite_finished.emit(false, "Friend invitation is unavailable.")
- return false
- var error := _post_social_json(
- _invitation_send_request,
- "/v1/invitations",
- {
- "inbox_token": inbox_token,
- "room_id": _lease_room_id,
- },
- )
- if error != OK:
- friend_invite_finished.emit(
- false, "Friend invitation could not be sent."
- )
- return false
- _pending_invite_fingerprint = fingerprint
- return true
-
-
-func set_base_url_override(value: String) -> bool:
- var normalized: String = value.strip_edges()
- while normalized.ends_with("/"):
- normalized = normalized.left(normalized.length() - 1)
- if not normalized.is_empty() and not (
- normalized.begins_with("https://")
- or normalized.begins_with("http://")
- ):
- return false
- _base_url = normalized
- return true
-
-
-func get_room_name() -> String:
- return _room_name
-
-
-func is_discoverable() -> bool:
- return _discoverable
-
-
-func get_host_status_message() -> String:
- return _host_status_message
-
-
-func host_status_is_error() -> bool:
- return _host_status_is_error
-
-
-func get_host_state() -> HostState:
- return _host_state
-
-
-func get_public_join_state() -> PublicJoinState:
- return _public_join_state
-
-
-func configure_dedicated_runtime(room_name: String) -> bool:
- _host_settings_persistence_enabled = false
- return set_room_name(room_name)
-
-
-func set_room_name(value: String) -> bool:
- var cleaned: String = _sanitize_room_name(value)
- if cleaned.is_empty():
- _set_host_state(HostState.ERROR)
- _set_host_status("Room name cannot be empty.", true)
- return false
- if cleaned == _room_name:
- if _room_name_uses_default:
- _room_name_uses_default = false
- _save_settings()
- return true
- _room_name = cleaned
- _room_name_uses_default = false
- _save_settings()
- if _session != null:
- _session.set_session_display_name(_room_name)
- host_settings_changed.emit(_room_name, _discoverable)
- if _discoverable:
- _synchronize_host_lease()
- return true
-
-
-func set_discoverable(enabled: bool) -> bool:
- if enabled and (
- _session == null
- or not _session.is_open_host()
- or not is_configured()
- ):
- _set_host_state(HostState.ERROR)
- _set_host_status(
- "Open the game before enabling discovery."
- if is_configured()
- else "Room discovery is not configured in this build.",
- true,
- )
- return false
- if _discoverable == enabled:
- return true
- _discoverable = enabled
- host_settings_changed.emit(_room_name, _discoverable)
- if enabled:
- _heartbeat.start()
- _set_host_state(
- HostState.REGISTERING
- if _session.is_dedicated_host()
- else HostState.OPENING_PORT
- )
- _set_host_status("Opening public room…", false)
- if _session.is_dedicated_host():
- _synchronize_host_lease()
- else:
- _begin_upnp_mapping(_session.get_host_port())
- else:
- _heartbeat.stop()
- _upnp_renew_timer.stop()
- _traversal_timer.stop()
- _remove_host_lease()
- _begin_upnp_cleanup()
- _set_host_state(HostState.PRIVATE)
- _set_host_status("Room is open but unlisted.", false)
- return true
-
-
-func is_public_join_preparing() -> bool:
- return _join_request_in_flight
-
-
-func preserve_public_join_for_session_switch() -> bool:
- _preserve_pending_join_on_inactive = not _pending_join_token.is_empty()
- return _preserve_pending_join_on_inactive
-
-
-func cancel_pending_public_join() -> void:
- _preserve_pending_join_on_inactive = false
- _set_public_join_state(PublicJoinState.IDLE)
- _clear_pending_join()
-
-
-func prepare_public_join(room: Dictionary) -> bool:
- if _join_request_in_flight:
- return false
- if _session == null or not is_configured():
- _set_public_join_state(PublicJoinState.ERROR)
- return false
- if is_own_room(room):
- _set_public_join_state(PublicJoinState.ERROR)
- public_join_status_changed.emit(
- "You are already hosting this room.", true
- )
- return false
- var endpoint: String = room_endpoint(room)
- var room_id: String = str(room.get("room_id", "")).strip_edges()
- if endpoint.is_empty() or room_id.is_empty():
- _set_public_join_state(PublicJoinState.ERROR)
- return false
- _pending_join_endpoint = endpoint
- _pending_join_room_id = room_id
- _pending_join_token = ""
- var url: String = "%s/v1/rooms/%s/join-attempts" % [
- _base_url,
- room_id.uri_encode(),
- ]
- var error: Error = _join_request.request(
- url,
- PackedStringArray(),
- HTTPClient.METHOD_POST,
- "",
- )
- if error != OK:
- _set_public_join_state(PublicJoinState.ERROR)
- _clear_pending_join()
- return false
- _join_request_in_flight = true
- _set_public_join_state(PublicJoinState.REQUESTING_ROUTE)
- public_join_status_changed.emit("Opening a route to the room…", false)
- return true
-
-
-func request_rooms() -> bool:
- if not is_configured():
- rooms_updated.emit(_empty_rooms())
- browse_status_changed.emit(
- "Public room discovery is not configured in this build.", true
- )
- return false
- if _browse_request_in_flight:
- return true
- var game_version: String = str(
- ProjectSettings.get_setting("application/config/version", "unknown")
- )
- var url: String = "%s/v1/rooms?game_version=%s&protocol_version=%d" % [
- _base_url,
- game_version.uri_encode(),
- NetworkProtocol.PROTOCOL_VERSION,
- ]
- var error: Error = _browse_request.request(url)
- if error != OK:
- rooms_updated.emit(_empty_rooms())
- browse_status_changed.emit("Could not request public rooms.", true)
- return false
- _browse_request_in_flight = true
- browse_status_changed.emit("Looking for public rooms…", false)
- return true
-
-
-func room_endpoint(room: Dictionary) -> String:
- var address: String = str(room.get("address", "")).strip_edges()
- var port: int = int(room.get("port", 0))
- if address.is_empty() or port < 1 or port > 65535:
- return ""
- if ":" in address and not address.begins_with("["):
- address = "[%s]" % address
- return "%s:%d" % [address, port]
-
-
-func is_own_room(room: Dictionary) -> bool:
- return (
- not _lease_room_id.is_empty()
- and str(room.get("room_id", "")) == _lease_room_id
- )
-
-
-func _configured_base_url() -> String:
- var value: String = OS.get_environment(BASE_URL_ENVIRONMENT).strip_edges()
- if value.is_empty():
- value = OS.get_environment(
- LEGACY_BASE_URL_ENVIRONMENT
- ).strip_edges()
- if value.is_empty():
- value = str(ProjectSettings.get_setting(BASE_URL_SETTING, "")).strip_edges()
- while value.ends_with("/"):
- value = value.left(value.length() - 1)
- if not value.is_empty() and not (
- value.begins_with("https://") or value.begins_with("http://")
- ):
- push_warning("Ignored invalid straywild discovery URL.")
- return ""
- return value
-
-
-func _on_heartbeat_timeout() -> void:
- _synchronize_host_lease()
- # NAT bindings can change during a long host session. Re-sending the signed
- # probe lets discovery refresh the authoritative public endpoint in place.
- _send_host_verification_probe()
-
-
-func _synchronize_host_lease() -> void:
- if not _should_advertise():
- if not _lease_room_id.is_empty():
- _remove_host_lease()
- return
- if _host_request_in_flight:
- _host_sync_queued = true
- return
- var method: HTTPClient.Method = HTTPClient.METHOD_POST
- var url: String = "%s/v1/rooms" % _base_url
- var headers := PackedStringArray(["Content-Type: application/json"])
- _host_request_kind = HostRequestKind.CREATE
- if not _lease_room_id.is_empty():
- method = HTTPClient.METHOD_PUT
- url = "%s/v1/rooms/%s" % [_base_url, _lease_room_id.uri_encode()]
- headers.append("Authorization: Bearer %s" % _lease_token)
- _host_request_kind = HostRequestKind.UPDATE
- elif _host_state != HostState.REGISTERING:
- _set_host_state(HostState.REGISTERING)
- _start_host_request(url, headers, method, JSON.stringify(_host_payload()))
-
-
-func _remove_host_lease() -> void:
- if _lease_room_id.is_empty():
- return
- if _host_request_in_flight:
- _host_sync_queued = true
- return
- var url: String = "%s/v1/rooms/%s" % [
- _base_url, _lease_room_id.uri_encode()
- ]
- var headers := PackedStringArray([
- "Authorization: Bearer %s" % _lease_token,
- ])
- _host_request_kind = HostRequestKind.DELETE
- _start_host_request(url, headers, HTTPClient.METHOD_DELETE, "")
-
-
-func _start_host_request(
- url: String,
- headers: PackedStringArray,
- method: HTTPClient.Method,
- body: String,
-) -> void:
- var error: Error = _host_request.request(url, headers, method, body)
- if error != OK:
- _host_request_kind = HostRequestKind.NONE
- _set_host_state(HostState.ERROR)
- _set_host_status("Could not contact room discovery.", true)
- return
- _host_request_in_flight = true
-
-
-func _host_payload() -> Dictionary:
- return {
- "room_name": _room_name,
- "port": _session.get_host_port(),
- "current_players": _session.get_player_count(),
- "max_players": _session.get_session_max_players(),
- "game_version": str(
- ProjectSettings.get_setting("application/config/version", "unknown")
- ),
- "protocol_version": NetworkProtocol.PROTOCOL_VERSION,
- }
-
-
-func _should_advertise() -> bool:
- return (
- _discoverable
- and is_configured()
- and _session != null
- and _session.is_open_host()
- and (
- not _upnp_mapping_in_progress
- or _upnp_operation_is_renewal
- )
- )
-
-
-func _on_host_request_completed(
- result: int,
- response_code: int,
- _headers: PackedStringArray,
- body: PackedByteArray,
-) -> void:
- var completed_kind: HostRequestKind = _host_request_kind
- _host_request_kind = HostRequestKind.NONE
- _host_request_in_flight = false
- var response: Dictionary = _parse_response_dictionary(body)
- var transport_ok: bool = result == HTTPRequest.RESULT_SUCCESS
- match completed_kind:
- HostRequestKind.CREATE:
- if transport_ok and response_code == HTTPClient.RESPONSE_CREATED:
- var room: Dictionary = response.get("room", {})
- _lease_room_id = str(room.get("room_id", ""))
- _lease_token = str(response.get("lease_token", ""))
- if _lease_room_id.is_empty() or _lease_token.is_empty():
- _clear_lease()
- _set_host_state(HostState.ERROR)
- _set_host_status("Discovery returned an invalid lease.", true)
- else:
- _apply_traversal_response(response)
- _host_verified = bool(room.get("verified", false))
- _traversal_timer.start()
- _send_host_verification_probe()
- _set_host_state(
- HostState.PUBLIC
- if _host_verified
- else HostState.VERIFYING
- )
- _set_host_status(
- "Room is listed publicly."
- if _host_verified
- else "Checking the public route…",
- false,
- )
- else:
- _set_host_state(HostState.ERROR)
- _set_host_status(_request_failure(response), true)
- HostRequestKind.UPDATE:
- if transport_ok and response_code == HTTPClient.RESPONSE_OK:
- var room: Dictionary = response.get("room", {})
- _host_verified = bool(room.get("verified", false))
- _set_host_state(
- HostState.PUBLIC
- if _host_verified
- else HostState.VERIFYING
- )
- _set_host_status(
- "Room is listed publicly."
- if _host_verified
- else "Checking the public route…",
- false,
- )
- elif response_code in [
- HTTPClient.RESPONSE_UNAUTHORIZED,
- HTTPClient.RESPONSE_NOT_FOUND,
- ]:
- _clear_lease()
- _host_sync_queued = true
- else:
- _set_host_state(HostState.ERROR)
- _set_host_status(_request_failure(response), true)
- HostRequestKind.DELETE:
- _clear_lease()
- if not transport_ok or response_code not in [
- HTTPClient.RESPONSE_NO_CONTENT,
- HTTPClient.RESPONSE_NOT_FOUND,
- ]:
- _set_host_state(HostState.ERROR)
- _set_host_status(_request_failure(response), true)
- _:
- pass
- if not _should_advertise() and not _lease_room_id.is_empty():
- _host_sync_queued = true
- if _host_sync_queued:
- _host_sync_queued = false
- call_deferred("_synchronize_host_lease")
-
-
-func _on_join_request_completed(
- result: int,
- response_code: int,
- _headers: PackedStringArray,
- body: PackedByteArray,
-) -> void:
- _join_request_in_flight = false
- var response: Dictionary = _parse_response_dictionary(body)
- if (
- result != HTTPRequest.RESULT_SUCCESS
- or response_code != HTTPClient.RESPONSE_CREATED
- ):
- _set_public_join_state(PublicJoinState.ERROR)
- public_join_status_changed.emit(_request_failure(response), true)
- _clear_pending_join()
- return
- _pending_join_token = str(response.get("join_token", ""))
- _apply_traversal_response(response)
- if (
- _pending_join_token.is_empty()
- or _traversal_host.is_empty()
- or _traversal_port < 1
- ):
- _set_public_join_state(PublicJoinState.ERROR)
- public_join_status_changed.emit(
- "Discovery returned an invalid traversal route.", true
- )
- _clear_pending_join()
- return
- _set_public_join_state(PublicJoinState.CONNECTING)
- public_join_status_changed.emit("Connecting…", false)
- public_join_prepared.emit(_pending_join_endpoint)
-
-
-func _on_traversal_timer_timeout() -> void:
- if not _should_advertise() or _lease_room_id.is_empty():
- _traversal_timer.stop()
- return
- if not _host_verified:
- _send_host_verification_probe()
- _synchronize_host_lease()
- return
- if _traversal_request.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
- return
- var url: String = "%s/v1/rooms/%s/join-attempts" % [
- _base_url,
- _lease_room_id.uri_encode(),
- ]
- var headers := PackedStringArray([
- "Authorization: Bearer %s" % _lease_token,
- ])
- var error: Error = _traversal_request.request(url, headers)
- if error != OK:
- _set_host_state(HostState.ERROR)
- _set_host_status("Could not check incoming public connections.", true)
-
-
-func _on_traversal_request_completed(
- result: int,
- response_code: int,
- _headers: PackedStringArray,
- body: PackedByteArray,
-) -> void:
- if result != HTTPRequest.RESULT_SUCCESS or response_code != HTTPClient.RESPONSE_OK:
- return
- var response: Dictionary = _parse_response_dictionary(body)
- var endpoints: Variant = response.get("endpoints", [])
- if typeof(endpoints) != TYPE_ARRAY:
- return
- for value: Variant in endpoints:
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var endpoint: Dictionary = value
- var address: String = str(endpoint.get("address", ""))
- var port: int = int(endpoint.get("port", 0))
- if address.is_empty() or port < 1 or port > 65535:
- continue
- var punch: PackedByteArray = "straywild_PUNCH_V1".to_utf8_buffer()
- for _attempt: int in 3:
- _session.send_traversal_packet(address, port, punch)
-
-
-func _apply_traversal_response(response: Dictionary) -> void:
- var traversal: Variant = response.get("traversal", {})
- if typeof(traversal) != TYPE_DICTIONARY:
- return
- var details: Dictionary = traversal
- _traversal_host = str(details.get("host", "")).strip_edges()
- _traversal_port = int(details.get("port", 0))
- _host_verification_token = str(
- details.get("verification_token", "")
- )
-
-
-func _send_host_verification_probe() -> void:
- if (
- _session == null
- or _lease_room_id.is_empty()
- or _host_verification_token.is_empty()
- ):
- return
- _send_traversal_probe({
- "kind": "host",
- "room_id": _lease_room_id,
- "token": _host_verification_token,
- })
-
-
-func _send_pending_join_probe() -> void:
- if (
- _session == null
- or _pending_join_token.is_empty()
- or _session.state not in [
- NetworkSession.State.CONNECTING,
- NetworkSession.State.AUTHENTICATING,
- ]
- ):
- _join_probe_timer.stop()
- return
- _send_traversal_probe({
- "kind": "join",
- "room_id": _pending_join_room_id,
- "token": _pending_join_token,
- })
-
-
-func _send_traversal_probe(payload: Dictionary) -> void:
- if _traversal_host.is_empty() or _traversal_port < 1:
- return
- var packet: PackedByteArray = (
- TRAVERSAL_PACKET_PREFIX + JSON.stringify(payload)
- ).to_utf8_buffer()
- _session.send_traversal_packet(_traversal_host, _traversal_port, packet)
-
-
-func _begin_upnp_mapping(port: int, is_renewal: bool = false) -> void:
- if _upnp_mapping_in_progress or port < 1:
- if not is_renewal:
- _synchronize_host_lease()
- return
- _upnp_mapping_in_progress = true
- _upnp_operation_is_renewal = is_renewal
- _upnp_thread = Thread.new()
- var error: Error = _upnp_thread.start(
- _configure_upnp_mapping.bind(port)
- )
- if error != OK:
- _upnp_thread = null
- _upnp_mapping_in_progress = false
- _upnp_operation_is_renewal = false
- if is_renewal:
- _schedule_upnp_renewal(UPNP_RETRY_INTERVAL_SECONDS)
- _synchronize_host_lease()
-
-
-func _configure_upnp_mapping(port: int) -> Dictionary:
- var upnp := UPNP.new()
- var discovery_result: int = upnp.discover()
- if discovery_result != UPNP.UPNP_RESULT_SUCCESS:
- return {"success": false, "upnp": upnp, "port": port}
- var mapping_result: int = upnp.add_port_mapping(
- port,
- port,
- "straywild",
- "UDP",
- UPNP_MAPPING_DURATION_SECONDS,
- )
- if mapping_result == UPNP.UPNP_RESULT_ONLY_PERMANENT_LEASE_SUPPORTED:
- mapping_result = upnp.add_port_mapping(
- port, port, "straywild", "UDP", 0
- )
- return {
- "success": mapping_result == UPNP.UPNP_RESULT_SUCCESS,
- "upnp": upnp,
- "port": port,
- }
-
-
-func _process(_delta: float) -> void:
- if _upnp_thread == null or _upnp_thread.is_alive():
- return
- var was_renewal: bool = _upnp_operation_is_renewal
- var result: Variant = _upnp_thread.wait_to_finish()
- _upnp_thread = null
- _upnp_mapping_in_progress = false
- _upnp_operation_is_renewal = false
- var mapping_succeeded: bool = false
- if typeof(result) == TYPE_DICTIONARY:
- var details: Dictionary = result
- if bool(details.get("success", false)):
- _upnp = details.get("upnp") as UPNP
- _upnp_mapped_port = int(details.get("port", 0))
- mapping_succeeded = true
- if not _discoverable:
- _begin_upnp_cleanup()
- return
- if mapping_succeeded:
- _schedule_upnp_renewal(UPNP_RENEW_INTERVAL_SECONDS)
- elif was_renewal:
- _schedule_upnp_renewal(UPNP_RETRY_INTERVAL_SECONDS)
- _synchronize_host_lease()
-
-
-func _schedule_upnp_renewal(delay_seconds: float) -> void:
- if (
- _upnp_mapped_port < 1
- or not _discoverable
- or _session == null
- or _session.is_dedicated_host()
- ):
- _upnp_renew_timer.stop()
- return
- _upnp_renew_timer.start(delay_seconds)
-
-
-func _on_upnp_renew_timer_timeout() -> void:
- if (
- not _discoverable
- or _session == null
- or not _session.is_open_host()
- or _session.is_dedicated_host()
- or _upnp_mapped_port < 1
- ):
- return
- _begin_upnp_mapping(_upnp_mapped_port, true)
-
-
-func _begin_upnp_cleanup() -> void:
- _upnp_renew_timer.stop()
- if _upnp == null or _upnp_mapped_port < 1:
- return
- _upnp.delete_port_mapping(_upnp_mapped_port, "UDP")
- _upnp = null
- _upnp_mapped_port = 0
-
-
-func _on_browse_request_completed(
- result: int,
- response_code: int,
- _headers: PackedStringArray,
- body: PackedByteArray,
-) -> void:
- _browse_request_in_flight = false
- var response: Dictionary = _parse_response_dictionary(body)
- if result != HTTPRequest.RESULT_SUCCESS or response_code != HTTPClient.RESPONSE_OK:
- rooms_updated.emit(_empty_rooms())
- browse_status_changed.emit(_request_failure(response), true)
- return
- var raw_rooms: Variant = response.get("rooms", [])
- if typeof(raw_rooms) != TYPE_ARRAY:
- rooms_updated.emit(_empty_rooms())
- browse_status_changed.emit("Discovery returned an invalid room list.", true)
- return
- var rooms: Array[Dictionary] = []
- for value: Variant in raw_rooms:
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var room: Dictionary = value
- if _valid_public_room(room):
- rooms.append(room.duplicate(true))
- rooms_updated.emit(rooms)
- browse_status_changed.emit(
- "No public rooms are available."
- if rooms.is_empty()
- else "%d public room%s found." % [rooms.size(), "" if rooms.size() == 1 else "s"],
- false,
- )
-
-
-func _valid_public_room(room: Dictionary) -> bool:
- var expected_version: String = str(
- ProjectSettings.get_setting("application/config/version", "unknown")
- )
- return (
- typeof(room.get("room_id")) == TYPE_STRING
- and typeof(room.get("room_name")) == TYPE_STRING
- and typeof(room.get("address")) == TYPE_STRING
- and typeof(room.get("game_version")) == TYPE_STRING
- and str(room.get("game_version")) == expected_version
- and _valid_json_integer(room.get("port"), 1, 65535)
- and _valid_json_integer(room.get("current_players"), 0, 128)
- and _valid_json_integer(room.get("max_players"), 1, 128)
- and int(room["current_players"]) <= int(room["max_players"])
- and _valid_json_integer(
- room.get("protocol_version"),
- NetworkProtocol.PROTOCOL_VERSION,
- NetworkProtocol.PROTOCOL_VERSION,
- )
- )
-
-
-func _valid_json_integer(value: Variant, minimum: int, maximum: int) -> bool:
- if typeof(value) == TYPE_INT:
- return int(value) >= minimum and int(value) <= maximum
- if typeof(value) != TYPE_FLOAT:
- return false
- var number: float = float(value)
- return (
- is_finite(number)
- and number == floor(number)
- and number >= float(minimum)
- and number <= float(maximum)
- )
-
-
-func _make_social_request(name: String, callback: Callable) -> HTTPRequest:
- var request := HTTPRequest.new()
- request.name = name
- request.timeout = REQUEST_TIMEOUT_SECONDS
- add_child(request)
- request.request_completed.connect(callback)
- return request
-
-
-func _social_payload(fields: Dictionary = {}) -> Dictionary:
- var payload: Dictionary = fields.duplicate(true)
- payload["game_version"] = NetworkProtocol.game_version()
- payload["protocol_version"] = NetworkProtocol.PROTOCOL_VERSION
- return payload
-
-
-func _post_social_json(
- request: HTTPRequest,
- path: String,
- fields: Dictionary,
-) -> Error:
- return request.request(
- _base_url + path,
- PackedStringArray(["Content-Type: application/json"]),
- HTTPClient.METHOD_POST,
- JSON.stringify(_social_payload(fields)),
- )
-
-
-func _refresh_social_state() -> void:
- if not is_configured() or _relationships == null:
- return
- _publish_friend_presence()
- _query_friend_presence()
- _poll_friend_invitations()
-
-
-func _publish_friend_presence() -> void:
- if (
- _presence_request.get_http_client_status()
- != HTTPClient.STATUS_DISCONNECTED
- ):
- return
- var current_tokens := _friend_social_values(
- "local_presence_write_token"
- )
- var online := _presence_sharing and not current_tokens.is_empty()
- var publish_online := online
- var tokens := PackedStringArray()
- if online:
- # Revoke removed or blocked friendships before refreshing the remaining
- # capabilities. This makes status disappear on the next request instead of
- # waiting for the server's short presence TTL.
- tokens = _presence_tokens_not_in(
- _published_presence_tokens, current_tokens
- )
- if not tokens.is_empty():
- publish_online = false
- else:
- tokens = current_tokens
- else:
- tokens = _published_presence_tokens
- if tokens.is_empty():
- return
- var error := _post_social_json(
- _presence_request,
- "/v1/presence",
- {
- "display_name": _session.get_local_display_name(),
- "room_id": _current_presence_room_id(),
- "online": publish_online,
- "write_tokens": Array(tokens),
- },
- )
- if error == OK:
- _pending_presence_online = publish_online
- _pending_presence_tokens = tokens.duplicate()
- else:
- social_status_changed.emit("Could not update friend presence.", true)
-
-
-func _query_friend_presence() -> void:
- if (
- _presence_query_request.get_http_client_status()
- != HTTPClient.STATUS_DISCONNECTED
- ):
- return
- var channels := _friend_social_values("remote_presence_channel")
- if channels.is_empty():
- var had_presence := not _friend_presence.is_empty()
- _friend_presence.clear()
- if had_presence:
- friend_presence_updated.emit(get_friend_presence())
- return
- var error := _post_social_json(
- _presence_query_request,
- "/v1/presence/query",
- {"channels": Array(channels)},
- )
- if error != OK:
- social_status_changed.emit("Could not refresh friend status.", true)
-
-
-func _poll_friend_invitations() -> void:
- if (
- _invitation_poll_request.get_http_client_status()
- != HTTPClient.STATUS_DISCONNECTED
- ):
- return
- var tokens := _friend_social_values("local_invite_token")
- if tokens.is_empty():
- return
- var error := _post_social_json(
- _invitation_poll_request,
- "/v1/invitations/poll",
- {"inbox_tokens": Array(tokens)},
- )
- if error != OK:
- social_status_changed.emit("Could not check friend invitations.", true)
-
-
-func _friend_social_values(key: String) -> PackedStringArray:
- var result := PackedStringArray()
- if _relationships == null:
- return result
- for friend: Dictionary in _relationships.get_friends():
- var value := str(friend.get(key, ""))
- if not value.is_empty() and value not in result:
- result.append(value)
- return result
-
-
-func _current_presence_room_id() -> String:
- if _session == null:
- return ""
- if _session.is_host() and _host_verified:
- return _lease_room_id
- if _session.is_joined_client():
- return _joined_public_room_id
- return ""
-
-
-func _on_presence_request_completed(
- result: int,
- response_code: int,
- _headers: PackedStringArray,
- body: PackedByteArray,
-) -> void:
- var published_online := _pending_presence_online
- var request_tokens := _pending_presence_tokens.duplicate()
- _pending_presence_online = false
- _pending_presence_tokens = PackedStringArray()
- var response := _parse_response_dictionary(body)
- if result != HTTPRequest.RESULT_SUCCESS or response_code != HTTPClient.RESPONSE_OK:
- social_status_changed.emit(_request_failure(response), true)
- return
- if published_online:
- for token: String in request_tokens:
- if token not in _published_presence_tokens:
- _published_presence_tokens.append(token)
- else:
- for token: String in request_tokens:
- var index := _published_presence_tokens.find(token)
- if index >= 0:
- _published_presence_tokens.remove_at(index)
- if _presence_publish_needs_follow_up():
- call_deferred("_publish_friend_presence")
-
-
-func _presence_publish_needs_follow_up() -> bool:
- var desired := (
- _friend_social_values("local_presence_write_token")
- if _presence_sharing
- else PackedStringArray()
- )
- return (
- not _presence_tokens_not_in(
- _published_presence_tokens, desired
- ).is_empty()
- or not _presence_tokens_not_in(
- desired, _published_presence_tokens
- ).is_empty()
- )
-
-
-func _presence_tokens_not_in(
- first: PackedStringArray,
- second: PackedStringArray,
-) -> PackedStringArray:
- var result := PackedStringArray()
- for token: String in first:
- if token not in second:
- result.append(token)
- return result
-
-
-func _on_presence_query_completed(
- result: int,
- response_code: int,
- _headers: PackedStringArray,
- body: PackedByteArray,
-) -> void:
- var response := _parse_response_dictionary(body)
- if result != HTTPRequest.RESULT_SUCCESS or response_code != HTTPClient.RESPONSE_OK:
- social_status_changed.emit(_request_failure(response), true)
- return
- var by_channel: Dictionary[String, String] = {}
- for friend: Dictionary in _relationships.get_friends():
- by_channel[str(friend.get("remote_presence_channel", ""))] = str(
- friend.get("fingerprint", "")
- )
- var next_presence: Dictionary[String, Dictionary] = {}
- var raw_presence: Variant = response.get("presence", [])
- if typeof(raw_presence) == TYPE_ARRAY:
- for value: Variant in raw_presence:
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var presence: Dictionary = value
- var fingerprint := str(
- by_channel.get(str(presence.get("channel", "")), "")
- )
- if fingerprint.is_empty() or _relationships.is_blocked(fingerprint):
- continue
- var room: Dictionary = {}
- var raw_room: Variant = presence.get("room", {})
- if typeof(raw_room) == TYPE_DICTIONARY and _valid_public_room(raw_room):
- room = (raw_room as Dictionary).duplicate(true)
- next_presence[fingerprint] = {
- "online": true,
- "room": room,
- }
- if next_presence == _friend_presence:
- return
- _friend_presence = next_presence
- friend_presence_updated.emit(get_friend_presence())
-
-
-func _on_invitation_poll_completed(
- result: int,
- response_code: int,
- _headers: PackedStringArray,
- body: PackedByteArray,
-) -> void:
- var response := _parse_response_dictionary(body)
- if result != HTTPRequest.RESULT_SUCCESS or response_code != HTTPClient.RESPONSE_OK:
- return
- var by_inbox: Dictionary[String, Dictionary] = {}
- for friend: Dictionary in _relationships.get_friends():
- var token := str(friend.get("local_invite_token", ""))
- if not token.is_empty():
- by_inbox[PlayerRelationshipStore.invite_inbox_id(token)] = friend
- var invitations: Variant = response.get("invitations", [])
- if typeof(invitations) != TYPE_ARRAY:
- return
- for value: Variant in invitations:
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var invitation: Dictionary = value
- var friend: Dictionary = by_inbox.get(
- str(invitation.get("inbox_id", "")), {}
- )
- var raw_room: Variant = invitation.get("room", {})
- if friend.is_empty() or typeof(raw_room) != TYPE_DICTIONARY:
- continue
- var room := raw_room as Dictionary
- if not _valid_public_room(room):
- continue
- var fingerprint := str(friend.get("fingerprint", ""))
- if _relationships.is_blocked(fingerprint):
- continue
- friend_invite_received.emit(
- fingerprint,
- str(friend.get("last_known_display_name", "Player")),
- room.duplicate(true),
- )
-
-
-func _on_invitation_send_completed(
- result: int,
- response_code: int,
- _headers: PackedStringArray,
- body: PackedByteArray,
-) -> void:
- _pending_invite_fingerprint = ""
- var response := _parse_response_dictionary(body)
- var success := (
- result == HTTPRequest.RESULT_SUCCESS
- and response_code == HTTPClient.RESPONSE_CREATED
- )
- friend_invite_finished.emit(
- success,
- "Invitation sent." if success else _request_failure(response),
- )
-
-
-func _on_social_relationship_changed(_fingerprint: String) -> void:
- for fingerprint: String in _friend_presence.keys():
- if not _relationships.is_friend(fingerprint):
- _friend_presence.erase(fingerprint)
- friend_presence_updated.emit(get_friend_presence())
- call_deferred("_refresh_social_state")
-
-
-func _parse_response_dictionary(body: PackedByteArray) -> Dictionary:
- if body.is_empty():
- return {}
- var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
- return parsed if typeof(parsed) == TYPE_DICTIONARY else {}
-
-
-func _request_failure(response: Dictionary) -> String:
- var error: Variant = response.get("error", {})
- if typeof(error) == TYPE_DICTIONARY:
- var details := error as Dictionary
- if str(details.get("code", "")) == "game_version_mismatch":
- var required_version: String = str(
- details.get("required_game_version", "")
- ).strip_edges()
- if not required_version.is_empty():
- return _discovery_version_mismatch_message(required_version)
- var message: String = str(details.get("message", "")).strip_edges()
- if not message.is_empty():
- return message
- return "Room discovery is temporarily unavailable."
-
-
-func _discovery_version_mismatch_message(required_version: String) -> String:
- var local_version: String = NetworkProtocol.game_version()
- var rejection: NetworkProtocol.RejectionCode = (
- NetworkProtocol.game_version_rejection(
- local_version,
- required_version,
- )
- )
- match rejection:
- NetworkProtocol.RejectionCode.CLIENT_OUTDATED:
- return (
- "Your straywild version is out of date. Update to %s to "
- + "enable public discovery. This room will not be listed "
- + "until you update."
- ) % required_version
- NetworkProtocol.RejectionCode.SERVER_OUTDATED:
- return (
- "Public discovery is still on straywild %s. This room "
- + "will not be listed until discovery is updated for %s."
- ) % [required_version, local_version]
- _:
- return (
- "Public discovery requires straywild %s. This room will "
- + "not be listed until both versions match."
- ) % required_version
-
-
-func _on_session_state_changed(state: NetworkSession.State) -> void:
- if state == NetworkSession.State.JOINED_CLIENT:
- if not _pending_join_room_id.is_empty():
- _joined_public_room_id = _pending_join_room_id
- elif state in [
- NetworkSession.State.PRIVATE_HOST,
- NetworkSession.State.OPEN_HOST,
- NetworkSession.State.SERVER_LOST,
- NetworkSession.State.CONNECTION_FAILED,
- ]:
- _joined_public_room_id = ""
- elif (
- state == NetworkSession.State.INACTIVE
- and not _preserve_pending_join_on_inactive
- ):
- _joined_public_room_id = ""
- if state == NetworkSession.State.CONNECTING and not _pending_join_token.is_empty():
- _preserve_pending_join_on_inactive = false
- _set_public_join_state(PublicJoinState.CONNECTING)
- _join_probe_timer.start()
- call_deferred("_send_pending_join_probe")
- elif state in [
- NetworkSession.State.JOINED_CLIENT,
- NetworkSession.State.SERVER_LOST,
- ]:
- _preserve_pending_join_on_inactive = false
- _set_public_join_state(PublicJoinState.IDLE)
- _join_probe_timer.stop()
- _clear_pending_join()
- elif state == NetworkSession.State.INACTIVE:
- if (
- _preserve_pending_join_on_inactive
- and not _pending_join_token.is_empty()
- ):
- _preserve_pending_join_on_inactive = false
- else:
- _set_public_join_state(PublicJoinState.IDLE)
- _join_probe_timer.stop()
- _clear_pending_join()
- elif state == NetworkSession.State.CONNECTION_FAILED:
- _preserve_pending_join_on_inactive = false
- _set_public_join_state(PublicJoinState.ERROR)
- _join_probe_timer.stop()
- _clear_pending_join()
- if state == NetworkSession.State.OPEN_HOST:
- if _discoverable:
- _heartbeat.start()
- _synchronize_host_lease()
- else:
- _set_host_state(HostState.PRIVATE)
- _set_host_status("Room is open but unlisted.", false)
- return
- if state in [
- NetworkSession.State.PRIVATE_HOST,
- NetworkSession.State.INACTIVE,
- NetworkSession.State.DISCONNECTING,
- NetworkSession.State.CONNECTION_FAILED,
- NetworkSession.State.SERVER_LOST,
- ]:
- if _discoverable:
- _discoverable = false
- host_settings_changed.emit(_room_name, false)
- _heartbeat.stop()
- _upnp_renew_timer.stop()
- _remove_host_lease()
- _begin_upnp_cleanup()
- if state == NetworkSession.State.PRIVATE_HOST:
- _set_host_state(HostState.CLOSED)
- _set_host_status("Open the game before listing it publicly.", false)
- else:
- _set_host_state(HostState.CLOSED)
- call_deferred("_refresh_social_state")
-
-
-func _on_host_openness_changed(is_open: bool) -> void:
- if not is_open and _discoverable:
- set_discoverable(false)
-
-
-func _on_peer_count_changed(_player_count: int, _max_players: int) -> void:
- if _discoverable:
- _synchronize_host_lease()
-
-
-func _clear_lease() -> void:
- _lease_room_id = ""
- _lease_token = ""
- _host_verified = false
- _host_verification_token = ""
- _traversal_timer.stop()
-
-
-func _clear_pending_join() -> void:
- _pending_join_endpoint = ""
- _pending_join_token = ""
- _pending_join_room_id = ""
- _join_probe_timer.stop()
-
-
-func _exit_tree() -> void:
- _upnp_renew_timer.stop()
- if _upnp_thread != null:
- var result: Variant = _upnp_thread.wait_to_finish()
- _upnp_thread = null
- if typeof(result) == TYPE_DICTIONARY:
- var details: Dictionary = result
- if bool(details.get("success", false)):
- _upnp = details.get("upnp") as UPNP
- _upnp_mapped_port = int(details.get("port", 0))
- _begin_upnp_cleanup()
-
-
-func _set_host_status(message: String, is_error: bool) -> void:
- _host_status_message = message
- _host_status_is_error = is_error
- host_status_changed.emit(message, is_error)
-
-
-func _set_host_state(state: HostState) -> void:
- if _host_state == state:
- return
- _host_state = state
- host_state_changed.emit(int(state))
-
-
-func _set_public_join_state(state: PublicJoinState) -> void:
- if _public_join_state == state:
- return
- _public_join_state = state
- public_join_state_changed.emit(int(state))
-
-
-func _empty_rooms() -> Array[Dictionary]:
- return []
-
-
-func _sanitize_room_name(value: String) -> String:
- var cleaned: String = value.strip_edges().replace("\n", " ").replace("\r", " ")
- cleaned = cleaned.replace("\t", " ")
- while " " in cleaned:
- cleaned = cleaned.replace(" ", " ")
- return cleaned.left(MAX_ROOM_NAME_LENGTH)
-
-
-func _default_room_name(player_name: String) -> String:
- var cleaned_name: String = player_name.strip_edges()
- if cleaned_name.is_empty():
- return DEFAULT_ROOM_NAME
- return "%s%s" % [
- cleaned_name.left(MAX_ROOM_NAME_LENGTH - ROOM_NAME_SUFFIX.length()),
- ROOM_NAME_SUFFIX,
- ]
-
-
-func _load_settings() -> void:
- var config := ConfigFile.new()
- if config.load(SETTINGS_PATH) == OK:
- var saved_name: String = _sanitize_room_name(
- str(config.get_value("host", "room_name", DEFAULT_ROOM_NAME))
- )
- if not saved_name.is_empty():
- _room_name = saved_name
- _room_name_uses_default = bool(config.get_value(
- "host",
- "room_name_uses_default",
- saved_name in [
- DEFAULT_ROOM_NAME,
- LEGACY_DEFAULT_ROOM_NAME,
- LEGACY_DEDICATED_DEFAULT_ROOM_NAME,
- ],
- ))
- _presence_sharing = bool(config.get_value(
- "social", "share_presence", false
- ))
-
-
-func _save_settings() -> void:
- if not _host_settings_persistence_enabled:
- return
- var config := ConfigFile.new()
- config.set_value("host", "room_name", _room_name)
- config.set_value(
- "host", "room_name_uses_default", _room_name_uses_default
- )
- config.set_value("social", "share_presence", _presence_sharing)
- var error: Error = config.save(SETTINGS_PATH)
- if error != OK:
- push_warning("Could not save local straywild discovery settings.")
diff --git a/network/discovery_client.gd.uid b/network/discovery_client.gd.uid
deleted file mode 100644
index eebfd6a..0000000
--- a/network/discovery_client.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://d0jv8n2nfqia0
diff --git a/network/endpoint_parser.gd b/network/endpoint_parser.gd
deleted file mode 100644
index fdaebbe..0000000
--- a/network/endpoint_parser.gd
+++ /dev/null
@@ -1,69 +0,0 @@
-class_name EndpointParser
-extends RefCounted
-
-const DEFAULT_PORT: int = 7777
-
-
-static func parse(value: String, default_port: int = DEFAULT_PORT) -> ConnectionEndpoint:
- var result := ConnectionEndpoint.new()
- result.original_display = value.strip_edges()
- result.port = default_port
- if default_port < 1 or default_port > 65535:
- result.error_message = "The configured default port is invalid."
- return result
- var input: String = result.original_display
- if input.is_empty():
- result.error_message = "Enter a hostname or IP address."
- return result
-
- var host: String = ""
- var port_text: String = ""
- if input.begins_with("["):
- var closing: int = input.find("]")
- if closing < 0:
- result.error_message = "The IPv6 address is missing a closing bracket."
- return result
- host = input.substr(1, closing - 1).strip_edges()
- var suffix: String = input.substr(closing + 1).strip_edges()
- if not suffix.is_empty():
- if not suffix.begins_with(":") or suffix.length() == 1:
- result.error_message = "Use [IPv6]:port for an IPv6 port."
- return result
- port_text = suffix.substr(1).strip_edges()
- elif input.contains("[") or input.contains("]"):
- result.error_message = "IPv6 brackets are malformed."
- return result
- else:
- var colon_count: int = input.count(":")
- if colon_count == 1:
- var separator: int = input.rfind(":")
- host = input.substr(0, separator).strip_edges()
- port_text = input.substr(separator + 1).strip_edges()
- else:
- # Zero colons is a hostname/IPv4 host. More than one is a raw
- # IPv6 host; an explicit port for it must use brackets.
- host = input.strip_edges()
-
- if host.is_empty():
- result.error_message = "The hostname or IP address is empty."
- return result
- if host.contains(" ") or host.contains("\t") or host.contains("\n"):
- result.error_message = "The hostname or IP address contains whitespace."
- return result
- if not port_text.is_empty():
- if not port_text.is_valid_int():
- result.error_message = "The port must be a number from 1 to 65535."
- return result
- result.port = int(port_text)
- if result.port < 1 or result.port > 65535:
- result.error_message = "The port must be from 1 to 65535."
- return result
-
- result.host = host.to_lower()
- var is_ipv6: bool = result.host.contains(":")
- result.normalized_display = (
- "[%s]:%d" % [result.host, result.port]
- if is_ipv6
- else "%s:%d" % [result.host, result.port]
- )
- return result
diff --git a/network/endpoint_parser.gd.uid b/network/endpoint_parser.gd.uid
deleted file mode 100644
index 6b14810..0000000
--- a/network/endpoint_parser.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://2drengcleq7e
diff --git a/network/host_ban_store.gd b/network/host_ban_store.gd
deleted file mode 100644
index 1251846..0000000
--- a/network/host_ban_store.gd
+++ /dev/null
@@ -1,142 +0,0 @@
-class_name HostBanStore
-extends Node
-
-signal bans_changed
-
-const FORMAT_VERSION := 1
-const MAX_BANS := 500
-
-var _namespaces: Dictionary = {}
-var _loaded := false
-var _write_blocked := false
-var _store_path := ""
-var _expected_hash := ""
-var _data_root: PlayerDataRoot
-
-
-func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
- _store_path = path
- _data_root = data_root
- _namespaces.clear()
- _loaded = false
- _write_blocked = false
- _expected_hash = ""
-
-
-func is_banned(host_fingerprint: String, target_fingerprint: String) -> bool:
- _ensure_loaded()
- return Dictionary(_namespaces.get(host_fingerprint, {})).has(target_fingerprint)
-
-
-func ban(
- host_fingerprint: String,
- target_fingerprint: String,
- display_name: String,
-) -> bool:
- if (
- not NetworkIdentityCrypto.valid_fingerprint(host_fingerprint)
- or not NetworkIdentityCrypto.valid_fingerprint(target_fingerprint)
- or not NetworkProfilePreferences.is_valid_display_name(display_name)
- ):
- return false
- _ensure_loaded()
- if _write_blocked:
- return false
- var previous_namespace: Dictionary = Dictionary(
- _namespaces.get(host_fingerprint, {})
- ).duplicate(true)
- var records: Dictionary = _namespaces.get(host_fingerprint, {})
- records[target_fingerprint] = {
- "host_fingerprint": host_fingerprint,
- "target_fingerprint": target_fingerprint,
- "last_known_display_name": display_name,
- "banned_unix": int(Time.get_unix_time_from_system()),
- "reason": "host_ban",
- }
- while records.size() > MAX_BANS:
- records.erase(records.keys().front())
- _namespaces[host_fingerprint] = records
- if not _save():
- _namespaces[host_fingerprint] = previous_namespace
- return false
- bans_changed.emit()
- return true
-
-
-func unban(host_fingerprint: String, target_fingerprint: String) -> bool:
- _ensure_loaded()
- if _write_blocked:
- return false
- var previous_namespace: Dictionary = Dictionary(
- _namespaces.get(host_fingerprint, {})
- ).duplicate(true)
- var records: Dictionary = _namespaces.get(host_fingerprint, {})
- records.erase(target_fingerprint)
- _namespaces[host_fingerprint] = records
- if not _save():
- _namespaces[host_fingerprint] = previous_namespace
- return false
- bans_changed.emit()
- return true
-
-
-func get_bans(host_fingerprint: String) -> Array[Dictionary]:
- _ensure_loaded()
- var result: Array[Dictionary] = []
- for value: Dictionary in Dictionary(_namespaces.get(host_fingerprint, {})).values():
- result.append(value.duplicate(true))
- result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
- return str(a["last_known_display_name"]).naturalnocasecmp_to(
- str(b["last_known_display_name"])
- ) < 0
- )
- return result
-
-
-func _ensure_loaded() -> void:
- if _loaded:
- return
- _loaded = true
- if _store_path.is_empty() or not FileAccess.file_exists(_store_path):
- return
- var file := FileAccess.open(_store_path, FileAccess.READ)
- if file == null:
- return
- var json := JSON.new()
- if json.parse(file.get_as_text()) != OK or typeof(json.data) != TYPE_DICTIONARY:
- return
- var data: Dictionary = json.data
- if data.get("format_version") != FORMAT_VERSION:
- _write_blocked = true
- return
- for value: Variant in data.get("records", []):
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var record: Dictionary = value
- var host := str(record.get("host_fingerprint", ""))
- var target := str(record.get("target_fingerprint", ""))
- if not NetworkIdentityCrypto.valid_fingerprint(host) or not NetworkIdentityCrypto.valid_fingerprint(target):
- continue
- var records: Dictionary = _namespaces.get(host, {})
- records[target] = record.duplicate(true)
- _namespaces[host] = records
- _expected_hash = PortableFileGuard.hash_file(_store_path)
-
-
-func _save() -> bool:
- var values: Array = []
- for records: Dictionary in _namespaces.values():
- values.append_array(records.values())
- var bytes := JSON.stringify({
- "format_version": FORMAT_VERSION,
- "records": values,
- }, "\t").to_utf8_buffer()
- var result := PortableFileGuard.write_guarded(
- _store_path, bytes, _expected_hash, _data_root.conflict_directory(),
- _data_root.device_id,
- )
- if bool(result.get("conflict", false)):
- _data_root.report_conflict(str(result.get("message", "")), str(result.get("conflict_path", "")))
- if bool(result.get("ok", false)):
- _expected_hash = str(result["hash"])
- return bool(result.get("ok", false))
diff --git a/network/host_ban_store.gd.uid b/network/host_ban_store.gd.uid
deleted file mode 100644
index 54c03fc..0000000
--- a/network/host_ban_store.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c71hostbans000001
diff --git a/network/host_identity_store.gd b/network/host_identity_store.gd
deleted file mode 100644
index a45dd1c..0000000
--- a/network/host_identity_store.gd
+++ /dev/null
@@ -1,6 +0,0 @@
-class_name HostIdentityStore
-extends "res://network/local_signing_identity_store.gd"
-
-
-func _ready() -> void:
- configure("host_identity")
diff --git a/network/host_identity_store.gd.uid b/network/host_identity_store.gd.uid
deleted file mode 100644
index 12d7557..0000000
--- a/network/host_identity_store.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dtyoh0v77x52f
diff --git a/network/identity_backup_service.gd b/network/identity_backup_service.gd
deleted file mode 100644
index c383e7b..0000000
--- a/network/identity_backup_service.gd
+++ /dev/null
@@ -1,226 +0,0 @@
-class_name IdentityBackupService
-extends Node
-
-signal operation_finished(success: bool, message: String)
-
-const MAGIC := "straywild_IDENTITY_BACKUP"
-const LEGACY_MAGIC := "NETFISHING_IDENTITY_BACKUP"
-const FORMAT_VERSION := 1
-const MAX_BACKUP_BYTES := 256 * 1024
-const MIN_PASSPHRASE_LENGTH := 12
-const MAX_PASSPHRASE_LENGTH := 256
-
-var _data_root: PlayerDataRoot
-var _player_identity: PlayerIdentityStore
-var _host_identity: HostIdentityStore
-
-
-func setup(
- data_root: PlayerDataRoot,
- player_identity: PlayerIdentityStore,
- host_identity: HostIdentityStore,
-) -> void:
- _data_root = data_root
- _player_identity = player_identity
- _host_identity = host_identity
-
-
-func default_export_path(identity_type: String) -> String:
- var store: LocalSigningIdentityStore = _store(identity_type)
- if store == null:
- return ""
- if not store.is_ready() and not store.load_or_create():
- return ""
- var timestamp: String = Time.get_datetime_string_from_system().replace(":", "-")
- return _data_root.identity_backup_directory().path_join(
- "%s-%s-%s.nfidentity" % [
- identity_type,
- NetworkIdentityCrypto.compact_suffix(store.fingerprint),
- timestamp,
- ]
- )
-
-
-func export_backup(
- identity_type: String,
- path: String,
- passphrase: String,
- confirmation: String,
-) -> bool:
- if not _valid_passphrase(passphrase) or passphrase != confirmation:
- return _finish(false, "Passphrases must match and contain at least 12 characters.")
- if FileAccess.file_exists(path):
- return _finish(false, "Choose a new backup filename.")
- var store: LocalSigningIdentityStore = _store(identity_type)
- if store == null or (not store.is_ready() and not store.load_or_create()):
- return _finish(false, "The active identity is unavailable.")
- var material: Dictionary = store.export_identity_material()
- var proof: PackedByteArray = store.sign("identity_backup_self_test", [
- identity_type, material["fingerprint"],
- ])
- var envelope: Dictionary = {
- "magic": MAGIC,
- "format_version": FORMAT_VERSION,
- "identity_type": identity_type,
- "algorithm": NetworkIdentityCrypto.ALGORITHM,
- "private_pem": material["private_pem"],
- "public_pem": material["public_pem"],
- "fingerprint": material["fingerprint"],
- "created_at_unix": int(Time.get_unix_time_from_system()),
- "source_device_id": _data_root.device_id,
- "self_signature": Marshalls.raw_to_base64(proof),
- }
- DirAccess.make_dir_recursive_absolute(path.get_base_dir())
- var file: FileAccess = FileAccess.open_encrypted_with_pass(
- path, FileAccess.WRITE, passphrase
- )
- if file == null:
- return _finish(false, "Could not write this identity backup.")
- file.store_string(JSON.stringify(envelope))
- file.flush()
- var ok: bool = file.get_error() == OK
- file.close()
- if not ok:
- return _finish(false, "Could not write this identity backup.")
- var verified: Dictionary = inspect_backup(path, passphrase, identity_type)
- if not bool(verified.get("ok", false)):
- DirAccess.remove_absolute(path)
- return _finish(false, "Could not verify this identity backup.")
- return _finish(true, "Encrypted identity backup created.")
-
-
-func inspect_backup(
- path: String,
- passphrase: String,
- expected_type: String,
-) -> Dictionary:
- if (
- not _valid_passphrase(passphrase)
- or not FileAccess.file_exists(path)
- or FileAccess.get_size(path) > MAX_BACKUP_BYTES
- ):
- return {"ok": false}
- var file: FileAccess = FileAccess.open_encrypted_with_pass(
- path, FileAccess.READ, passphrase
- )
- if file == null:
- return {"ok": false}
- var text: String = file.get_as_text()
- file.close()
- var json: JSON = JSON.new()
- if json.parse(text) != OK or typeof(json.data) != TYPE_DICTIONARY:
- return {"ok": false}
- var data: Dictionary = json.data
- var legacy_backup: bool = data.get("magic") == LEGACY_MAGIC
- if (
- data.get("magic") not in [MAGIC, LEGACY_MAGIC]
- or data.get("format_version") != FORMAT_VERSION
- or data.get("identity_type") != expected_type
- or data.get("algorithm") != NetworkIdentityCrypto.ALGORITHM
- ):
- return {"ok": false}
- var private_pem: String = str(data.get("private_pem", ""))
- var public_pem: String = NetworkIdentityCrypto.normalize_public_pem(
- str(data.get("public_pem", ""))
- )
- var fingerprint: String = str(data.get("fingerprint", ""))
- if (
- private_pem.length() > 128 * 1024
- or public_pem.length() > 32 * 1024
- or NetworkIdentityCrypto.fingerprint_public_pem(public_pem) != fingerprint
- ):
- return {"ok": false}
- var key: CryptoKey = CryptoKey.new()
- if key.load_from_string(private_pem) != OK:
- return {"ok": false}
- var signature: PackedByteArray = Marshalls.base64_to_raw(
- str(data.get("self_signature", ""))
- )
- var public_key: CryptoKey = NetworkIdentityCrypto.load_public_key(public_pem)
- var backup_signature_valid: bool = (
- NetworkIdentityCrypto.verify_legacy_fields(
- public_key,
- "identity_backup_self_test",
- [expected_type, fingerprint],
- signature,
- )
- if legacy_backup
- else NetworkIdentityCrypto.verify_fields(
- public_key,
- "identity_backup_self_test",
- [expected_type, fingerprint],
- signature,
- )
- )
- if not backup_signature_valid:
- return {"ok": false}
- var fresh: PackedByteArray = NetworkIdentityCrypto.sign_fields(
- key, "identity_import_self_test", [fingerprint]
- )
- if not NetworkIdentityCrypto.verify_fields(
- NetworkIdentityCrypto.load_public_key(public_pem),
- "identity_import_self_test",
- [fingerprint],
- fresh,
- ):
- return {"ok": false}
- return {
- "ok": true,
- "identity_type": expected_type,
- "fingerprint": fingerprint,
- "private_pem": private_pem,
- "public_pem": public_pem,
- }
-
-
-func import_backup(
- identity_type: String,
- path: String,
- passphrase: String,
- confirmed_replacement: bool,
-) -> Dictionary:
- var inspected: Dictionary = inspect_backup(path, passphrase, identity_type)
- if not bool(inspected.get("ok", false)):
- _finish(false, "Could not open this identity backup.")
- return {"ok": false}
- var store: LocalSigningIdentityStore = _store(identity_type)
- var incoming: String = str(inspected["fingerprint"])
- if store.fingerprint == incoming:
- _finish(true, "This identity is already active.")
- return {"ok": true, "same": true}
- if not confirmed_replacement:
- return {
- "ok": false,
- "requires_confirmation": true,
- "current_fingerprint": store.fingerprint,
- "incoming_fingerprint": incoming,
- }
- var result: Dictionary = store.install_identity_material(
- str(inspected["private_pem"]),
- str(inspected["public_pem"]),
- incoming,
- )
- _finish(
- bool(result.get("ok", false)),
- "Identity imported. Restart straywild before multiplayer."
- if bool(result.get("ok", false))
- else "Could not install this identity backup.",
- )
- return result
-
-
-func _store(identity_type: String) -> LocalSigningIdentityStore:
- if identity_type == "player":
- return _player_identity
- if identity_type == "host":
- return _host_identity
- return null
-
-
-func _valid_passphrase(value: String) -> bool:
- return value.length() >= MIN_PASSPHRASE_LENGTH and value.length() <= MAX_PASSPHRASE_LENGTH
-
-
-func _finish(success: bool, message: String) -> bool:
- operation_finished.emit(success, message)
- return success
diff --git a/network/identity_backup_service.gd.uid b/network/identity_backup_service.gd.uid
deleted file mode 100644
index 05f1de0..0000000
--- a/network/identity_backup_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bu5q4a8jfvxkf
diff --git a/network/known_player_store.gd b/network/known_player_store.gd
deleted file mode 100644
index 1118656..0000000
--- a/network/known_player_store.gd
+++ /dev/null
@@ -1,125 +0,0 @@
-class_name KnownPlayerStore
-extends Node
-
-const FORMAT_VERSION: int = 1
-const MAX_RECORDS: int = 500
-
-var _records: Dictionary = {}
-var _loaded: bool = false
-var _write_blocked: bool = false
-var _store_path := ""
-var _expected_hash := ""
-var _data_root: PlayerDataRoot
-
-
-func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
- _store_path = path
- _data_root = data_root
- _records.clear()
- _loaded = false
- _write_blocked = false
- _expected_hash = ""
-
-
-func observe(fingerprint: String, display_name: String) -> String:
- _ensure_loaded()
- if (
- not NetworkIdentityCrypto.valid_fingerprint(fingerprint)
- or not NetworkProfilePreferences.is_valid_display_name(display_name)
- ):
- return ""
- var now := int(Time.get_unix_time_from_system())
- var status := "New identity"
- var record: Dictionary = _records.get(fingerprint, {})
- if not record.is_empty():
- status = "Known player"
- else:
- for value: Dictionary in _records.values():
- if str(value.get("last_known_display_name", "")).nocasecmp_to(display_name) == 0:
- status = "New identity using a familiar name"
- break
- record = {
- "fingerprint": fingerprint,
- "first_seen_unix": now,
- "locally_verified": false,
- }
- record["last_known_display_name"] = display_name
- record["last_seen_unix"] = now
- _records[fingerprint] = record
- _bound_records()
- _save()
- return status
-
-
-func get_record(fingerprint: String) -> Dictionary:
- _ensure_loaded()
- return Dictionary(_records.get(fingerprint, {})).duplicate(true)
-
-
-func identity_status(fingerprint: String, display_name: String) -> String:
- _ensure_loaded()
- var record: Dictionary = _records.get(fingerprint, {})
- if not record.is_empty():
- return "Known player"
- for value: Dictionary in _records.values():
- if str(value.get("last_known_display_name", "")).nocasecmp_to(display_name) == 0:
- return "New identity using a familiar name"
- return "New identity"
-
-
-func _ensure_loaded() -> void:
- if _loaded:
- return
- _loaded = true
- if _store_path.is_empty() or not FileAccess.file_exists(_store_path):
- return
- var file := FileAccess.open(_store_path, FileAccess.READ)
- if file == null:
- return
- var json := JSON.new()
- if json.parse(file.get_as_text()) != OK or typeof(json.data) != TYPE_DICTIONARY:
- return
- var data: Dictionary = json.data
- if data.get("format_version") != FORMAT_VERSION:
- _write_blocked = true
- return
- if typeof(data.get("records")) != TYPE_ARRAY:
- return
- for value: Variant in data["records"]:
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var record: Dictionary = value
- var fingerprint := str(record.get("fingerprint", ""))
- if NetworkIdentityCrypto.valid_fingerprint(fingerprint):
- _records[fingerprint] = record.duplicate(true)
- _expected_hash = PortableFileGuard.hash_file(_store_path)
-
-
-func _save() -> bool:
- if _write_blocked:
- return false
- var bytes := JSON.stringify({
- "format_version": FORMAT_VERSION,
- "records": _records.values(),
- }, "\t").to_utf8_buffer()
- var result := PortableFileGuard.write_guarded(
- _store_path, bytes, _expected_hash, _data_root.conflict_directory(),
- _data_root.device_id,
- )
- if bool(result.get("conflict", false)):
- _data_root.report_conflict(str(result.get("message", "")), str(result.get("conflict_path", "")))
- if bool(result.get("ok", false)):
- _expected_hash = str(result["hash"])
- return bool(result.get("ok", false))
-
-
-func _bound_records() -> void:
- while _records.size() > MAX_RECORDS:
- var oldest_key := ""
- var oldest_time := 9223372036854775807
- for key: String in _records:
- var seen := int(_records[key].get("last_seen_unix", 0))
- if seen < oldest_time:
- oldest_time = seen
- oldest_key = key
- _records.erase(oldest_key)
diff --git a/network/known_player_store.gd.uid b/network/known_player_store.gd.uid
deleted file mode 100644
index 85c44ec..0000000
--- a/network/known_player_store.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dossvvvh4yuid
diff --git a/network/local_signing_identity_store.gd b/network/local_signing_identity_store.gd
deleted file mode 100644
index a8ac7a0..0000000
--- a/network/local_signing_identity_store.gd
+++ /dev/null
@@ -1,291 +0,0 @@
-class_name LocalSigningIdentityStore
-extends Node
-
-const FORMAT_VERSION: int = 1
-
-var fingerprint: String = ""
-var public_pem: String = ""
-var error_message: String = ""
-var _private_key: CryptoKey
-var _prefix: String = ""
-var _allow_generation: bool = true
-var _storage_directory: String = ""
-
-
-func configure(
- prefix: String,
- allow_generation: bool = true,
- storage_directory: String = "",
-) -> void:
- _prefix = prefix
- _allow_generation = allow_generation
- _storage_directory = storage_directory.strip_edges()
-
-
-func load_or_create() -> bool:
- if _prefix.is_empty():
- error_message = "Identity storage is not configured."
- return false
- var key_path := _path(".key")
- var public_path := _path(".pub")
- var metadata_path := _path(".json")
- var any_exists := (
- FileAccess.file_exists(key_path)
- or FileAccess.file_exists(public_path)
- or FileAccess.file_exists(metadata_path)
- )
- if any_exists:
- return _load_existing()
- if not _allow_generation:
- return false
- return _generate_new()
-
-
-func is_ready() -> bool:
- return _private_key != null and NetworkIdentityCrypto.valid_fingerprint(fingerprint)
-
-
-func sign(domain: String, fields: Array) -> PackedByteArray:
- return NetworkIdentityCrypto.sign_fields(_private_key, domain, fields)
-
-
-func verify(domain: String, fields: Array, signature: PackedByteArray) -> bool:
- return NetworkIdentityCrypto.verify_fields(
- NetworkIdentityCrypto.load_public_key(public_pem),
- domain,
- fields,
- signature,
- )
-
-
-func export_identity_material() -> Dictionary:
- if not is_ready():
- return {}
- return {
- "private_pem": _private_key.save_to_string(),
- "public_pem": public_pem,
- "fingerprint": fingerprint,
- }
-
-
-func install_identity_material(
- private_pem: String,
- public_value: String,
- expected_fingerprint: String,
-) -> Dictionary:
- var normalized_public := NetworkIdentityCrypto.normalize_public_pem(public_value)
- var derived := NetworkIdentityCrypto.fingerprint_public_pem(normalized_public)
- var key := CryptoKey.new()
- if (
- derived != expected_fingerprint
- or key.load_from_string(private_pem) != OK
- ):
- return {"ok": false}
- var probe := NetworkIdentityCrypto.sign_fields(
- key, "identity_import_self_test", [derived]
- )
- if not NetworkIdentityCrypto.verify_fields(
- NetworkIdentityCrypto.load_public_key(normalized_public),
- "identity_import_self_test",
- [derived],
- probe,
- ):
- return {"ok": false}
- if derived == fingerprint:
- return {"ok": true, "same": true, "archive_path": ""}
- var had_active_identity := is_ready()
- var archive := _archive_current_identity() if had_active_identity else ""
- if had_active_identity and archive.is_empty():
- return {"ok": false}
- var metadata := {
- "format_version": FORMAT_VERSION,
- "algorithm": NetworkIdentityCrypto.ALGORITHM,
- "fingerprint": derived,
- "created_at_unix": int(Time.get_unix_time_from_system()),
- }
- if (
- not _write_atomic(_path(".key"), private_pem)
- or not _write_atomic(_path(".pub"), normalized_public)
- or not _write_atomic(_path(".json"), JSON.stringify(metadata, "\t"))
- ):
- if not archive.is_empty():
- _restore_archive(archive)
- _load_existing()
- return {"ok": false}
- FileAccess.set_unix_permissions(_path(".key"), 384)
- if not _load_existing() or fingerprint != derived:
- if not archive.is_empty():
- _restore_archive(archive)
- _load_existing()
- return {"ok": false}
- return {"ok": true, "same": false, "archive_path": archive}
-
-
-func _generate_new() -> bool:
- var key := Crypto.new().generate_rsa(3072)
- if key == null:
- error_message = "A signing identity could not be generated."
- return false
- var private_text := key.save_to_string()
- var public_text := NetworkIdentityCrypto.normalize_public_pem(
- key.save_to_string(true)
- )
- var derived := NetworkIdentityCrypto.fingerprint_public_pem(public_text)
- var probe := NetworkIdentityCrypto.sign_fields(key, "identity_self_test", [derived])
- var public_key := NetworkIdentityCrypto.load_public_key(public_text)
- if (
- not NetworkIdentityCrypto.valid_fingerprint(derived)
- or not NetworkIdentityCrypto.verify_fields(
- public_key, "identity_self_test", [derived], probe
- )
- ):
- error_message = "The generated signing identity failed verification."
- return false
- var metadata := {
- "format_version": FORMAT_VERSION,
- "algorithm": NetworkIdentityCrypto.ALGORITHM,
- "fingerprint": derived,
- "created_at_unix": int(Time.get_unix_time_from_system()),
- }
- if (
- not _write_atomic(_path(".key"), private_text)
- or not _write_atomic(_path(".pub"), public_text)
- or not _write_atomic(_path(".json"), JSON.stringify(metadata, "\t"))
- ):
- error_message = "The signing identity could not be stored."
- return false
- FileAccess.set_unix_permissions(_path(".key"), 384)
- return _load_existing()
-
-
-func _load_existing() -> bool:
- var required := [_path(".key"), _path(".pub"), _path(".json")]
- for path: String in required:
- if not FileAccess.file_exists(path):
- error_message = "Identity recovery is required. An identity file is missing."
- return false
- var private_file := FileAccess.open(_path(".key"), FileAccess.READ)
- var public_file := FileAccess.open(_path(".pub"), FileAccess.READ)
- var metadata_file := FileAccess.open(_path(".json"), FileAccess.READ)
- if private_file == null or public_file == null or metadata_file == null:
- error_message = "Identity recovery is required. Identity files could not be read."
- return false
- var private_text := private_file.get_as_text()
- var loaded_public := NetworkIdentityCrypto.normalize_public_pem(public_file.get_as_text())
- var metadata_text := metadata_file.get_as_text()
- var json := JSON.new()
- if json.parse(metadata_text) != OK or typeof(json.data) != TYPE_DICTIONARY:
- error_message = "Identity recovery is required. Identity metadata is damaged."
- return false
- var metadata: Dictionary = json.data
- var derived := NetworkIdentityCrypto.fingerprint_public_pem(loaded_public)
- if (
- metadata.get("format_version") != FORMAT_VERSION
- or metadata.get("algorithm") != NetworkIdentityCrypto.ALGORITHM
- or metadata.get("fingerprint") != derived
- ):
- error_message = "Identity recovery is required. Identity files do not match."
- return false
- var key := CryptoKey.new()
- if key.load_from_string(private_text) != OK:
- error_message = "Identity recovery is required. The private key is damaged."
- return false
- var probe := NetworkIdentityCrypto.sign_fields(key, "identity_self_test", [derived])
- if not NetworkIdentityCrypto.verify_fields(
- NetworkIdentityCrypto.load_public_key(loaded_public),
- "identity_self_test",
- [derived],
- probe,
- ):
- error_message = "Identity recovery is required. The key pair does not match."
- return false
- _private_key = key
- public_pem = loaded_public
- fingerprint = derived
- error_message = ""
- return true
-
-
-func _write_atomic(path: String, content: String) -> bool:
- var temporary := path + ".tmp"
- var file := FileAccess.open(temporary, FileAccess.WRITE)
- if file == null:
- return false
- file.store_string(content)
- file.flush()
- var write_error := file.get_error()
- file.close()
- if write_error != OK:
- return false
- if FileAccess.file_exists(path):
- var backup := path + ".backup"
- _remove_if_present(backup)
- if not _rename(path, backup):
- return false
- if not _rename(temporary, path):
- _rename(backup, path)
- return false
- _remove_if_present(backup)
- return true
- return _rename(temporary, path)
-
-
-func _path(extension: String) -> String:
- var filename: String = "%s%s" % [_prefix, extension]
- return (
- _storage_directory.path_join(filename)
- if not _storage_directory.is_empty()
- else "user://%s" % filename
- )
-
-
-func identity_type() -> String:
- return "player" if _prefix == "player_identity" else "host"
-
-
-func _archive_current_identity() -> String:
- var timestamp := Time.get_datetime_string_from_system().replace(":", "-")
- var archive := ProjectSettings.globalize_path(
- "user://identity-recovery/%s-%s" % [_prefix, timestamp]
- )
- if DirAccess.make_dir_recursive_absolute(archive) != OK:
- return ""
- for extension: String in [".key", ".pub", ".json"]:
- var source := _path(extension)
- var bytes := PortableFileGuard.read_bytes(source, 1024 * 1024)
- if bytes.is_empty():
- return ""
- var file := FileAccess.open(archive.path_join(_prefix + extension), FileAccess.WRITE)
- if file == null:
- return ""
- file.store_buffer(bytes)
- file.close()
- FileAccess.set_unix_permissions(
- archive.path_join(_prefix + ".key"), 384
- )
- return archive
-
-
-func _restore_archive(archive: String) -> void:
- for extension: String in [".key", ".pub", ".json"]:
- var source := archive.path_join(_prefix + extension)
- var bytes := PortableFileGuard.read_bytes(source, 1024 * 1024)
- if bytes.is_empty():
- continue
- var file := FileAccess.open(_path(extension), FileAccess.WRITE)
- if file != null:
- file.store_buffer(bytes)
- file.close()
- FileAccess.set_unix_permissions(_path(".key"), 384)
-
-
-func _rename(from_path: String, to_path: String) -> bool:
- return DirAccess.rename_absolute(
- ProjectSettings.globalize_path(from_path),
- ProjectSettings.globalize_path(to_path),
- ) == OK
-
-
-func _remove_if_present(path: String) -> void:
- if FileAccess.file_exists(path):
- DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
diff --git a/network/local_signing_identity_store.gd.uid b/network/local_signing_identity_store.gd.uid
deleted file mode 100644
index 44c6907..0000000
--- a/network/local_signing_identity_store.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://1h1kh0jlhc87
diff --git a/network/network_chat_protocol.gd b/network/network_chat_protocol.gd
deleted file mode 100644
index 2e91432..0000000
--- a/network/network_chat_protocol.gd
+++ /dev/null
@@ -1,92 +0,0 @@
-class_name NetworkChatProtocol
-extends RefCounted
-
-const CAPABILITY: StringName = &"chat_v1"
-const RELIABLE_CHANNEL: int = NetworkProtocol.CHAT_RELIABLE_CHANNEL
-const MAX_VISIBLE_CHARACTERS: int = 256
-const MAX_UTF8_BYTES: int = 1024
-const MAX_HISTORY: int = 100
-const LATE_JOIN_HISTORY: int = 50
-const MAX_ID_LENGTH: int = 96
-
-static var _invisible_edge_pattern: RegEx = RegEx.create_from_string(
- "^[\\s\\p{Z}\\p{Cf}\\x{2800}]+|[\\s\\p{Z}\\p{Cf}\\x{2800}]+$"
-)
-static var _visible_content_pattern: RegEx = RegEx.create_from_string(
- "[^\\s\\p{Z}\\p{Cf}\\x{2800}]"
-)
-
-enum Kind { PLAYER, SYSTEM }
-
-
-static func sanitize_body(value: Variant) -> String:
- if typeof(value) != TYPE_STRING:
- return ""
- var result: String = str(value).replace("\r", " ").replace("\n", " ")
- result = result.replace("\t", " ")
- result = _invisible_edge_pattern.sub(result, "", true)
- if (
- result.is_empty()
- or _visible_content_pattern.search(result) == null
- or result.length() > MAX_VISIBLE_CHARACTERS
- ):
- return ""
- if result.to_utf8_buffer().size() > MAX_UTF8_BYTES:
- return ""
- for index: int in result.length():
- var codepoint: int = result.unicode_at(index)
- if codepoint < 32 or codepoint == 127:
- return ""
- return result
-
-
-static func validate_request(data: Variant) -> bool:
- return (
- typeof(data) == TYPE_DICTIONARY
- and _valid_id(data.get("request_id"))
- and _valid_id(data.get("session_id"))
- and not sanitize_body(data.get("body")).is_empty()
- and NetworkIdentityCrypto.valid_fingerprint(
- data.get("sender_fingerprint")
- )
- and typeof(data.get("sender_signature")) == TYPE_PACKED_BYTE_ARRAY
- and PackedByteArray(data["sender_signature"]).size()
- <= NetworkIdentityCrypto.MAX_SIGNATURE_BYTES
- )
-
-
-static func validate_message(data: Variant) -> bool:
- return (
- typeof(data) == TYPE_DICTIONARY
- and _valid_id(data.get("message_id"))
- and _valid_id(data.get("session_id"))
- and typeof(data.get("sequence")) == TYPE_INT
- and int(data["sequence"]) >= 0
- and typeof(data.get("kind")) == TYPE_INT
- and int(data["kind"]) in [Kind.PLAYER, Kind.SYSTEM]
- and typeof(data.get("sender_peer_id")) == TYPE_INT
- and typeof(data.get("sender_display_name")) == TYPE_STRING
- and str(data["sender_display_name"]).length() <= 24
- and not sanitize_body(data.get("body")).is_empty()
- and NetworkIdentityCrypto.valid_fingerprint(
- data.get("sender_fingerprint")
- )
- and typeof(data.get("sender_signature")) == TYPE_PACKED_BYTE_ARRAY
- )
-
-
-static func signature_fields(data: Dictionary) -> Array:
- return [
- str(data.get("session_id", "")),
- str(data.get("request_id", "")),
- str(data.get("sender_fingerprint", "")),
- sanitize_body(data.get("body")),
- ]
-
-
-static func _valid_id(value: Variant) -> bool:
- return (
- typeof(value) == TYPE_STRING
- and not str(value).is_empty()
- and str(value).length() <= MAX_ID_LENGTH
- )
diff --git a/network/network_chat_protocol.gd.uid b/network/network_chat_protocol.gd.uid
deleted file mode 100644
index 916bd77..0000000
--- a/network/network_chat_protocol.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c3hg65v27hi2g
diff --git a/network/network_chat_service.gd b/network/network_chat_service.gd
deleted file mode 100644
index 55ba768..0000000
--- a/network/network_chat_service.gd
+++ /dev/null
@@ -1,657 +0,0 @@
-class_name NetworkChatService
-extends Node
-
-const BURST_COUNT: int = 3
-const WINDOW_COUNT: int = 5
-const WINDOW_SECONDS: float = 10.0
-const CALL_COOLDOWN_MILLISECONDS: int = 90
-const MAX_PENDING_MESSAGES_PER_PEER: int = 8
-const DEDICATED_CHAT_WARNING: String = (
- "Privacy notice: This dedicated server records and retains chat messages."
-)
-const CALL_PITCH_VARIANTS: Array[float] = [
- 0.96,
- 1.03,
- 0.985,
- 1.055,
- 1.0,
- 0.975,
- 1.04,
-]
-const VoiceProfilesType = preload(
- "res://player/animalese_voice_profiles.gd"
-)
-
-signal message_received(message: Dictionary)
-signal local_message_confirmed(message: Dictionary)
-signal send_rejected(message: String)
-signal history_replaced(messages: Array[Dictionary])
-signal character_call_received(
- peer_id: int,
- call_id: String,
- pitch_scale: float,
-)
-
-var _session: NetworkSession
-var _history: Array[Dictionary] = []
-var _seen_messages: Dictionary[String, bool] = {}
-var _request_ledgers: Dictionary[int, Dictionary] = {}
-var _rate_times: Dictionary[int, Array] = {}
-var _last_call_msec: Dictionary[int, int] = {}
-var _call_variant_indices: Dictionary[int, int] = {}
-var _sequence: int = 0
-var _peer_names: Dictionary[int, String] = {}
-var _pending_peer_messages: Dictionary[int, Array] = {}
-var _relationships: PlayerRelationshipStore
-var _dedicated_history_enabled: bool = false
-var _chat_log_path: String = ""
-
-
-func configure_dedicated_history(enabled: bool, log_path: String) -> bool:
- _dedicated_history_enabled = false
- _chat_log_path = ""
- if not enabled:
- return true
- var normalized_path: String = log_path.strip_edges()
- if normalized_path.is_empty() or not normalized_path.is_absolute_path():
- return false
- var directory: String = normalized_path.get_base_dir()
- if DirAccess.make_dir_recursive_absolute(directory) != OK:
- return false
- var file: FileAccess
- if FileAccess.file_exists(normalized_path):
- file = FileAccess.open(normalized_path, FileAccess.READ_WRITE)
- else:
- file = FileAccess.open(normalized_path, FileAccess.WRITE)
- if file == null:
- return false
- file.close()
- _dedicated_history_enabled = true
- _chat_log_path = normalized_path
- return true
-
-
-func setup(session: NetworkSession) -> void:
- _session = session
- _session.peer_authenticated.connect(_on_peer_authenticated)
- _session.peer_removed.connect(_on_peer_removed)
- _session.state_changed.connect(_on_session_state_changed)
- _session.peer_display_name_changed.connect(
- func(peer_id: int, display_name: String) -> void:
- _peer_names[peer_id] = display_name
- )
-
-
-func set_relationship_store(store: PlayerRelationshipStore) -> void:
- _relationships = store
-
-
-func refresh_relationship_filters() -> void:
- history_replaced.emit(get_history())
-
-
-func is_sender_filtered(fingerprint: String) -> bool:
- return (
- _relationships != null
- and _relationships.is_muted(fingerprint)
- )
-
-
-func send_local_message(
- body: String,
- voice_id: String = VoiceProfilesType.DEFAULT_ID,
- sample_set_id: String = VoiceProfilesType.DEFAULT_SAMPLE_SET_ID,
-) -> bool:
- if (
- _session == null
- or not _session.is_gameplay_session_active()
- or (
- not _session.is_host()
- and not _session.supports_server_capability(
- NetworkChatProtocol.CAPABILITY
- )
- )
- ):
- send_rejected.emit("Chat is unavailable.")
- return false
- var clean := NetworkChatProtocol.sanitize_body(body)
- if clean.is_empty():
- send_rejected.emit("Message is empty or too long.")
- return false
- var request := {
- "request_id": _new_id("chat_request"),
- "session_id": _session.get_session_id(),
- "body": clean,
- "sender_fingerprint": _session.get_local_identity_fingerprint(),
- "voice_id": VoiceProfilesType.sanitized_id(voice_id),
- "sample_set_id": VoiceProfilesType.sanitized_sample_set_id(
- sample_set_id
- ),
- }
- request["sender_signature"] = _session.sign_local_action(
- "chat_send", NetworkChatProtocol.signature_fields(request)
- )
- if _session.is_host():
- _handle_request(_session.get_local_peer_id(), request)
- else:
- submit_chat_message.rpc_id(1, request)
- return true
-
-
-func send_local_character_call(call_id: String) -> bool:
- if (
- _session == null
- or not _session.is_gameplay_session_active()
- or not VoiceProfilesType.is_valid_call(call_id)
- or (
- not _session.is_host()
- and not _session.supports_server_capability(
- NetworkChatProtocol.CAPABILITY
- )
- )
- ):
- return false
- var request := {
- "request_id": _new_id("character_call"),
- "session_id": _session.get_session_id(),
- "call_id": call_id,
- "sender_fingerprint": _session.get_local_identity_fingerprint(),
- }
- request["sender_signature"] = _session.sign_local_action(
- "character_call", _character_call_signature_fields(request)
- )
- if _session.is_host():
- _handle_character_call(_session.get_local_peer_id(), request)
- else:
- submit_character_call.rpc_id(1, request)
- return true
-
-
-@rpc("any_peer", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
-func submit_character_call(data: Dictionary) -> void:
- var sender_id := multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(sender_id):
- _handle_character_call(sender_id, data)
-
-
-func _handle_character_call(peer_id: int, data: Dictionary) -> void:
- if (
- typeof(data.get("request_id")) != TYPE_STRING
- or str(data["request_id"]).is_empty()
- or str(data["request_id"]).length() > 64
- or typeof(data.get("session_id")) != TYPE_STRING
- or str(data["session_id"]) != _session.get_session_id()
- or typeof(data.get("call_id")) != TYPE_STRING
- or not VoiceProfilesType.is_valid_call(str(data["call_id"]))
- or typeof(data.get("sender_fingerprint")) != TYPE_STRING
- or typeof(data.get("sender_signature")) != TYPE_PACKED_BYTE_ARRAY
- ):
- return
- var record := _session.get_peer_record(peer_id)
- if (
- record == null
- or record.identity_fingerprint != str(data["sender_fingerprint"])
- or not _session.verify_peer_action(
- peer_id,
- "character_call",
- _character_call_signature_fields(data),
- data["sender_signature"],
- )
- or not _consume_character_call_rate(peer_id)
- ):
- return
- var pitch_scale: float = _next_character_call_pitch(peer_id)
- _apply_character_call(peer_id, str(data["call_id"]), pitch_scale)
- receive_character_call.rpc(
- peer_id,
- str(data["call_id"]),
- pitch_scale,
- )
-
-
-@rpc("authority", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
-func receive_character_call(
- peer_id: int,
- call_id: String,
- pitch_scale: float,
-) -> void:
- _apply_character_call(peer_id, call_id, pitch_scale)
-
-
-func _apply_character_call(
- peer_id: int,
- call_id: String,
- pitch_scale: float,
-) -> void:
- if (
- not VoiceProfilesType.is_valid_call(call_id)
- or pitch_scale < 0.9
- or pitch_scale > 1.1
- or _session == null
- or not _session.is_gameplay_session_active()
- ):
- return
- var record := _session.get_peer_record(peer_id)
- if record == null or is_sender_filtered(record.identity_fingerprint):
- return
- character_call_received.emit(peer_id, call_id, pitch_scale)
-
-
-func _next_character_call_pitch(peer_id: int) -> float:
- var variant_index: int = _call_variant_indices.get(peer_id, 0)
- var pitch_scale: float = CALL_PITCH_VARIANTS[
- variant_index % CALL_PITCH_VARIANTS.size()
- ]
- _call_variant_indices[peer_id] = variant_index + 1
- return pitch_scale
-
-
-func _consume_character_call_rate(peer_id: int) -> bool:
- var now_msec: int = Time.get_ticks_msec()
- var last_msec: int = _last_call_msec.get(
- peer_id, now_msec - CALL_COOLDOWN_MILLISECONDS
- )
- if now_msec - last_msec < CALL_COOLDOWN_MILLISECONDS:
- return false
- _last_call_msec[peer_id] = now_msec
- return true
-
-
-func _character_call_signature_fields(data: Dictionary) -> Array:
- return [
- str(data.get("session_id", "")),
- str(data.get("request_id", "")),
- str(data.get("sender_fingerprint", "")),
- str(data.get("call_id", "")),
- ]
-
-
-func broadcast_system_message(body: String) -> bool:
- if _session == null or not _session.is_host():
- return false
- var clean := NetworkChatProtocol.sanitize_body(body)
- if clean.is_empty():
- return false
- _broadcast(_make_message(
- NetworkChatProtocol.Kind.SYSTEM,
- 0,
- "",
- clean,
- ))
- return true
-
-
-func get_history() -> Array[Dictionary]:
- var result: Array[Dictionary] = []
- for message: Dictionary in _history:
- if _message_is_visible(message):
- result.append(message.duplicate(true))
- return result
-
-
-@rpc("any_peer", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
-func submit_chat_message(data: Dictionary) -> void:
- var sender_id := multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(sender_id):
- _handle_request(sender_id, data)
-
-
-func _handle_request(peer_id: int, data: Dictionary) -> void:
- if (
- not NetworkChatProtocol.validate_request(data)
- or str(data["session_id"]) != _session.get_session_id()
- ):
- _send_rejection(peer_id, "Message could not be sent.")
- return
- var record := _session.get_peer_record(peer_id)
- if (
- record == null
- or record.identity_fingerprint != str(data["sender_fingerprint"])
- or not _session.verify_peer_action(
- peer_id,
- "chat_send",
- NetworkChatProtocol.signature_fields(data),
- data["sender_signature"],
- )
- ):
- _send_rejection(peer_id, "Message identity could not be verified.")
- return
- var request_id: String = data["request_id"]
- var ledger: Dictionary = _request_ledgers.get(peer_id, {})
- if ledger.has(request_id):
- if typeof(ledger[request_id]) == TYPE_DICTIONARY:
- _send_message(peer_id, ledger[request_id])
- return
- if not _consume_rate(peer_id):
- _send_rejection(peer_id, "Slow down.")
- return
- var message := _make_message(
- NetworkChatProtocol.Kind.PLAYER,
- peer_id,
- record.display_name,
- NetworkChatProtocol.sanitize_body(data["body"]),
- VoiceProfilesType.sanitized_id(str(data.get(
- "voice_id", VoiceProfilesType.DEFAULT_ID
- ))),
- VoiceProfilesType.sanitized_sample_set_id(str(data.get(
- "sample_set_id", VoiceProfilesType.DEFAULT_SAMPLE_SET_ID
- ))),
- )
- message["request_id"] = request_id
- message["sender_fingerprint"] = data["sender_fingerprint"]
- message["sender_signature"] = data["sender_signature"]
- ledger[request_id] = (
- message.duplicate(true)
- if _should_store_host_history()
- else true
- )
- while ledger.size() > 64:
- ledger.erase(ledger.keys().front())
- _request_ledgers[peer_id] = ledger
- _broadcast(message)
-
-
-func _make_message(
- kind: int,
- peer_id: int,
- display_name: String,
- body: String,
- voice_id: String = VoiceProfilesType.DEFAULT_ID,
- sample_set_id: String = VoiceProfilesType.DEFAULT_SAMPLE_SET_ID,
-) -> Dictionary:
- _sequence += 1
- var message := {
- "message_id": _new_id("chat"),
- "request_id": _new_id("chat_system"),
- "session_id": _session.get_session_id(),
- "sequence": _sequence,
- "kind": kind,
- "sender_peer_id": peer_id,
- "sender_display_name": display_name.left(24),
- "body": body,
- "sender_fingerprint": _session.get_host_identity_fingerprint(),
- "voice_id": VoiceProfilesType.sanitized_id(voice_id),
- "sample_set_id": VoiceProfilesType.sanitized_sample_set_id(
- sample_set_id
- ),
- }
- if kind == NetworkChatProtocol.Kind.SYSTEM:
- message["sender_signature"] = _session.sign_host_action(
- "chat_system", NetworkChatProtocol.signature_fields(message)
- )
- return message
-
-
-func _broadcast(message: Dictionary) -> void:
- _apply_message(message)
- receive_chat_message.rpc(message)
-
-
-func _send_message(peer_id: int, message: Dictionary) -> void:
- if peer_id == _session.get_local_peer_id():
- _apply_message(message)
- else:
- receive_chat_message.rpc_id(peer_id, message)
-
-
-@rpc("authority", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
-func receive_chat_message(data: Dictionary) -> void:
- _apply_message(data)
-
-
-func _apply_message(
- data: Dictionary,
- emit_live_signals: bool = true,
-) -> void:
- if (
- not NetworkChatProtocol.validate_message(data)
- or str(data["session_id"]) != _session.get_session_id()
- or _seen_messages.has(str(data["message_id"]))
- ):
- return
- var kind := int(data["kind"])
- var valid_signature := false
- if kind == NetworkChatProtocol.Kind.SYSTEM:
- valid_signature = _session.verify_host_action(
- "chat_system",
- NetworkChatProtocol.signature_fields(data),
- data["sender_signature"],
- )
- else:
- var sender_id := int(data["sender_peer_id"])
- var record := _session.get_peer_record(sender_id)
- if record == null:
- _queue_pending_peer_message(sender_id, data, emit_live_signals)
- return
- valid_signature = (
- record.identity_fingerprint == str(data["sender_fingerprint"])
- and _session.verify_peer_action(
- sender_id,
- "chat_send",
- NetworkChatProtocol.signature_fields(data),
- data["sender_signature"],
- )
- )
- if not valid_signature:
- return
- _seen_messages[data["message_id"]] = true
- var stored_message := data.duplicate(true)
- # Suppression is immutable for this received copy. Relationship changes
- # later must never resurrect text that was hidden on arrival.
- stored_message["locally_suppressed"] = (
- kind == NetworkChatProtocol.Kind.PLAYER
- and is_sender_filtered(str(data.get("sender_fingerprint", "")))
- )
- if _should_store_local_history():
- _history.append(stored_message)
- while _history.size() > NetworkChatProtocol.MAX_HISTORY:
- _history.pop_front()
- if _should_log_message(stored_message):
- _append_chat_log(stored_message)
- if emit_live_signals and _message_is_visible(stored_message):
- message_received.emit(stored_message.duplicate(true))
- if (
- kind == NetworkChatProtocol.Kind.PLAYER
- and int(stored_message["sender_peer_id"])
- == _session.get_local_peer_id()
- ):
- local_message_confirmed.emit(stored_message.duplicate(true))
-
-
-func _send_rejection(peer_id: int, message: String) -> void:
- if peer_id == _session.get_local_peer_id():
- send_rejected.emit(message)
- else:
- receive_chat_rejection.rpc_id(peer_id, message.left(80))
-
-
-@rpc("authority", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
-func receive_chat_rejection(message: String) -> void:
- send_rejected.emit(message.left(80))
-
-
-func _queue_pending_peer_message(
- peer_id: int,
- message: Dictionary,
- emit_live_signals: bool,
-) -> void:
- if peer_id <= 0:
- return
- var pending: Array = _pending_peer_messages.get(peer_id, [])
- var message_id := str(message.get("message_id", ""))
- for value: Variant in pending:
- if (
- typeof(value) == TYPE_DICTIONARY
- and str(value.get("message", {}).get("message_id", "")) == message_id
- ):
- return
- pending.append({
- "message": message.duplicate(true),
- "emit_live_signals": emit_live_signals,
- })
- while pending.size() > MAX_PENDING_MESSAGES_PER_PEER:
- pending.pop_front()
- _pending_peer_messages[peer_id] = pending
-
-
-func _flush_pending_peer_messages(peer_id: int) -> void:
- var pending: Array = _pending_peer_messages.get(peer_id, [])
- _pending_peer_messages.erase(peer_id)
- for value: Variant in pending:
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var queued: Dictionary = value
- if typeof(queued.get("message")) != TYPE_DICTIONARY:
- continue
- _apply_message(
- queued["message"],
- bool(queued.get("emit_live_signals", true)),
- )
-
-
-func _on_peer_authenticated(peer_id: int, display_name: String) -> void:
- _flush_pending_peer_messages(peer_id)
- if not _session.is_host():
- return
- _peer_names[peer_id] = display_name
- var history: Array[Dictionary] = []
- if _dedicated_history_enabled and _session.is_dedicated_host():
- var start := maxi(
- 0,
- _history.size() - NetworkChatProtocol.LATE_JOIN_HISTORY,
- )
- history = _history.slice(start)
- # An explicit empty replacement is intentional for player-hosted rooms and
- # dedicated servers without logging. It destroys any stale client scrollback
- # instead of replaying another player's live-session chat on reconnect.
- receive_chat_history.rpc_id(peer_id, history)
- if _dedicated_history_enabled and _session.is_dedicated_host():
- # Deliver the disclosure as a live system message after scrollback. This
- # raises the normal unread indicator without creating speech or audio.
- _send_message(peer_id, _make_message(
- NetworkChatProtocol.Kind.SYSTEM,
- 0,
- "",
- DEDICATED_CHAT_WARNING,
- ))
- _broadcast(_make_message(
- NetworkChatProtocol.Kind.SYSTEM, 0, "", "%s joined." % display_name
- ))
-
-
-func _on_peer_removed(peer_id: int) -> void:
- _request_ledgers.erase(peer_id)
- _rate_times.erase(peer_id)
- _last_call_msec.erase(peer_id)
- _call_variant_indices.erase(peer_id)
- _pending_peer_messages.erase(peer_id)
- if not _session.is_host():
- return
- var display_name: String = _peer_names.get(peer_id, "Player")
- _peer_names.erase(peer_id)
- _broadcast(_make_message(
- NetworkChatProtocol.Kind.SYSTEM, 0, "", "%s left." % display_name
- ))
-
-
-@rpc("authority", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
-func receive_chat_history(values: Array) -> void:
- if values.size() > NetworkChatProtocol.LATE_JOIN_HISTORY:
- return
- _history.clear()
- _seen_messages.clear()
- for value: Variant in values:
- if NetworkChatProtocol.validate_message(value):
- # Late-join history belongs in the scrollback, but it is not a live
- # event. In particular, do not recreate speech bubbles or replay
- # animalese for every stored player message when joining a room.
- _apply_message(value, false)
- history_replaced.emit(get_history())
-
-
-func _message_is_visible(message: Dictionary) -> bool:
- if bool(message.get("locally_suppressed", false)):
- return false
- if int(message.get("kind", -1)) == NetworkChatProtocol.Kind.SYSTEM:
- return true
- return true
-
-
-func _consume_rate(peer_id: int) -> bool:
- var now := Time.get_ticks_msec() / 1000.0
- var times: Array = _rate_times.get(peer_id, [])
- while not times.is_empty() and now - float(times.front()) > WINDOW_SECONDS:
- times.pop_front()
- var recent_burst := 0
- for value: Variant in times:
- if now - float(value) <= 1.0:
- recent_burst += 1
- if recent_burst >= BURST_COUNT or times.size() >= WINDOW_COUNT:
- _rate_times[peer_id] = times
- return false
- times.append(now)
- _rate_times[peer_id] = times
- return true
-
-
-func _should_store_host_history() -> bool:
- return (
- _session == null
- or not _session.is_dedicated_host()
- or _dedicated_history_enabled
- )
-
-
-func _should_store_local_history() -> bool:
- return _should_store_host_history()
-
-
-func _should_log_message(message: Dictionary) -> bool:
- return (
- _dedicated_history_enabled
- and not _chat_log_path.is_empty()
- and _session != null
- and _session.is_dedicated_host()
- and int(message.get("kind", -1)) == NetworkChatProtocol.Kind.PLAYER
- )
-
-
-func _append_chat_log(message: Dictionary) -> void:
- var file := FileAccess.open(_chat_log_path, FileAccess.READ_WRITE)
- if file == null:
- push_error("The configured dedicated-server chat log is unavailable.")
- return
- file.seek_end()
- file.store_line(JSON.stringify({
- "recorded_at_utc": (
- Time.get_datetime_string_from_system(true, false) + "Z"
- ),
- "sender_display_name": str(message.get("sender_display_name", "")),
- "body": str(message.get("body", "")),
- }))
- file.close()
-
-
-func _on_session_state_changed(state: NetworkSession.State) -> void:
- if state in [
- NetworkSession.State.INACTIVE,
- NetworkSession.State.DISCONNECTING,
- NetworkSession.State.CONNECTION_FAILED,
- NetworkSession.State.SERVER_LOST,
- ]:
- _history.clear()
- _seen_messages.clear()
- _request_ledgers.clear()
- _rate_times.clear()
- _last_call_msec.clear()
- _call_variant_indices.clear()
- _peer_names.clear()
- _pending_peer_messages.clear()
- _sequence = 0
- history_replaced.emit([])
-
-
-func _new_id(prefix: String) -> String:
- return "%s:%s" % [
- prefix, Crypto.new().generate_random_bytes(16).hex_encode(),
- ]
diff --git a/network/network_chat_service.gd.uid b/network/network_chat_service.gd.uid
deleted file mode 100644
index 228279d..0000000
--- a/network/network_chat_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b10jayoog5dc2
diff --git a/network/network_fish_showcase_protocol.gd b/network/network_fish_showcase_protocol.gd
deleted file mode 100644
index 2ecd1ee..0000000
--- a/network/network_fish_showcase_protocol.gd
+++ /dev/null
@@ -1,61 +0,0 @@
-class_name NetworkFishShowcaseProtocol
-extends RefCounted
-
-const CAPABILITY: StringName = &"fish_showcase_v1"
-const RELIABLE_CHANNEL: int = NetworkProtocol.SHOWCASE_RELIABLE_CHANNEL
-const MAX_SESSION_ID_LENGTH: int = 96
-const MAX_FISH_ID_LENGTH: int = 96
-const MAX_WEIGHT_LB: float = 1000.0
-const MAX_DISPLAY_SCALE: float = 20.0
-const FishQualityType = preload("res://fish/fish_quality.gd")
-
-
-static func validate_state(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- for key: String in [
- "session_id",
- "owner_peer_id",
- "visible",
- "fish_id",
- "weight_lb",
- "display_scale",
- "quality",
- "revision",
- ]:
- if not value.has(key):
- return false
- if (
- typeof(value["session_id"]) != TYPE_STRING
- or str(value["session_id"]).is_empty()
- or str(value["session_id"]).length() > MAX_SESSION_ID_LENGTH
- or typeof(value["owner_peer_id"]) != TYPE_INT
- or int(value["owner_peer_id"]) <= 0
- or typeof(value["visible"]) != TYPE_BOOL
- or typeof(value["fish_id"]) not in [TYPE_STRING, TYPE_STRING_NAME]
- or str(value["fish_id"]).length() > MAX_FISH_ID_LENGTH
- or typeof(value["weight_lb"]) not in [TYPE_FLOAT, TYPE_INT]
- or not is_finite(float(value["weight_lb"]))
- or typeof(value["display_scale"]) not in [TYPE_FLOAT, TYPE_INT]
- or not is_finite(float(value["display_scale"]))
- or typeof(value["quality"]) != TYPE_INT
- or not FishQualityType.is_valid(int(value["quality"]))
- or typeof(value["revision"]) != TYPE_INT
- or int(value["revision"]) < 0
- ):
- return false
- if bool(value["visible"]):
- return (
- not str(value["fish_id"]).is_empty()
- and float(value["weight_lb"]) > 0.0
- and float(value["weight_lb"]) <= MAX_WEIGHT_LB
- and float(value["display_scale"]) > 0.0
- and float(value["display_scale"]) <= MAX_DISPLAY_SCALE
- )
- return (
- str(value["fish_id"]).is_empty()
- and is_zero_approx(float(value["weight_lb"]))
- and is_zero_approx(float(value["display_scale"]))
- and int(value["quality"]) == FishQualityType.Tier.BORING
- )
diff --git a/network/network_fish_showcase_protocol.gd.uid b/network/network_fish_showcase_protocol.gd.uid
deleted file mode 100644
index 322418d..0000000
--- a/network/network_fish_showcase_protocol.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b3h0h47ilh3nm
diff --git a/network/network_fish_showcase_service.gd b/network/network_fish_showcase_service.gd
deleted file mode 100644
index d956240..0000000
--- a/network/network_fish_showcase_service.gd
+++ /dev/null
@@ -1,309 +0,0 @@
-class_name NetworkFishShowcaseService
-extends Node
-
-const FishPoolType = preload("res://fish/fish_pool.gd")
-const FishDataType = preload("res://fish/fish_data.gd")
-const FishCatchType = preload("res://fish/fish_catch.gd")
-const FishQualityType = preload("res://fish/fish_quality.gd")
-const FishInventoryType = preload("res://inventory/fish_inventory.gd")
-const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
-const LOCAL_STATE_RETRY_SECONDS: float = 0.75
-
-var _session: NetworkSession
-var _spawn_service: PlayerSpawnService
-var _fish_catalog: FishPoolType
-var _local_inventory: FishInventoryType
-var _local_hotbar: PlayerHotbarType
-var _states: Dictionary[int, Dictionary] = {}
-var _local_revision: int = 0
-var _local_acknowledged_revision: int = -1
-var _local_state_retry_elapsed: float = 0.0
-var _local_visible: bool = false
-var _local_catch_id: StringName
-
-
-func setup(
- session: NetworkSession,
- spawn_service: PlayerSpawnService,
- fish_catalog: FishPoolType,
- local_inventory: FishInventoryType,
- local_hotbar: PlayerHotbarType,
-) -> void:
- _session = session
- _spawn_service = spawn_service
- _fish_catalog = fish_catalog
- _local_inventory = local_inventory
- _local_hotbar = local_hotbar
- _session.peer_authenticated.connect(_on_peer_authenticated)
- _session.peer_removed.connect(_on_peer_removed)
- _session.state_changed.connect(_on_session_state_changed)
- _spawn_service.avatar_spawned.connect(_on_avatar_spawned)
- _local_inventory.catches_changed.connect(_on_local_inventory_changed)
- _local_hotbar.selected_assignment_changed.connect(
- _on_selected_assignment_changed
- )
- var selected_kind: int = (
- _local_hotbar.get_selected_assignment_kind()
- )
- var selected_identity: StringName = StringName()
- if selected_kind == PlayerHotbarType.AssignmentKind.ITEM:
- selected_identity = _local_hotbar.get_selected_item_id()
- elif selected_kind == PlayerHotbarType.AssignmentKind.FISH:
- selected_identity = _local_hotbar.get_selected_fish_catch_id()
- _on_selected_assignment_changed(
- _local_hotbar.get_selected_slot(),
- selected_kind,
- selected_identity,
- )
-
-
-func _process(delta: float) -> void:
- if (
- _session == null
- or not _session.is_joined_client()
- or _local_revision <= 0
- or _local_revision <= _local_acknowledged_revision
- ):
- _local_state_retry_elapsed = 0.0
- return
- _local_state_retry_elapsed += delta
- if _local_state_retry_elapsed < LOCAL_STATE_RETRY_SECONDS:
- return
- var fish_catch: FishCatchType
- if _local_visible:
- fish_catch = _local_inventory.get_catch_by_id(_local_catch_id)
- if fish_catch == null or not fish_catch.is_valid():
- _submit_local_state(null, false)
- return
- # A client can finish a generated-world transition on the same frame that
- # its local showcase changes. Republish until the authoritative echo arrives
- # so that one early application-level miss cannot leave held fish out of sync.
- _submit_local_state(fish_catch, _local_visible)
-
-
-func toggle_selected_fish() -> bool:
- if _local_hotbar == null or _local_inventory == null:
- return false
- var catch_id: StringName = _local_hotbar.get_selected_fish_catch_id()
- var fish_catch: FishCatchType = _local_inventory.get_catch_by_id(catch_id)
- if fish_catch == null or not fish_catch.is_valid():
- return false
- if _local_visible and _local_catch_id == catch_id:
- _submit_local_state(null, false)
- else:
- _submit_local_state(fish_catch, true)
- return true
-
-
-func is_local_showcase_visible() -> bool:
- return _local_visible
-
-
-func get_local_showcase_catch_id() -> StringName:
- return _local_catch_id
-
-
-func _submit_local_state(fish_catch: FishCatchType, should_show: bool) -> void:
- _local_revision += 1
- _local_state_retry_elapsed = 0.0
- var local_peer_id: int = (
- _session.get_local_peer_id() if _session != null else 1
- )
- var data: Dictionary = {
- "session_id": (
- _session.get_session_id()
- if _session != null and not _session.get_session_id().is_empty()
- else "local"
- ),
- "owner_peer_id": local_peer_id,
- "visible": should_show and fish_catch != null,
- "fish_id": String(fish_catch.fish_id) if fish_catch != null else "",
- "weight_lb": fish_catch.weight_lb if fish_catch != null else 0.0,
- "display_scale": (
- fish_catch.display_scale if fish_catch != null else 0.0
- ),
- "quality": (
- fish_catch.quality
- if fish_catch != null
- else FishQualityType.Tier.BORING
- ),
- "revision": _local_revision,
- }
- _local_visible = bool(data["visible"])
- _local_catch_id = (
- fish_catch.catch_id if _local_visible else StringName()
- )
- _apply_state(data)
- if _session == null or not _session.is_gameplay_session_active():
- return
- data["session_id"] = _session.get_session_id()
- if _session.is_host():
- _handle_showcase_state(local_peer_id, data)
- elif _session.supports_server_capability(
- NetworkFishShowcaseProtocol.CAPABILITY
- ):
- submit_showcase_state.rpc_id(1, data)
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- NetworkFishShowcaseProtocol.RELIABLE_CHANNEL,
-)
-func submit_showcase_state(data: Dictionary) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(sender_id):
- _handle_showcase_state(sender_id, data)
-
-
-func _handle_showcase_state(peer_id: int, data: Dictionary) -> void:
- if (
- not _session.is_host()
- or not NetworkFishShowcaseProtocol.validate_state(data)
- or str(data["session_id"]) != _session.get_session_id()
- or int(data["owner_peer_id"]) != peer_id
- or (
- peer_id != _session.get_local_peer_id()
- and not _session.is_authenticated_peer(peer_id)
- )
- ):
- return
- var previous: Dictionary = _states.get(peer_id, {})
- if int(data["revision"]) <= int(previous.get("revision", -1)):
- return
- if not _state_matches_catalog(data):
- return
- var sanitized: Dictionary = data.duplicate(true)
- _states[peer_id] = sanitized
- _apply_state(sanitized)
- receive_showcase_state.rpc(sanitized)
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- NetworkFishShowcaseProtocol.RELIABLE_CHANNEL,
-)
-func receive_showcase_state(data: Dictionary) -> void:
- if (
- not NetworkFishShowcaseProtocol.validate_state(data)
- or str(data["session_id"]) != _session.get_session_id()
- or not _state_matches_catalog(data)
- ):
- return
- var peer_id: int = int(data["owner_peer_id"])
- if peer_id == _session.get_local_peer_id():
- _local_acknowledged_revision = maxi(
- _local_acknowledged_revision,
- int(data["revision"]),
- )
- var previous: Dictionary = _states.get(peer_id, {})
- if int(data["revision"]) <= int(previous.get("revision", -1)):
- return
- _states[peer_id] = data.duplicate(true)
- _apply_state(data)
-
-
-func _state_matches_catalog(data: Dictionary) -> bool:
- if not bool(data["visible"]):
- return true
- var fish_id: StringName = StringName(str(data["fish_id"]))
- var fish: FishDataType = _fish_catalog.get_fish_by_id(fish_id)
- if fish == null or not fish.is_selectable():
- return false
- var weight_lb: float = float(data["weight_lb"])
- return (
- weight_lb >= fish.get_minimum_weight()
- and weight_lb <= fish.get_maximum_weight()
- )
-
-
-func _apply_state(data: Dictionary) -> void:
- var peer_id: int = int(data["owner_peer_id"])
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- if avatar == null:
- return
- if not bool(data["visible"]):
- avatar.set_held_fish(null, 1.0, false, true)
- return
- var fish: FishDataType = _fish_catalog.get_fish_by_id(
- StringName(str(data["fish_id"]))
- )
- avatar.set_held_fish(
- fish,
- fish.get_display_scale_for_weight(float(data["weight_lb"])),
- true,
- )
-
-
-func _on_selected_assignment_changed(
- _slot_index: int,
- kind: int,
- identity: StringName,
-) -> void:
- if kind == PlayerHotbarType.AssignmentKind.FISH:
- if _local_visible and identity == _local_catch_id:
- return
- var fish_catch: FishCatchType = _local_inventory.get_catch_by_id(identity)
- if fish_catch != null and fish_catch.is_valid():
- _submit_local_state(fish_catch, true)
- elif _local_visible:
- _submit_local_state(null, false)
- return
- if _local_visible:
- _submit_local_state(null, false)
-
-
-func _on_local_inventory_changed() -> void:
- if (
- _local_visible
- and not _local_inventory.contains_catch_id(_local_catch_id)
- ):
- _submit_local_state(null, false)
-
-
-func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
- if not _session.is_host():
- return
- for state: Dictionary in _states.values():
- if bool(state.get("visible", false)):
- receive_showcase_state.rpc_id(peer_id, state)
-
-
-func _on_avatar_spawned(peer_id: int, _avatar: Player) -> void:
- var state: Dictionary = _states.get(peer_id, {})
- if not state.is_empty():
- _apply_state(state)
-
-
-func _on_peer_removed(peer_id: int) -> void:
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- if avatar != null:
- avatar.set_held_fish(null, 1.0, false)
- _states.erase(peer_id)
-
-
-func _on_session_state_changed(state: NetworkSession.State) -> void:
- if state not in [
- NetworkSession.State.INACTIVE,
- NetworkSession.State.DISCONNECTING,
- NetworkSession.State.CONNECTION_FAILED,
- NetworkSession.State.SERVER_LOST,
- ]:
- return
- for peer_id: int in _states:
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- if avatar != null:
- avatar.set_held_fish(null, 1.0, false)
- _states.clear()
- _local_visible = false
- _local_catch_id = StringName()
- _local_acknowledged_revision = -1
- _local_state_retry_elapsed = 0.0
- var local_avatar: Player = _spawn_service.get_avatar(
- _session.get_local_peer_id()
- )
- if local_avatar != null:
- local_avatar.set_held_fish(null, 1.0, false)
diff --git a/network/network_fish_showcase_service.gd.uid b/network/network_fish_showcase_service.gd.uid
deleted file mode 100644
index 0a6c086..0000000
--- a/network/network_fish_showcase_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://m4n0bn38f34i
diff --git a/network/network_fishing_attempt.gd b/network/network_fishing_attempt.gd
deleted file mode 100644
index 638b4da..0000000
--- a/network/network_fishing_attempt.gd
+++ /dev/null
@@ -1,43 +0,0 @@
-class_name NetworkFishingAttempt
-extends RefCounted
-
-enum Phase {
- CASTING,
- WAITING_FOR_BITE,
- FIGHTING,
- PENDING_CAPACITY,
- CAUGHT,
- ESCAPED,
- CANCELLED,
-}
-
-var owner_peer_id: int = 0
-var request_id: String = ""
-var attempt_id: String = ""
-var result_id: String = ""
-var session_id: String = ""
-var phase: Phase = Phase.CASTING
-var origin: Vector3
-var target: Vector3
-var bobber_position: Vector3
-var fish_id: StringName
-var rod_id: StringName
-var bait_id: StringName
-var bait_tags: Array[StringName] = []
-var lure_effects: Array[StringName] = []
-var encounter_seed: int = 0
-var bite_time_remaining: float = 0.0
-var bite_confirmation_pending: bool = false
-var withdrawal_progress: float = 0.0
-var reel_speed: float = 0.0
-var barrier_damage: int = 1
-var input_held: bool = false
-var last_input_sequence: int = 0
-var catch_payload: Dictionary = {}
-var capacity_nonce: String = ""
-var capacity_deadline: float = 0.0
-var controller: CatchController
-
-
-func is_terminal() -> bool:
- return phase in [Phase.CAUGHT, Phase.ESCAPED, Phase.CANCELLED]
diff --git a/network/network_fishing_attempt.gd.uid b/network/network_fishing_attempt.gd.uid
deleted file mode 100644
index 549ffcf..0000000
--- a/network/network_fishing_attempt.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c4c00yp1546af
diff --git a/network/network_fishing_protocol.gd b/network/network_fishing_protocol.gd
deleted file mode 100644
index 7f15fdd..0000000
--- a/network/network_fishing_protocol.gd
+++ /dev/null
@@ -1,157 +0,0 @@
-class_name NetworkFishingProtocol
-extends RefCounted
-
-const MAX_ID_LENGTH: int = 96
-const MAX_EVIDENCE_FISH_IDS: int = 256
-const MAX_CAST_DISTANCE: float = 30.0
-const MAX_REEL_SPEED: float = 10.0
-const MAX_BARRIER_DAMAGE: int = 128
-const INPUT_CHANNEL: int = 3
-const SNAPSHOT_CHANNEL: int = 4
-const OBSERVER_SNAPSHOT_CHANNEL: int = 10
-const RELIABLE_CHANNEL: int = NetworkProtocol.FISHING_RELIABLE_CHANNEL
-const INPUT_RATE: float = 1.0 / 30.0
-const SNAPSHOT_RATE: float = 1.0 / 20.0
-
-enum Outcome {
- CATCH,
- ESCAPE,
- CANCELLED,
-}
-
-
-static func validate_cast_request(data: Variant) -> String:
- if typeof(data) != TYPE_DICTIONARY:
- return "Malformed fishing request."
- var payload: Dictionary = data
- for key: String in [
- "request_id",
- "session_id",
- "origin",
- "target",
- "charge",
- "rod_id",
- "reel_speed",
- "barrier_damage",
- "bite_multiplier",
- "rarity_multipliers",
- "discovered_fish_ids",
- "capacity_available",
- "movement_sequence",
- ]:
- if not payload.has(key):
- return "Malformed fishing request."
- if (
- typeof(payload["request_id"]) != TYPE_STRING
- or typeof(payload["session_id"]) != TYPE_STRING
- or typeof(payload["origin"]) != TYPE_ARRAY
- or typeof(payload["target"]) != TYPE_ARRAY
- or typeof(payload["charge"]) not in [TYPE_FLOAT, TYPE_INT]
- or typeof(payload["rod_id"]) not in [TYPE_STRING, TYPE_STRING_NAME]
- or typeof(payload["reel_speed"]) not in [TYPE_FLOAT, TYPE_INT]
- or typeof(payload["barrier_damage"]) != TYPE_INT
- or typeof(payload["bite_multiplier"]) not in [TYPE_FLOAT, TYPE_INT]
- or typeof(payload["rarity_multipliers"]) != TYPE_ARRAY
- or typeof(payload["discovered_fish_ids"]) != TYPE_ARRAY
- or typeof(payload["capacity_available"]) != TYPE_BOOL
- or typeof(payload["movement_sequence"]) != TYPE_INT
- ):
- return "Malformed fishing request."
- if payload.has("bait_id") and typeof(payload["bait_id"]) not in [
- TYPE_STRING, TYPE_STRING_NAME
- ]:
- return "Malformed fishing request."
- if payload.has("lure_id") and typeof(payload["lure_id"]) not in [
- TYPE_STRING, TYPE_STRING_NAME
- ]:
- return "Malformed fishing request."
- var request_id: String = payload["request_id"]
- var session_id: String = payload["session_id"]
- var origin: Array = payload["origin"]
- var target: Array = payload["target"]
- var charge: float = float(payload["charge"])
- var reel_speed: float = float(payload["reel_speed"])
- var barrier_damage: int = payload["barrier_damage"]
- var bite_multiplier: float = float(payload["bite_multiplier"])
- var rarity: Array = payload["rarity_multipliers"]
- var discovered: Array = payload["discovered_fish_ids"]
- if (
- request_id.is_empty()
- or request_id.length() > MAX_ID_LENGTH
- or session_id.is_empty()
- or session_id.length() > MAX_ID_LENGTH
- or origin.size() != 3
- or target.size() != 3
- or not _array_is_finite_vector3(origin)
- or not _array_is_finite_vector3(target)
- or not is_finite(charge)
- or charge < 0.0
- or charge > 1.01
- or not is_finite(reel_speed)
- or reel_speed <= 0.0
- or reel_speed > MAX_REEL_SPEED
- or barrier_damage < 1
- or barrier_damage > MAX_BARRIER_DAMAGE
- or not is_finite(bite_multiplier)
- or bite_multiplier < 0.1
- or bite_multiplier > 10.0
- or rarity.size() > 16
- or discovered.size() > MAX_EVIDENCE_FISH_IDS
- or str(payload["rod_id"]).is_empty()
- or str(payload["rod_id"]).length() > 96
- or str(payload.get("lure_id", "")).length() > 96
- or int(payload["movement_sequence"]) < 0
- or int(payload["movement_sequence"]) > 2147483647
- ):
- return "Fishing request values are outside allowed limits."
- for value: Variant in rarity:
- if (
- typeof(value) not in [TYPE_FLOAT, TYPE_INT]
- or not is_finite(float(value))
- or float(value) < 0.0
- or float(value) > 10.0
- ):
- return "Fishing rarity evidence is invalid."
- for value: Variant in discovered:
- if (
- typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME]
- or str(value).is_empty()
- or str(value).length() > 96
- ):
- return "Fishing discovery evidence is invalid."
- return ""
-
-
-static func validate_input(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var payload: Dictionary = data
- return (
- typeof(payload.get("attempt_id")) == TYPE_STRING
- and not str(payload["attempt_id"]).is_empty()
- and str(payload["attempt_id"]).length() <= MAX_ID_LENGTH
- and typeof(payload.get("sequence")) == TYPE_INT
- and int(payload["sequence"]) > 0
- and typeof(payload.get("held")) == TYPE_BOOL
- and typeof(payload.get("pressed")) == TYPE_BOOL
- )
-
-
-static func vector3_to_array(value: Vector3) -> Array[float]:
- return [value.x, value.y, value.z]
-
-
-static func array_to_vector3(value: Array) -> Vector3:
- if value.size() != 3:
- return Vector3.INF
- return Vector3(float(value[0]), float(value[1]), float(value[2]))
-
-
-static func _array_is_finite_vector3(value: Array) -> bool:
- for component: Variant in value:
- if (
- typeof(component) not in [TYPE_FLOAT, TYPE_INT]
- or not is_finite(float(component))
- ):
- return false
- return true
diff --git a/network/network_fishing_protocol.gd.uid b/network/network_fishing_protocol.gd.uid
deleted file mode 100644
index 2826314..0000000
--- a/network/network_fishing_protocol.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cbihol2i6e2iw
diff --git a/network/network_fishing_service.gd b/network/network_fishing_service.gd
deleted file mode 100644
index 8c9c798..0000000
--- a/network/network_fishing_service.gd
+++ /dev/null
@@ -1,1545 +0,0 @@
-class_name NetworkFishingService
-extends Node
-
-const FishCatchType = preload("res://fish/fish_catch.gd")
-const FishDataType = preload("res://fish/fish_data.gd")
-const FishPoolType = preload("res://fish/fish_pool.gd")
-const FishSelectorType = preload("res://fish/fish_selector.gd")
-const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
-const FishingContextType = preload("res://fishing/fishing_context.gd")
-const CollectionLogType = preload("res://collection/collection_log.gd")
-const FishExperienceType = preload("res://fish/fish_experience.gd")
-const PlayerExperienceType = preload(
- "res://progression/player_experience.gd"
-)
-const RemotePresentationType = preload(
- "res://fishing/remote_fishing_presentation.gd"
-)
-const ItemDataType = preload("res://items/item_data.gd")
-
-const MAX_LEDGER_ENTRIES_PER_PEER: int = 64
-const CAST_ORIGIN_TOLERANCE: float = 2.5
-const CAPACITY_RESPONSE_TIMEOUT: float = 5.0
-const MIN_CAST_INTERVAL: float = 0.25
-const OBSERVER_SNAPSHOT_INTERVAL: float = 0.1
-const OBSERVER_RELEVANCE_DISTANCE: float = 96.0
-
-signal local_cast_accepted(attempt_id: String, target: Vector3)
-signal local_cast_rejected(message: String)
-signal local_bite_started(attempt_id: String)
-signal local_bite_pending(attempt_id: String)
-signal local_snapshot_received(snapshot: Dictionary)
-signal local_catch_received(fish_catch: FishCatch)
-signal local_attempt_ended(outcome: StringName, message: String)
-
-var _session: NetworkSession
-var _spawn_service: PlayerSpawnService
-var _fishing_spot: FishingSpot
-var _local_inventory: FishInventory
-var _local_bag: PlayerBag
-var _local_collection: CollectionLog
-var _local_capacity: PlayerCoolerCapacity
-var _local_experience: PlayerExperienceType
-var _save_manager: PlayerSaveManager
-var _item_catalog: ItemCatalog
-var _fish_catalog: FishPoolType
-var _item_use: NetworkItemUseService
-var _attempts: Dictionary[int, NetworkFishingAttempt] = {}
-var _request_ledgers: Dictionary[int, Dictionary] = {}
-var _result_ledgers: Dictionary[String, bool] = {}
-var _result_acknowledgements: Dictionary[String, String] = {}
-var _last_cast_time: Dictionary[int, float] = {}
-var _last_input_time: Dictionary[int, float] = {}
-var _remote_presentations: Dictionary[int, RemoteFishingPresentation] = {}
-var _pending_local_bait_by_request: Dictionary[String, StringName] = {}
-var _snapshot_accumulator: float = 0.0
-var _observer_snapshot_accumulator: float = 0.0
-var _local_input_sequence: int = 0
-var _presentation_enabled: bool = true
-var _owner_snapshot_packets_sent: int = 0
-var _owner_snapshot_states_sent: int = 0
-var _observer_snapshot_packets_sent: int = 0
-var _observer_snapshot_states_sent: int = 0
-
-
-func setup(
- session: NetworkSession,
- spawn_service: PlayerSpawnService,
- fishing_spot: FishingSpot,
- local_inventory: FishInventory,
- local_bag: PlayerBag,
- local_collection: CollectionLog,
- local_capacity: PlayerCoolerCapacity,
- local_experience: PlayerExperienceType,
- save_manager: PlayerSaveManager,
- item_catalog: ItemCatalog,
- fish_catalog: FishPoolType,
- item_use: NetworkItemUseService,
- presentation_enabled: bool = true,
-) -> void:
- _session = session
- _spawn_service = spawn_service
- _fishing_spot = fishing_spot
- _local_inventory = local_inventory
- _local_bag = local_bag
- _local_collection = local_collection
- _local_capacity = local_capacity
- _local_experience = local_experience
- _save_manager = save_manager
- _item_catalog = item_catalog
- _fish_catalog = fish_catalog
- _item_use = item_use
- _presentation_enabled = presentation_enabled
- if not _session.peer_removed.is_connected(_on_peer_removed):
- _session.peer_removed.connect(_on_peer_removed)
- if not _session.state_changed.is_connected(_on_session_state_changed):
- _session.state_changed.connect(_on_session_state_changed)
- if not _spawn_service.avatar_removed.is_connected(_on_avatar_removed):
- _spawn_service.avatar_removed.connect(_on_avatar_removed)
-
-
-func request_local_cast(
- origin: Vector3,
- target: Vector3,
- charge: float,
- evidence: Dictionary,
-) -> String:
- if (
- _session == null
- or not _session.is_gameplay_session_active()
- or not origin.is_finite()
- or not target.is_finite()
- ):
- local_cast_rejected.emit("Fishing attempt ended.")
- return ""
- var bait_id := StringName(str(evidence.get("bait_id", "")))
- if not bait_id.is_empty() and (
- _local_bag == null or not _local_bag.owns_item(bait_id)
- ):
- local_cast_rejected.emit("No bait available.")
- return ""
- var lure_id := StringName(str(evidence.get("lure_id", "")))
- if not lure_id.is_empty() and (
- _local_bag == null or not _local_bag.owns_item(lure_id)
- ):
- local_cast_rejected.emit("Lure is unavailable.")
- return ""
- var request_id: String = _new_id("cast")
- var data: Dictionary = {
- "request_id": request_id,
- "session_id": _session.get_session_id(),
- "origin": NetworkFishingProtocol.vector3_to_array(origin),
- "target": NetworkFishingProtocol.vector3_to_array(target),
- "charge": charge,
- "rod_id": str(evidence.get("rod_id", "")),
- "reel_speed": float(evidence.get("reel_speed", 0.0)),
- "barrier_damage": int(evidence.get("barrier_damage", 0)),
- "bite_multiplier": float(evidence.get("bite_multiplier", 1.0)),
- "rarity_multipliers": evidence.get("rarity_multipliers", []),
- "discovered_fish_ids": evidence.get("discovered_fish_ids", []),
- "capacity_available": bool(evidence.get("capacity_available", false)),
- "bait_id": str(bait_id),
- "lure_id": str(lure_id),
- "movement_sequence": _session.get_latest_movement_input_sequence(),
- }
- if _session.is_host():
- _handle_cast_request(_session.get_local_peer_id(), data)
- else:
- if not bait_id.is_empty():
- _pending_local_bait_by_request[request_id] = bait_id
- submit_cast_request.rpc_id(1, data)
- return request_id
-
-
-func submit_local_input(held: bool, pressed: bool) -> void:
- var peer_id: int = _session.get_local_peer_id()
- var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
- if attempt == null:
- return
- _local_input_sequence += 1
- var data: Dictionary = {
- "attempt_id": attempt.attempt_id,
- "sequence": _local_input_sequence,
- "held": held,
- "pressed": pressed,
- }
- if _session.is_host():
- _handle_fishing_input(peer_id, data)
- else:
- submit_fishing_input.rpc_id(1, data)
-
-
-func cancel_local_attempt(reason: String = "Fishing attempt ended.") -> void:
- if _session == null or not _session.is_gameplay_session_active():
- return
- var peer_id: int = _session.get_local_peer_id()
- var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
- if attempt == null:
- return
- if _session.is_host():
- _cancel_attempt(peer_id, reason)
- else:
- _pending_local_bait_by_request.erase(attempt.request_id)
- submit_cancel_request.rpc_id(1, attempt.attempt_id)
-
-
-func cancel_peer_attempt(peer_id: int, reason: String) -> void:
- if _session != null and _session.is_host() and _attempts.has(peer_id):
- _cancel_attempt(peer_id, reason)
-
-
-func has_local_attempt() -> bool:
- return (
- _session != null
- and _attempts.has(_session.get_local_peer_id())
- )
-
-
-func has_peer_attempt(peer_id: int) -> bool:
- return _attempts.has(peer_id)
-
-
-func _process(delta: float) -> void:
- if _session == null or not _session.is_host():
- return
- var now: float = Time.get_ticks_msec() / 1000.0
- for peer_id: int in _attempts.keys():
- var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
- if attempt == null:
- continue
- match attempt.phase:
- NetworkFishingAttempt.Phase.WAITING_FOR_BITE:
- if attempt.bite_confirmation_pending:
- continue
- _update_waiting_attempt(attempt, delta)
- if not _attempts.has(peer_id):
- continue
- attempt.bite_time_remaining -= delta
- if attempt.bite_time_remaining <= 0.0:
- if &"deferred_fight" in attempt.lure_effects:
- _set_bite_pending(attempt)
- else:
- _start_bite(attempt)
- NetworkFishingAttempt.Phase.PENDING_CAPACITY:
- if now >= attempt.capacity_deadline:
- _cancel_attempt(peer_id, "Fishing attempt ended.")
- _snapshot_accumulator += delta
- _observer_snapshot_accumulator += delta
- if _snapshot_accumulator >= NetworkFishingProtocol.SNAPSHOT_RATE:
- _snapshot_accumulator = fmod(
- _snapshot_accumulator,
- NetworkFishingProtocol.SNAPSHOT_RATE
- )
- _broadcast_owner_snapshots()
- if _observer_snapshot_accumulator >= OBSERVER_SNAPSHOT_INTERVAL:
- _observer_snapshot_accumulator = fmod(
- _observer_snapshot_accumulator,
- OBSERVER_SNAPSHOT_INTERVAL,
- )
- _broadcast_observer_snapshots()
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- NetworkFishingProtocol.RELIABLE_CHANNEL,
-)
-func submit_cast_request(data: Dictionary) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if not _session.is_host() or not _session.is_authenticated_peer(sender_id):
- return
- _handle_cast_request(sender_id, data)
-
-
-func _handle_cast_request(peer_id: int, data: Dictionary) -> void:
- var validation_error: String = NetworkFishingProtocol.validate_cast_request(
- data
- )
- if not validation_error.is_empty():
- _send_cast_rejected(peer_id, str(data.get("request_id", "")),
- validation_error)
- return
- var request_id: String = data["request_id"]
- var ledger: Dictionary = _request_ledgers.get(peer_id, {})
- if ledger.has(request_id):
- _resend_request_response(peer_id, ledger[request_id])
- return
- if str(data["session_id"]) != _session.get_session_id():
- _record_and_reject(peer_id, request_id, "Fishing attempt ended.")
- return
- var now: float = Time.get_ticks_msec() / 1000.0
- if now - float(_last_cast_time.get(peer_id, -INF)) < MIN_CAST_INTERVAL:
- _record_and_reject(peer_id, request_id, "Already fishing.")
- return
- _last_cast_time[peer_id] = now
- if _attempts.has(peer_id):
- _record_and_reject(peer_id, request_id, "Already fishing.")
- return
- if not bool(data["capacity_available"]):
- _record_and_reject(peer_id, request_id, "Inventory is full.")
- return
- var rod := _item_catalog.get_item_by_id(
- StringName(str(data["rod_id"]))
- ) as FishingRodDataType
- if rod == null or not rod.is_available():
- _record_and_reject(peer_id, request_id, "Select a fishing rod to cast.")
- return
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- if avatar == null or avatar.is_water_recovery_active():
- _record_and_reject(peer_id, request_id, "Fishing attempt ended.")
- return
- var origin: Vector3 = NetworkFishingProtocol.array_to_vector3(data["origin"])
- var target: Vector3 = NetworkFishingProtocol.array_to_vector3(data["target"])
- var movement_state: Dictionary = avatar.get_lag_compensated_movement_state(
- int(data.get("movement_sequence", 0))
- )
- var authoritative_origin: Vector3 = movement_state.get(
- "cast_origin",
- avatar.get_cast_origin_position(),
- )
- if origin.distance_to(authoritative_origin) > CAST_ORIGIN_TOLERANCE:
- _record_and_reject(peer_id, request_id, "Cannot fish here.")
- return
- var cast_offset: Vector3 = target - authoritative_origin
- cast_offset.y = 0.0
- var expected_distance: float = lerpf(
- _fishing_spot.minimum_cast_distance,
- _fishing_spot.maximum_cast_distance,
- float(data["charge"])
- )
- var facing: Vector3 = movement_state.get(
- "facing",
- avatar.get_facing_direction(),
- )
- facing.y = 0.0
- if (
- cast_offset.length() < _fishing_spot.minimum_cast_distance - 0.25
- or cast_offset.length() > _fishing_spot.maximum_cast_distance + 0.5
- or absf(cast_offset.length() - expected_distance) > 1.25
- or (
- not facing.is_zero_approx()
- and facing.normalized().dot(cast_offset.normalized()) < 0.2
- )
- ):
- _record_and_reject(peer_id, request_id, "Cannot fish here.")
- return
- var surface: FishingSurfaceSample = _fishing_spot.resolve_fishing_surface(
- target,
- authoritative_origin.y,
- )
- var region: FishableWaterRegion = surface.water_region
- var authoritative_target: Vector3 = surface.position
- if (
- not surface.is_fishable()
- or region == null
- or region.fish_pool == null
- or not _fishing_spot.is_cast_path_clear(
- authoritative_origin,
- authoritative_target
- )
- ):
- _record_and_reject(peer_id, request_id, "Cannot fish here.")
- return
- var effects: PlayerItemEffects = (
- _item_use.get_effects_for_peer(peer_id)
- if _item_use != null else null
- )
- var selected_fish: FishDataType = _select_authoritative_fish(
- region, data, effects, rod
- )
- if selected_fish == null or not selected_fish.is_allowed_in_water(
- region.water_type
- ):
- _record_and_reject(peer_id, request_id, "Nothing is biting here.")
- return
- var bait_id: StringName = StringName(str(data.get("bait_id", "")))
- var avatar_bag: PlayerBag = avatar.bag
- var owns_authoritative_bag: bool = (
- peer_id == _session.get_local_peer_id()
- )
- var bait: ItemDataType = (
- _item_catalog.get_item_by_id(bait_id)
- if not bait_id.is_empty() and _item_catalog != null
- else null
- )
- var lure_id: StringName = StringName(str(data.get("lure_id", "")))
- var lure: ItemDataType = (
- _item_catalog.get_item_by_id(lure_id)
- if not lure_id.is_empty() and _item_catalog != null
- else null
- )
- if (
- not lure_id.is_empty()
- and (
- lure == null
- or not lure.is_available()
- or not lure.is_lure()
- or (
- owns_authoritative_bag
- and (
- avatar_bag == null
- or not avatar_bag.owns_item(lure_id)
- )
- )
- )
- ):
- _record_and_reject(peer_id, request_id, "Lure is unavailable.")
- return
- if not bait_id.is_empty() and (
- bait == null
- or not bait.is_available()
- or not bait.is_bait()
- or (
- owns_authoritative_bag
- and (
- avatar_bag == null
- or not avatar_bag.owns_item(bait_id)
- )
- )
- ):
- _record_and_reject(peer_id, request_id, "No bait available.")
- return
- var attempt := NetworkFishingAttempt.new()
- attempt.owner_peer_id = peer_id
- attempt.request_id = request_id
- attempt.attempt_id = _new_id("attempt")
- attempt.session_id = _session.get_session_id()
- attempt.phase = NetworkFishingAttempt.Phase.WAITING_FOR_BITE
- attempt.origin = authoritative_origin
- attempt.target = authoritative_target
- attempt.bobber_position = authoritative_target
- attempt.fish_id = selected_fish.id
- attempt.rod_id = rod.item_id
- attempt.bait_id = bait_id
- attempt.bait_tags = _bait_tags_for_request(data)
- attempt.lure_effects.clear()
- if lure != null:
- for effect_id: StringName in lure.lure_effects:
- attempt.lure_effects.append(effect_id)
- var effect_reel_multiplier: float = (
- effects.get_reel_multiplier() if effects != null else 1.0
- )
- var effect_barrier_bonus: int = (
- effects.get_barrier_bonus() if effects != null else 0
- )
- var effect_bite_multiplier: float = (
- effects.get_bite_time_multiplier() if effects != null else 1.0
- )
- attempt.reel_speed = (
- float(data["reel_speed"])
- * effect_reel_multiplier
- * rod.reel_speed_multiplier
- )
- attempt.barrier_damage = maxi(
- roundi(
- (float(data["barrier_damage"]) + effect_barrier_bonus)
- * rod.barrier_power_multiplier
- ),
- 1,
- )
- attempt.bite_time_remaining = (
- _fishing_spot.roll_bite_wait_time()
- * effect_bite_multiplier
- * rod.bite_time_multiplier
- )
- attempt.controller = CatchController.new()
- add_child(attempt.controller)
- attempt.controller.encounter_updated.connect(
- _on_encounter_updated.bind(peer_id)
- )
- attempt.controller.caught.connect(_on_attempt_caught.bind(peer_id))
- attempt.controller.escaped.connect(_on_attempt_escaped.bind(peer_id))
- attempt.set_meta("snapshot", _make_waiting_snapshot(attempt))
- _attempts[peer_id] = attempt
- avatar.set_movement_enabled(false)
- var response: Dictionary = _make_cast_accepted(attempt)
- _record_request_response(peer_id, request_id, response)
- _broadcast_cast_accepted(response)
-
-
-func _update_waiting_attempt(
- attempt: NetworkFishingAttempt,
- delta: float,
-) -> void:
- if not attempt.input_held:
- return
- var flat_offset: Vector3 = attempt.target - attempt.origin
- flat_offset.y = 0.0
- var withdrawable_distance: float = (
- flat_offset.length() - _fishing_spot.withdrawal_cancel_distance
- )
- if withdrawable_distance <= 0.0:
- _cancel_attempt(attempt.owner_peer_id, "", &"withdrawal")
- return
- attempt.withdrawal_progress = minf(
- attempt.withdrawal_progress
- + _fishing_spot.withdrawal_rate * delta / withdrawable_distance,
- 1.0
- )
- var endpoint: Vector3 = attempt.origin + (
- flat_offset.normalized()
- * _fishing_spot.withdrawal_cancel_distance
- )
- endpoint.y = attempt.target.y
- var desired: Vector3 = attempt.target.lerp(
- endpoint,
- attempt.withdrawal_progress
- )
- var withdrawal_surface: FishingSurfaceSample = (
- _fishing_spot.resolve_safe_withdrawal_surface(
- attempt.bobber_position,
- desired,
- endpoint - attempt.bobber_position,
- )
- )
- if not withdrawal_surface.is_fishable():
- _cancel_attempt(attempt.owner_peer_id, "", &"withdrawal")
- return
- attempt.bobber_position = withdrawal_surface.position
- attempt.set_meta("snapshot", _make_waiting_snapshot(attempt))
- if is_equal_approx(attempt.withdrawal_progress, 1.0):
- _cancel_attempt(attempt.owner_peer_id, "", &"withdrawal")
-
-
-func _make_waiting_snapshot(
- attempt: NetworkFishingAttempt,
-) -> Dictionary:
- return {
- "attempt_id": attempt.attempt_id,
- "owner_peer_id": attempt.owner_peer_id,
- "phase": int(attempt.phase),
- "progress": 0.0,
- "chase_progress": 0.0,
- "barrier_positions": [],
- "barrier_health": [],
- "barrier_max_health": [],
- "active_barrier_index": -1,
- "visible": false,
- "acknowledged_input_sequence": attempt.last_input_sequence,
- "bobber_position": NetworkFishingProtocol.vector3_to_array(
- attempt.bobber_position
- ),
- }
-
-
-func _select_authoritative_fish(
- region: FishableWaterRegion,
- data: Dictionary,
- effects: PlayerItemEffects,
- rod: FishingRodDataType,
-) -> FishDataType:
- var evidence_log := CollectionLogType.new()
- for value: Variant in data["discovered_fish_ids"]:
- var fish_id := StringName(str(value))
- if _fish_catalog.get_fish_by_id(fish_id) == null:
- return null
- evidence_log.mark_discovered(fish_id)
- var selector := FishSelectorType.new()
- selector.active_rod = rod
- selector.undiscovered_weight_multiplier = (
- _fishing_spot.undiscovered_weight_multiplier
- )
- for rarity: int in range(FishDataType.Rarity.size()):
- selector.rarity_weight_multipliers.append(
- effects.get_rarity_weight_multiplier(rarity)
- if effects != null else 1.0
- )
- selector.begin_roll()
- var context: FishingContextType = _fishing_spot.build_network_context(region)
- context.active_bait_tags = _bait_tags_for_request(data)
- var selected_fish := selector.select_fish(
- region.fish_pool, context, evidence_log
- )
- evidence_log.free()
- return selected_fish
-
-
-func _bait_tags_for_request(data: Dictionary) -> Array[StringName]:
- var bait_id: StringName = StringName(str(data.get("bait_id", "")))
- if bait_id.is_empty() or _item_catalog == null:
- return []
- var bait: ItemDataType = _item_catalog.get_item_by_id(bait_id)
- return (
- bait.bait_tags.duplicate()
- if bait != null and bait.is_available() and bait.is_bait()
- else []
- )
-
-
-func _set_bite_pending(attempt: NetworkFishingAttempt) -> void:
- if (
- attempt.phase != NetworkFishingAttempt.Phase.WAITING_FOR_BITE
- or attempt.bite_confirmation_pending
- ):
- return
- attempt.bite_confirmation_pending = true
- attempt.bite_time_remaining = 0.0
- attempt.input_held = false
- var data: Dictionary = {
- "attempt_id": attempt.attempt_id,
- "owner_peer_id": attempt.owner_peer_id,
- }
- _apply_bite_pending(data)
- receive_bite_pending.rpc(data)
-
-
-func _start_bite(attempt: NetworkFishingAttempt) -> void:
- if attempt.phase != NetworkFishingAttempt.Phase.WAITING_FOR_BITE:
- return
- var fish: FishDataType = _fish_catalog.get_fish_by_id(attempt.fish_id)
- var rod := _item_catalog.get_item_by_id(attempt.rod_id) as FishingRodDataType
- if (
- fish == null
- or fish.catch_profile == null
- or rod == null
- or not rod.is_available()
- ):
- _cancel_attempt(attempt.owner_peer_id, "Fishing attempt ended.")
- return
- attempt.bite_confirmation_pending = false
- attempt.encounter_seed = _new_seed()
- var selector := FishSelectorType.new()
- selector.active_rod = rod
- selector.use_deterministic_test_seed = true
- selector.deterministic_test_seed = attempt.encounter_seed ^ 0x5F3759DF
- selector.begin_roll()
- var fish_catch: FishCatch = selector.create_catch(fish, attempt.bait_tags)
- if fish_catch == null or not fish_catch.is_valid():
- _cancel_attempt(attempt.owner_peer_id, "Fishing attempt ended.")
- return
- if (
- attempt.owner_peer_id == _session.get_local_peer_id()
- and not attempt.bait_id.is_empty()
- and (
- _local_bag == null
- or not _local_bag.remove_item(attempt.bait_id, 1)
- )
- ):
- _cancel_attempt(attempt.owner_peer_id, "No bait available.")
- return
- attempt.phase = NetworkFishingAttempt.Phase.FIGHTING
- attempt.catch_payload = fish_catch.to_network_dict()
- attempt.controller.start_authoritative_encounter(
- fish.catch_profile,
- attempt.reel_speed,
- attempt.barrier_damage,
- attempt.encounter_seed,
- fish_catch.quality,
- )
- var data: Dictionary = {
- "attempt_id": attempt.attempt_id,
- "owner_peer_id": attempt.owner_peer_id,
- }
- _apply_bite_started(data)
- receive_bite_started.rpc(data)
-
-
-@rpc("any_peer", "call_remote", "unreliable_ordered", 3)
-func submit_fishing_input(data: Dictionary) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if not _session.is_host() or not _session.is_authenticated_peer(sender_id):
- return
- _handle_fishing_input(sender_id, data)
-
-
-func _handle_fishing_input(peer_id: int, data: Dictionary) -> void:
- if not NetworkFishingProtocol.validate_input(data):
- return
- var now: float = Time.get_ticks_msec() / 1000.0
- if now - float(_last_input_time.get(peer_id, -INF)) < 1.0 / 120.0:
- return
- _last_input_time[peer_id] = now
- var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
- if (
- attempt == null
- or attempt.attempt_id != str(data["attempt_id"])
- or int(data["sequence"]) <= attempt.last_input_sequence
- ):
- return
- attempt.last_input_sequence = int(data["sequence"])
- attempt.input_held = bool(data["held"])
- if (
- attempt.phase == NetworkFishingAttempt.Phase.WAITING_FOR_BITE
- and attempt.bite_confirmation_pending
- ):
- if bool(data["pressed"]):
- attempt.input_held = false
- _start_bite(attempt)
- return
- if attempt.phase != NetworkFishingAttempt.Phase.FIGHTING:
- return
- attempt.controller.set_reel_input(attempt.input_held)
- if bool(data["pressed"]):
- attempt.controller.handle_primary_pressed()
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- NetworkFishingProtocol.RELIABLE_CHANNEL,
-)
-func submit_cancel_request(attempt_id: String) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- var attempt: NetworkFishingAttempt = _attempts.get(sender_id)
- if (
- not _session.is_host()
- or not _session.is_authenticated_peer(sender_id)
- or attempt == null
- or attempt.attempt_id != attempt_id
- ):
- return
- _cancel_attempt(sender_id, "")
-
-
-func _on_encounter_updated(
- progress: float,
- chase_progress: float,
- barrier_positions: PackedFloat32Array,
- barrier_health: PackedInt32Array,
- barrier_max_health: PackedInt32Array,
- active_barrier_index: int,
- visible: bool,
- peer_id: int,
-) -> void:
- var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
- if attempt == null or attempt.phase != NetworkFishingAttempt.Phase.FIGHTING:
- return
- attempt.set_meta("snapshot", {
- "attempt_id": attempt.attempt_id,
- "owner_peer_id": peer_id,
- "phase": int(attempt.phase),
- "progress": progress,
- "chase_progress": chase_progress,
- "barrier_positions": Array(barrier_positions),
- "barrier_health": Array(barrier_health),
- "barrier_max_health": Array(barrier_max_health),
- "active_barrier_index": active_barrier_index,
- "visible": visible,
- "acknowledged_input_sequence": attempt.last_input_sequence,
- "bobber_position": NetworkFishingProtocol.vector3_to_array(
- attempt.bobber_position
- ),
- })
-
-
-func _broadcast_owner_snapshots() -> void:
- for attempt: NetworkFishingAttempt in _attempts.values():
- var snapshot: Dictionary = attempt.get_meta("snapshot", {})
- if snapshot.is_empty():
- continue
- var owner_peer_id: int = attempt.owner_peer_id
- if owner_peer_id == _session.get_local_peer_id():
- _apply_snapshots([snapshot])
- elif owner_peer_id > 1:
- receive_fishing_snapshots.rpc_id(owner_peer_id, [snapshot])
- _owner_snapshot_packets_sent += 1
- _owner_snapshot_states_sent += 1
-
-
-func _broadcast_observer_snapshots() -> void:
- if _attempts.is_empty():
- return
- var observer_ids: Array[int] = _session.get_authenticated_peer_ids()
- for observer_peer_id: int in observer_ids:
- var observer_avatar: Player = _spawn_service.get_avatar(observer_peer_id)
- if observer_avatar == null:
- continue
- var summaries: Array = []
- for attempt: NetworkFishingAttempt in _attempts.values():
- if attempt.owner_peer_id == observer_peer_id:
- continue
- var owner_avatar: Player = _spawn_service.get_avatar(
- attempt.owner_peer_id
- )
- if (
- owner_avatar == null
- or observer_avatar.global_position.distance_squared_to(
- owner_avatar.global_position
- ) > OBSERVER_RELEVANCE_DISTANCE * OBSERVER_RELEVANCE_DISTANCE
- ):
- continue
- summaries.append([
- attempt.attempt_id,
- attempt.owner_peer_id,
- attempt.bobber_position,
- int(attempt.phase),
- ])
- if summaries.is_empty():
- continue
- if observer_peer_id == _session.get_local_peer_id():
- _apply_observer_snapshots(summaries)
- elif observer_peer_id > 1:
- receive_fishing_observer_snapshots.rpc_id(
- observer_peer_id,
- summaries,
- )
- _observer_snapshot_packets_sent += 1
- _observer_snapshot_states_sent += summaries.size()
-
-
-@rpc("authority", "call_remote", "unreliable_ordered", 4)
-func receive_fishing_snapshots(snapshots: Array) -> void:
- _apply_snapshots(snapshots)
-
-
-@rpc(
- "authority",
- "call_remote",
- "unreliable_ordered",
- NetworkFishingProtocol.OBSERVER_SNAPSHOT_CHANNEL,
-)
-func receive_fishing_observer_snapshots(summaries: Array) -> void:
- _apply_observer_snapshots(summaries)
-
-
-func _apply_observer_snapshots(summaries: Array) -> void:
- if not _presentation_enabled or summaries.size() > 128:
- return
- for value: Variant in summaries:
- if typeof(value) != TYPE_ARRAY:
- continue
- var fields: Array = value
- if (
- fields.size() != 4
- or typeof(fields[0]) != TYPE_STRING
- or str(fields[0]).is_empty()
- or str(fields[0]).length() > NetworkFishingProtocol.MAX_ID_LENGTH
- or typeof(fields[1]) != TYPE_INT
- or int(fields[1]) <= 0
- or typeof(fields[2]) != TYPE_VECTOR3
- or typeof(fields[3]) != TYPE_INT
- or not (fields[2] as Vector3).is_finite()
- or int(fields[3]) not in [
- NetworkFishingAttempt.Phase.WAITING_FOR_BITE,
- NetworkFishingAttempt.Phase.FIGHTING,
- NetworkFishingAttempt.Phase.PENDING_CAPACITY,
- ]
- ):
- continue
- var owner_peer_id: int = int(fields[1])
- if owner_peer_id == _session.get_local_peer_id():
- continue
- var presentation := _get_remote_presentation(owner_peer_id)
- if presentation != null:
- presentation.synchronize_active(
- str(fields[0]),
- fields[2],
- int(fields[3]) in [
- NetworkFishingAttempt.Phase.FIGHTING,
- NetworkFishingAttempt.Phase.PENDING_CAPACITY,
- ],
- )
-
-
-func _apply_snapshots(snapshots: Array) -> void:
- var local_peer_id: int = _session.get_local_peer_id()
- for value: Variant in snapshots:
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var snapshot: Dictionary = value
- if not _valid_snapshot(snapshot):
- continue
- var owner_peer_id: int = int(snapshot["owner_peer_id"])
- if owner_peer_id == local_peer_id:
- local_snapshot_received.emit(snapshot)
- else:
- var presentation := _get_remote_presentation(owner_peer_id)
- if presentation != null:
- presentation.update_bobber(
- NetworkFishingProtocol.array_to_vector3(
- snapshot["bobber_position"]
- )
- )
-
-
-func _valid_snapshot(data: Dictionary) -> bool:
- if not (
- typeof(data.get("attempt_id")) == TYPE_STRING
- and typeof(data.get("owner_peer_id")) == TYPE_INT
- and typeof(data.get("phase")) == TYPE_INT
- and typeof(data.get("progress")) in [TYPE_FLOAT, TYPE_INT]
- and typeof(data.get("chase_progress")) in [TYPE_FLOAT, TYPE_INT]
- and typeof(data.get("barrier_positions")) == TYPE_ARRAY
- and typeof(data.get("barrier_health")) == TYPE_ARRAY
- and typeof(data.get("barrier_max_health")) == TYPE_ARRAY
- and typeof(data.get("active_barrier_index")) == TYPE_INT
- and typeof(data.get("visible")) == TYPE_BOOL
- and typeof(data.get("bobber_position")) == TYPE_ARRAY
- and NetworkFishingProtocol.array_to_vector3(
- data["bobber_position"]
- ).is_finite()
- ):
- return false
- var positions: Array = data["barrier_positions"]
- var health: Array = data["barrier_health"]
- var maximum_health: Array = data["barrier_max_health"]
- if (
- positions.size() > 32
- or health.size() != positions.size()
- or maximum_health.size() != positions.size()
- ):
- return false
- var progress: float = float(data["progress"])
- var chase: float = float(data["chase_progress"])
- if (
- not is_finite(progress)
- or not is_finite(chase)
- or progress < -0.01
- or progress > 1.01
- or chase < -10.0
- or chase > 1.01
- ):
- return false
- for index: int in range(positions.size()):
- if (
- typeof(positions[index]) not in [TYPE_FLOAT, TYPE_INT]
- or not is_finite(float(positions[index]))
- or typeof(health[index]) != TYPE_INT
- or typeof(maximum_health[index]) != TYPE_INT
- or int(health[index]) < 0
- or int(maximum_health[index]) < 0
- or int(health[index]) > int(maximum_health[index])
- ):
- return false
- return true
-
-
-func _on_attempt_caught(peer_id: int) -> void:
- var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
- if attempt == null or attempt.phase != NetworkFishingAttempt.Phase.FIGHTING:
- return
- var fish: FishDataType = _fish_catalog.get_fish_by_id(attempt.fish_id)
- if fish == null:
- _cancel_attempt(peer_id, "Fishing attempt ended.")
- return
- var fish_catch: FishCatch = FishCatchType.from_network_dict(
- attempt.catch_payload,
- fish,
- )
- if fish_catch == null or not fish_catch.is_valid():
- _cancel_attempt(peer_id, "Fishing attempt ended.")
- return
- attempt.phase = NetworkFishingAttempt.Phase.PENDING_CAPACITY
- attempt.result_id = _new_id("result")
- attempt.catch_payload = fish_catch.to_network_dict()
- attempt.capacity_nonce = _new_id("capacity")
- attempt.capacity_deadline = (
- Time.get_ticks_msec() / 1000.0 + CAPACITY_RESPONSE_TIMEOUT
- )
- var probe: Dictionary = {
- "attempt_id": attempt.attempt_id,
- "capacity_nonce": attempt.capacity_nonce,
- "catch_id": str(fish_catch.catch_id),
- }
- if peer_id == _session.get_local_peer_id():
- _handle_local_capacity_probe(probe)
- else:
- receive_capacity_probe.rpc_id(peer_id, probe)
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- NetworkFishingProtocol.RELIABLE_CHANNEL,
-)
-func receive_capacity_probe(data: Dictionary) -> void:
- _handle_local_capacity_probe(data)
-
-
-func _handle_local_capacity_probe(data: Dictionary) -> void:
- if (
- typeof(data.get("attempt_id")) != TYPE_STRING
- or typeof(data.get("capacity_nonce")) != TYPE_STRING
- or typeof(data.get("catch_id")) != TYPE_STRING
- ):
- return
- var can_accept: bool = (
- _local_inventory != null
- and _local_inventory.can_accept_catch(
- StringName(data["catch_id"])
- )
- )
- if _session.is_host():
- _handle_capacity_response(
- _session.get_local_peer_id(),
- str(data["attempt_id"]),
- str(data["capacity_nonce"]),
- can_accept
- )
- else:
- submit_capacity_response.rpc_id(
- 1,
- str(data["attempt_id"]),
- str(data["capacity_nonce"]),
- can_accept
- )
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- NetworkFishingProtocol.RELIABLE_CHANNEL,
-)
-func submit_capacity_response(
- attempt_id: String,
- capacity_nonce: String,
- can_accept: bool,
-) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if not _session.is_host() or not _session.is_authenticated_peer(sender_id):
- return
- _handle_capacity_response(
- sender_id, attempt_id, capacity_nonce, can_accept
- )
-
-
-func _handle_capacity_response(
- peer_id: int,
- attempt_id: String,
- capacity_nonce: String,
- can_accept: bool,
-) -> void:
- var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
- if (
- attempt == null
- or attempt.phase != NetworkFishingAttempt.Phase.PENDING_CAPACITY
- or attempt.attempt_id != attempt_id
- or attempt.capacity_nonce != capacity_nonce
- ):
- return
- if not can_accept:
- _cancel_attempt(peer_id, "Inventory is full.")
- return
- _finalize_catch(attempt)
-
-
-func _finalize_catch(attempt: NetworkFishingAttempt) -> void:
- attempt.phase = NetworkFishingAttempt.Phase.CAUGHT
- var outcome: Dictionary = {
- "result_id": attempt.result_id,
- "request_id": attempt.request_id,
- "attempt_id": attempt.attempt_id,
- "owner_peer_id": attempt.owner_peer_id,
- "session_id": attempt.session_id,
- "outcome": int(NetworkFishingProtocol.Outcome.CATCH),
- "catch": attempt.catch_payload.duplicate(true),
- }
- if attempt.owner_peer_id == _session.get_local_peer_id():
- _apply_target_outcome(outcome)
- else:
- receive_target_outcome.rpc_id(attempt.owner_peer_id, outcome)
- _broadcast_public_outcome(
- attempt,
- &"catch",
- "",
- attempt.catch_payload,
- )
- _dispose_attempt(attempt.owner_peer_id)
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- NetworkFishingProtocol.RELIABLE_CHANNEL,
-)
-func receive_target_outcome(data: Dictionary) -> void:
- _apply_target_outcome(data)
-
-
-func _apply_target_outcome(data: Dictionary) -> void:
- if not _validate_target_outcome(data):
- return
- var result_id: String = data["result_id"]
- var catch_data: Dictionary = data["catch"]
- var catch_id := StringName(str(catch_data.get("catch_id", "")))
- if _result_ledgers.has(result_id):
- _acknowledge_result(result_id, catch_id)
- return
- var fish_id := StringName(str(catch_data.get("fish_id", "")))
- var fish: FishDataType = _fish_catalog.get_fish_by_id(fish_id)
- var fish_catch: FishCatch = FishCatchType.from_network_dict(
- catch_data, fish
- )
- if fish_catch == null:
- return
- var already_owned: bool = _local_inventory.contains_catch_id(catch_id)
- if not already_owned and not _local_inventory.can_accept_catch(catch_id):
- return
- if not already_owned:
- var experience_award: int = (
- FishExperienceType.calculate_for_collection(
- fish_catch,
- _local_collection,
- )
- )
- _local_inventory.add_catch(fish_catch)
- _local_collection.record_catch(
- fish_id,
- fish_catch.quality,
- )
- _local_experience.award_experience(experience_award)
- if not _save_manager.save_if_dirty():
- return
- _result_ledgers[result_id] = true
- _bound_result_ledger()
- if not _session.is_host():
- _attempts.erase(_session.get_local_peer_id())
- local_catch_received.emit(fish_catch)
- _acknowledge_result(result_id, catch_id)
-
-
-func _validate_target_outcome(data: Dictionary) -> bool:
- return (
- typeof(data.get("result_id")) == TYPE_STRING
- and typeof(data.get("request_id")) == TYPE_STRING
- and typeof(data.get("attempt_id")) == TYPE_STRING
- and typeof(data.get("owner_peer_id")) == TYPE_INT
- and int(data["owner_peer_id"]) == _session.get_local_peer_id()
- and typeof(data.get("session_id")) == TYPE_STRING
- and str(data["session_id"]) == _session.get_session_id()
- and int(data.get("outcome", -1))
- == NetworkFishingProtocol.Outcome.CATCH
- and typeof(data.get("catch")) == TYPE_DICTIONARY
- )
-
-
-func _acknowledge_result(result_id: String, catch_id: StringName) -> void:
- if _session.is_host():
- return
- acknowledge_fishing_result.rpc_id(1, result_id, str(catch_id))
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- NetworkFishingProtocol.RELIABLE_CHANNEL,
-)
-func acknowledge_fishing_result(result_id: String, catch_id: String) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if (
- not _session.is_host()
- or not _session.is_authenticated_peer(sender_id)
- or result_id.is_empty()
- or catch_id.is_empty()
- ):
- return
- _result_acknowledgements[result_id] = catch_id
- while _result_acknowledgements.size() > MAX_LEDGER_ENTRIES_PER_PEER:
- _result_acknowledgements.erase(
- _result_acknowledgements.keys().front()
- )
-
-
-func _on_attempt_escaped(peer_id: int) -> void:
- var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
- if attempt == null:
- return
- attempt.phase = NetworkFishingAttempt.Phase.ESCAPED
- _broadcast_public_outcome(attempt, &"escape", "")
- _dispose_attempt(peer_id)
-
-
-func _cancel_attempt(
- peer_id: int,
- message: String,
- outcome: StringName = &"cancelled",
-) -> void:
- var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
- if attempt == null:
- return
- attempt.phase = NetworkFishingAttempt.Phase.CANCELLED
- _broadcast_public_outcome(attempt, outcome, message)
- _dispose_attempt(peer_id)
-
-
-func _broadcast_public_outcome(
- attempt: NetworkFishingAttempt,
- outcome: StringName,
- message: String,
- catch_payload: Dictionary = {},
-) -> void:
- var data: Dictionary = {
- "attempt_id": attempt.attempt_id,
- "owner_peer_id": attempt.owner_peer_id,
- "outcome": str(outcome),
- "message": message.left(128),
- }
- if outcome == &"catch" and not catch_payload.is_empty():
- data["catch"] = catch_payload.duplicate(true)
- _apply_public_outcome(data)
- receive_public_outcome.rpc(data)
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- NetworkFishingProtocol.RELIABLE_CHANNEL,
-)
-func receive_public_outcome(data: Dictionary) -> void:
- _apply_public_outcome(data)
-
-
-func _apply_public_outcome(data: Dictionary) -> void:
- if (
- typeof(data.get("owner_peer_id")) != TYPE_INT
- or typeof(data.get("outcome")) != TYPE_STRING
- or typeof(data.get("message")) != TYPE_STRING
- ):
- return
- var peer_id: int = data["owner_peer_id"]
- if peer_id == _session.get_local_peer_id():
- if str(data["outcome"]) != "catch":
- if not _session.is_host():
- var local_attempt: NetworkFishingAttempt = _attempts.get(
- peer_id
- )
- if local_attempt != null:
- _pending_local_bait_by_request.erase(
- local_attempt.request_id
- )
- _attempts.erase(peer_id)
- local_attempt_ended.emit(
- StringName(str(data["outcome"])),
- str(data["message"])
- )
- else:
- var outcome := StringName(str(data["outcome"]))
- if outcome in [&"catch", &"escape", &"withdrawal"]:
- _return_remote_presentation(peer_id, outcome, data)
- else:
- _cleanup_remote_presentation(peer_id)
-
-
-func _make_cast_accepted(attempt: NetworkFishingAttempt) -> Dictionary:
- return {
- "accepted": true,
- "request_id": attempt.request_id,
- "attempt_id": attempt.attempt_id,
- "owner_peer_id": attempt.owner_peer_id,
- "origin": NetworkFishingProtocol.vector3_to_array(attempt.origin),
- "target": NetworkFishingProtocol.vector3_to_array(attempt.target),
- }
-
-
-func _broadcast_cast_accepted(data: Dictionary) -> void:
- _apply_cast_accepted(data)
- receive_cast_accepted.rpc(data)
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- NetworkFishingProtocol.RELIABLE_CHANNEL,
-)
-func receive_cast_accepted(data: Dictionary) -> void:
- _apply_cast_accepted(data)
-
-
-func _apply_cast_accepted(data: Dictionary) -> void:
- if (
- typeof(data.get("attempt_id")) != TYPE_STRING
- or typeof(data.get("owner_peer_id")) != TYPE_INT
- or typeof(data.get("origin")) != TYPE_ARRAY
- or typeof(data.get("target")) != TYPE_ARRAY
- ):
- return
- var peer_id: int = data["owner_peer_id"]
- var origin: Vector3 = NetworkFishingProtocol.array_to_vector3(
- data.get("origin", [])
- )
- var target: Vector3 = NetworkFishingProtocol.array_to_vector3(data["target"])
- if not origin.is_finite() or not target.is_finite():
- return
- if peer_id == _session.get_local_peer_id():
- var attempt := NetworkFishingAttempt.new()
- attempt.owner_peer_id = peer_id
- attempt.request_id = str(data.get("request_id", ""))
- attempt.attempt_id = str(data["attempt_id"])
- attempt.session_id = _session.get_session_id()
- attempt.phase = NetworkFishingAttempt.Phase.WAITING_FOR_BITE
- attempt.target = target
- # On the host this replaces the same authoritative value with itself.
- if not _session.is_host():
- _attempts[peer_id] = attempt
- local_cast_accepted.emit(attempt.attempt_id, target)
- else:
- var presentation := _get_remote_presentation(peer_id)
- if presentation != null:
- presentation.show_cast(
- origin,
- target,
- str(data["attempt_id"]),
- )
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- NetworkFishingProtocol.RELIABLE_CHANNEL,
-)
-func receive_bite_pending(data: Dictionary) -> void:
- _apply_bite_pending(data)
-
-
-func _apply_bite_pending(data: Dictionary) -> void:
- if (
- typeof(data.get("attempt_id")) != TYPE_STRING
- or typeof(data.get("owner_peer_id")) != TYPE_INT
- ):
- return
- if int(data["owner_peer_id"]) == _session.get_local_peer_id():
- local_bite_pending.emit(str(data["attempt_id"]))
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- NetworkFishingProtocol.RELIABLE_CHANNEL,
-)
-func receive_bite_started(data: Dictionary) -> void:
- _apply_bite_started(data)
-
-
-func _apply_bite_started(data: Dictionary) -> void:
- if (
- typeof(data.get("attempt_id")) != TYPE_STRING
- or typeof(data.get("owner_peer_id")) != TYPE_INT
- ):
- return
- var peer_id: int = data["owner_peer_id"]
- if peer_id == _session.get_local_peer_id():
- if not _session.is_host():
- var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
- var pending_bait_id := StringName()
- if attempt != null:
- pending_bait_id = _pending_local_bait_by_request.get(
- attempt.request_id,
- StringName(),
- )
- _pending_local_bait_by_request.erase(attempt.request_id)
- if not pending_bait_id.is_empty() and (
- _local_bag == null
- or not _local_bag.remove_item(pending_bait_id, 1)
- ):
- cancel_local_attempt("No bait available.")
- local_cast_rejected.emit("No bait available.")
- return
- local_bite_started.emit(str(data["attempt_id"]))
- else:
- var presentation := _get_remote_presentation(peer_id)
- if presentation != null:
- presentation.show_bite()
-
-
-func _record_and_reject(
- peer_id: int,
- request_id: String,
- message: String,
-) -> void:
- var response: Dictionary = {
- "accepted": false,
- "request_id": request_id,
- "message": message.left(128),
- }
- _record_request_response(peer_id, request_id, response)
- _send_cast_rejected(peer_id, request_id, message)
-
-
-func _send_cast_rejected(
- peer_id: int,
- request_id: String,
- message: String,
-) -> void:
- if peer_id == _session.get_local_peer_id():
- local_cast_rejected.emit(message)
- else:
- receive_cast_rejected.rpc_id(peer_id, request_id, message.left(128))
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- NetworkFishingProtocol.RELIABLE_CHANNEL,
-)
-func receive_cast_rejected(request_id: String, message: String) -> void:
- _pending_local_bait_by_request.erase(request_id)
- local_cast_rejected.emit(message)
-
-
-func _record_request_response(
- peer_id: int,
- request_id: String,
- response: Dictionary,
-) -> void:
- var ledger: Dictionary = _request_ledgers.get(peer_id, {})
- ledger[request_id] = response.duplicate(true)
- while ledger.size() > MAX_LEDGER_ENTRIES_PER_PEER:
- ledger.erase(ledger.keys().front())
- _request_ledgers[peer_id] = ledger
-
-
-func _resend_request_response(peer_id: int, response: Dictionary) -> void:
- if bool(response.get("accepted", false)):
- if peer_id == _session.get_local_peer_id():
- _apply_cast_accepted(response)
- else:
- receive_cast_accepted.rpc_id(peer_id, response)
- else:
- _send_cast_rejected(
- peer_id,
- str(response.get("request_id", "")),
- str(response.get("message", "Fishing attempt ended."))
- )
-
-
-func _get_remote_presentation(
- peer_id: int,
-) -> RemoteFishingPresentation:
- if not _presentation_enabled:
- return null
- var existing: RemoteFishingPresentation = _remote_presentations.get(peer_id)
- if existing != null and is_instance_valid(existing):
- return existing
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- if avatar == null:
- return null
- var presentation := RemotePresentationType.new()
- presentation.name = "RemoteFishing_%d" % peer_id
- add_child(presentation)
- presentation.setup(avatar)
- _remote_presentations[peer_id] = presentation
- return presentation
-
-
-func _cleanup_remote_presentation(peer_id: int) -> void:
- var presentation: RemoteFishingPresentation = _remote_presentations.get(peer_id)
- _remote_presentations.erase(peer_id)
- if presentation != null and is_instance_valid(presentation):
- presentation.cleanup()
- presentation.queue_free()
-
-
-func _return_remote_presentation(
- peer_id: int,
- outcome: StringName,
- data: Dictionary,
-) -> void:
- var presentation: RemoteFishingPresentation = _remote_presentations.get(
- peer_id
- )
- _remote_presentations.erase(peer_id)
- if presentation == null or not is_instance_valid(presentation):
- return
- presentation.return_completed.connect(
- presentation.queue_free,
- CONNECT_ONE_SHOT,
- )
- var showcase_catch: FishCatch = null
- if outcome == &"catch" and typeof(data.get("catch")) == TYPE_DICTIONARY:
- var catch_data: Dictionary = data["catch"]
- var fish_id := StringName(str(catch_data.get("fish_id", "")))
- var fish: FishDataType = _fish_catalog.get_fish_by_id(fish_id)
- showcase_catch = FishCatchType.from_network_dict(catch_data, fish)
- presentation.play_return(showcase_catch)
-
-
-func _dispose_attempt(peer_id: int) -> void:
- var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
- _attempts.erase(peer_id)
- if attempt != null and attempt.controller != null:
- attempt.controller.reset()
- attempt.controller.queue_free()
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- var local_fishing_sequence_is_active: bool = (
- peer_id == _session.get_local_peer_id()
- and _fishing_spot != null
- and _fishing_spot.is_fishing_sequence_active()
- )
- if avatar != null and not local_fishing_sequence_is_active:
- avatar.set_movement_enabled(true)
-
-
-func _on_peer_removed(peer_id: int) -> void:
- if _session.is_host() and _attempts.has(peer_id):
- _cancel_attempt(peer_id, "Fishing attempt ended.")
- else:
- _attempts.erase(peer_id)
- _cleanup_remote_presentation(peer_id)
- _request_ledgers.erase(peer_id)
- _last_cast_time.erase(peer_id)
- _last_input_time.erase(peer_id)
-
-
-func _on_avatar_removed(peer_id: int) -> void:
- _cleanup_remote_presentation(peer_id)
-
-
-func _on_session_state_changed(state: NetworkSession.State) -> void:
- if state in [
- NetworkSession.State.INACTIVE,
- NetworkSession.State.DISCONNECTING,
- NetworkSession.State.CONNECTION_FAILED,
- NetworkSession.State.SERVER_LOST,
- ]:
- _clear_all()
-
-
-func _clear_all() -> void:
- for peer_id: int in _attempts.keys():
- var attempt: NetworkFishingAttempt = _attempts[peer_id]
- if attempt != null and attempt.controller != null:
- attempt.controller.queue_free()
- _attempts.clear()
- for peer_id: int in _remote_presentations.keys():
- _cleanup_remote_presentation(peer_id)
- _request_ledgers.clear()
- _result_ledgers.clear()
- _result_acknowledgements.clear()
- _last_cast_time.clear()
- _last_input_time.clear()
- _pending_local_bait_by_request.clear()
- _snapshot_accumulator = 0.0
- _observer_snapshot_accumulator = 0.0
- _local_input_sequence = 0
-
-
-func get_network_metrics() -> Dictionary:
- return {
- "active_attempts": _attempts.size(),
- "remote_presentations": _remote_presentations.size(),
- "owner_snapshot_packets_sent": _owner_snapshot_packets_sent,
- "owner_snapshot_states_sent": _owner_snapshot_states_sent,
- "observer_snapshot_packets_sent": _observer_snapshot_packets_sent,
- "observer_snapshot_states_sent": _observer_snapshot_states_sent,
- }
-
-
-func _bound_result_ledger() -> void:
- while _result_ledgers.size() > MAX_LEDGER_ENTRIES_PER_PEER:
- _result_ledgers.erase(_result_ledgers.keys().front())
-
-
-func _new_id(prefix: String) -> String:
- return "%s:%s" % [
- prefix,
- Crypto.new().generate_random_bytes(16).hex_encode(),
- ]
-
-
-func _new_seed() -> int:
- var bytes: PackedByteArray = Crypto.new().generate_random_bytes(8)
- var seed_value: int = 0
- for byte: int in bytes:
- seed_value = (seed_value << 8) ^ byte
- return seed_value
diff --git a/network/network_fishing_service.gd.uid b/network/network_fishing_service.gd.uid
deleted file mode 100644
index 921f52c..0000000
--- a/network/network_fishing_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b4v56htdn1y2l
diff --git a/network/network_home_protocol.gd b/network/network_home_protocol.gd
deleted file mode 100644
index 96756e9..0000000
--- a/network/network_home_protocol.gd
+++ /dev/null
@@ -1,188 +0,0 @@
-class_name NetworkHomeProtocol
-extends RefCounted
-
-const PlayerHomeStateType = preload("res://homes/player_home_state.gd")
-
-const CAPABILITY: StringName = &"portable_home_v1"
-const RELIABLE_CHANNEL: int = NetworkProtocol.HOME_RELIABLE_CHANNEL
-const MAX_ID_LENGTH: int = 96
-const MAX_MESSAGE_LENGTH: int = 160
-const MAX_TRANSITION_COORDINATE: float = 10000.0
-const WORLD_SPACE: StringName = &"world"
-
-enum TransitionAction {
- ENTER,
- EXIT,
-}
-
-
-static func make_space_id(owner_fingerprint: String) -> StringName:
- return StringName("rv:%s" % owner_fingerprint)
-
-
-static func manifest_digest(home_data: Dictionary) -> String:
- var sanitized: Dictionary = PlayerHomeStateType.sanitize_save_data(home_data)
- if sanitized.is_empty():
- return ""
- var canonical_owned: Array[Dictionary] = []
- var owned: Dictionary = sanitized["owned_decor"]
- var owned_ids: Array[String] = []
- for value: Variant in owned.keys():
- owned_ids.append(str(value))
- owned_ids.sort()
- for decor_id: String in owned_ids:
- canonical_owned.append({
- "id": decor_id,
- "quantity": int(owned[decor_id]),
- })
- var placements: Array[Dictionary] = []
- for value: Variant in sanitized["placements"]:
- placements.append((value as Dictionary).duplicate(true))
- placements.sort_custom(func(first: Dictionary, second: Dictionary) -> bool:
- return str(first["instance_id"]) < str(second["instance_id"])
- )
- var canonical: Dictionary = {
- "schema_version": int(sanitized["schema_version"]),
- "tier": int(sanitized["tier"]),
- "exterior_id": str(sanitized["exterior_id"]),
- "privacy": int(sanitized["privacy"]),
- "owned": canonical_owned,
- "placements": placements,
- "revision": int(sanitized["revision"]),
- "next_instance_sequence": int(sanitized["next_instance_sequence"]),
- }
- return JSON.stringify(canonical).sha256_text()
-
-
-static func manifest_signature_fields(data: Dictionary) -> Array:
- return [
- str(data.get("session_id", "")),
- str(data.get("request_id", "")),
- str(data.get("owner_fingerprint", "")),
- int((data.get("home", {}) as Dictionary).get("revision", -1)),
- str(data.get("digest", "")),
- ]
-
-
-static func validate_manifest_request(value: Variant) -> bool:
- if typeof(value) != TYPE_DICTIONARY:
- return false
- var data: Dictionary = value
- if (
- not _valid_id(data.get("request_id"))
- or not _valid_id(data.get("session_id"))
- or not NetworkIdentityCrypto.valid_fingerprint(
- data.get("owner_fingerprint")
- )
- or typeof(data.get("home")) != TYPE_DICTIONARY
- or typeof(data.get("digest")) != TYPE_STRING
- or str(data["digest"]).length() != 64
- or typeof(data.get("sender_signature")) != TYPE_PACKED_BYTE_ARRAY
- ):
- return false
- var sanitized: Dictionary = PlayerHomeStateType.sanitize_save_data(
- data["home"]
- )
- return (
- not sanitized.is_empty()
- and manifest_digest(sanitized) == str(data["digest"])
- )
-
-
-static func validate_manifest_broadcast(value: Variant) -> bool:
- if typeof(value) != TYPE_DICTIONARY:
- return false
- var data: Dictionary = value
- return (
- typeof(data.get("owner_peer_id")) == TYPE_INT
- and int(data["owner_peer_id"]) > 0
- and NetworkIdentityCrypto.valid_fingerprint(
- data.get("owner_fingerprint")
- )
- and typeof(data.get("home")) == TYPE_DICTIONARY
- and not PlayerHomeStateType.sanitize_save_data(data["home"]).is_empty()
- and _valid_transform(data.get("exterior_transform"))
- and typeof(data.get("interior_index")) == TYPE_INT
- and int(data["interior_index"]) >= 0
- and int(data["interior_index"]) < 128
- )
-
-
-static func transition_signature_fields(data: Dictionary) -> Array:
- return [
- str(data.get("session_id", "")),
- str(data.get("request_id", "")),
- int(data.get("action", -1)),
- str(data.get("owner_fingerprint", "")),
- ]
-
-
-static func validate_transition_request(value: Variant) -> bool:
- if typeof(value) != TYPE_DICTIONARY:
- return false
- var data: Dictionary = value
- return (
- _valid_id(data.get("request_id"))
- and _valid_id(data.get("session_id"))
- and typeof(data.get("action")) == TYPE_INT
- and int(data["action"]) >= TransitionAction.ENTER
- and int(data["action"]) <= TransitionAction.EXIT
- and NetworkIdentityCrypto.valid_fingerprint(
- data.get("owner_fingerprint")
- )
- and typeof(data.get("sender_signature")) == TYPE_PACKED_BYTE_ARRAY
- )
-
-
-static func transform_to_array(value: Transform3D) -> Array[float]:
- return [
- value.basis.x.x, value.basis.x.y, value.basis.x.z,
- value.basis.y.x, value.basis.y.y, value.basis.y.z,
- value.basis.z.x, value.basis.z.y, value.basis.z.z,
- value.origin.x, value.origin.y, value.origin.z,
- ]
-
-
-static func array_to_transform(value: Variant) -> Transform3D:
- if not validate_transition_transform(value):
- return Transform3D()
- var fields: Array = value
- return Transform3D(
- Basis(
- Vector3(float(fields[0]), float(fields[1]), float(fields[2])),
- Vector3(float(fields[3]), float(fields[4]), float(fields[5])),
- Vector3(float(fields[6]), float(fields[7]), float(fields[8])),
- ),
- Vector3(float(fields[9]), float(fields[10]), float(fields[11])),
- )
-
-
-static func validate_transition_transform(value: Variant) -> bool:
- if not _valid_transform(value):
- return false
- var fields: Array = value
- return (
- absf(float(fields[9])) <= MAX_TRANSITION_COORDINATE
- and absf(float(fields[10])) <= MAX_TRANSITION_COORDINATE
- and absf(float(fields[11])) <= MAX_TRANSITION_COORDINATE
- )
-
-
-static func _valid_transform(value: Variant) -> bool:
- if typeof(value) != TYPE_ARRAY or (value as Array).size() != 12:
- return false
- for component: Variant in value:
- if (
- typeof(component) not in [TYPE_FLOAT, TYPE_INT]
- or not is_finite(float(component))
- ):
- return false
- return true
-
-
-static func _valid_id(value: Variant) -> bool:
- return (
- typeof(value) == TYPE_STRING
- and not str(value).is_empty()
- and str(value).length() <= MAX_ID_LENGTH
- )
diff --git a/network/network_home_protocol.gd.uid b/network/network_home_protocol.gd.uid
deleted file mode 100644
index 24b6aae..0000000
--- a/network/network_home_protocol.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://fl3qwiluejhm
diff --git a/network/network_home_service.gd b/network/network_home_service.gd
deleted file mode 100644
index 33fa6ee..0000000
--- a/network/network_home_service.gd
+++ /dev/null
@@ -1,1390 +0,0 @@
-class_name NetworkHomeService
-extends Node
-
-const NetworkHomeProtocolType = preload(
- "res://network/network_home_protocol.gd"
-)
-const PlayerHomeStateType = preload("res://homes/player_home_state.gd")
-const DecorCatalogType = preload("res://homes/decor_catalog.gd")
-const OwnedItemType = preload("res://items/owned_item.gd")
-
-signal local_transition_finished(accepted: bool, message: String)
-signal local_space_changed(space_id: StringName, owner_fingerprint: String)
-signal local_manifest_rejected(message: String)
-
-const MAX_MANIFESTS: int = 128
-const MAX_PENDING_LOCAL_DECOR_TRANSACTIONS: int = 16
-
-var _session: NetworkSession
-var _spawn_service: PlayerSpawnService
-var _local_home: PlayerHomeStateType
-var _save_manager: PlayerSaveManager
-var _world_service: HomeWorldService
-var _bag: PlayerBag
-var _inventory_layout: PlayerInventoryLayout
-var _hotbar: PlayerHotbar
-var _manifests_by_fingerprint: Dictionary[String, Dictionary] = {}
-var _fingerprint_by_peer: Dictionary[int, String] = {}
-var _authorized_owned_counts: Dictionary[int, Dictionary] = {}
-var _authorization_result_owner: Dictionary[String, int] = {}
-var _interior_slots_by_space: Dictionary[StringName, Dictionary] = {}
-var _next_interior_index: int = 0
-var _suppress_local_submission: bool = false
-var _publish_queued: bool = false
-var _last_submitted_local_home_revision: int = -1
-var _pending_local_decor_transactions: Dictionary[int, Dictionary] = {}
-
-
-func setup(
- session: NetworkSession,
- spawn_service: PlayerSpawnService,
- local_home: PlayerHomeStateType,
- save_manager: PlayerSaveManager,
- world_service: HomeWorldService,
- bag: PlayerBag,
- inventory_layout: PlayerInventoryLayout,
- hotbar: PlayerHotbar,
-) -> void:
- _session = session
- _spawn_service = spawn_service
- _local_home = local_home
- _save_manager = save_manager
- _world_service = world_service
- _bag = bag
- _inventory_layout = inventory_layout
- _hotbar = hotbar
- if not _session.state_changed.is_connected(_on_session_state_changed):
- _session.state_changed.connect(_on_session_state_changed)
- if not _session.join_authenticated.is_connected(_on_join_authenticated):
- _session.join_authenticated.connect(_on_join_authenticated)
- if not _session.peer_authenticated.is_connected(_on_peer_authenticated):
- _session.peer_authenticated.connect(_on_peer_authenticated)
- if not _session.peer_removed.is_connected(_on_peer_removed):
- _session.peer_removed.connect(_on_peer_removed)
- if not _local_home.changed.is_connected(_on_local_home_changed):
- _local_home.changed.connect(_on_local_home_changed)
-
-
-func get_local_space() -> StringName:
- if _session == null or not _session.is_gameplay_session_active():
- return NetworkHomeProtocolType.WORLD_SPACE
- return _session.get_peer_space(_session.get_local_peer_id())
-
-
-func get_local_home_owner_fingerprint() -> String:
- var space_id: String = String(get_local_space())
- return space_id.trim_prefix("rv:") if space_id.begins_with("rv:") else ""
-
-
-func can_local_decorate() -> bool:
- return (
- get_local_home_owner_fingerprint()
- == _session.get_local_identity_fingerprint()
- )
-
-
-func has_pending_local_decor_mutation() -> bool:
- return not _pending_local_decor_transactions.is_empty()
-
-
-func get_authoritative_owned_count(
- peer_id: int,
- decor_id: StringName,
-) -> int:
- var authorized: Dictionary = _authorized_owned_counts.get(peer_id, {})
- if authorized.has(String(decor_id)):
- return int(authorized[String(decor_id)])
- var fingerprint: String = _fingerprint_for_peer(peer_id)
- var manifest: Dictionary = _manifests_by_fingerprint.get(fingerprint, {})
- if manifest.is_empty():
- return -1
- var home: Dictionary = manifest.get("home", {})
- var owned: Dictionary = home.get("owned_decor", {})
- return int(owned.get(String(decor_id), 0))
-
-
-func place_local_decor(
- decor_id: StringName,
- position: Vector3,
- yaw_radians: float,
- mode: PlayerHomeStateType.PlacementMode,
-) -> String:
- if (
- not can_local_decorate()
- or has_pending_local_decor_mutation()
- or _bag == null
- or _bag.get_quantity(decor_id) <= 0
- or _world_service == null
- ):
- return ""
- var owner_fingerprint: String = (
- _session.get_local_identity_fingerprint()
- )
- var normalized: Dictionary = (
- _local_home.get_nearest_valid_placement_transform(
- decor_id,
- position,
- yaw_radians,
- mode,
- "",
- Callable(
- self, "_candidate_clear_of_players"
- ).bind(owner_fingerprint, decor_id),
- )
- )
- if normalized.is_empty():
- return ""
- var transaction: Dictionary = _capture_local_decor_transaction()
- if transaction.is_empty():
- return ""
- _suppress_local_submission = true
- var instance_id: String = _local_home.place_decor(
- decor_id,
- normalized["position"],
- float(normalized["yaw"]),
- mode,
- )
- if instance_id.is_empty():
- _suppress_local_submission = false
- return ""
- if not _bag.remove_item(decor_id, 1):
- _local_home.remove_placement(instance_id)
- _suppress_local_submission = false
- return ""
- _suppress_local_submission = false
- if not _commit_local_decor_transaction(transaction):
- return ""
- return instance_id
-
-
-func remove_local_decor(instance_id: String) -> bool:
- if (
- not can_local_decorate()
- or has_pending_local_decor_mutation()
- or _bag == null
- or _inventory_layout == null
- or _hotbar == null
- ):
- return false
- var placement: Dictionary = _local_home.get_placement(instance_id)
- if placement.is_empty():
- return false
- var transaction: Dictionary = _capture_local_decor_transaction()
- if transaction.is_empty():
- return false
- var decor_id := StringName(str(placement["decor_id"]))
- var target_hotbar_slot: int = _hotbar.find_item_slot(decor_id)
- if target_hotbar_slot < 0:
- target_hotbar_slot = _hotbar.find_first_empty_slot()
- var prepared_hotbar_placement: bool = false
- if (
- target_hotbar_slot >= 0
- and _inventory_layout.get_container(
- PlayerInventoryLayout.EntryKind.ITEM,
- decor_id,
- ) < 0
- ):
- prepared_hotbar_placement = (
- _inventory_layout.prepare_new_item_placement(
- decor_id,
- PlayerInventoryLayout.InventoryContainer.HOTBAR,
- target_hotbar_slot,
- )
- )
- var returned_to_inventory: bool = _bag.add_item(decor_id, 1)
- if not returned_to_inventory:
- if prepared_hotbar_placement:
- _inventory_layout.cancel_prepared_item_placement(decor_id)
- returned_to_inventory = _bag.add_item_to_storage(decor_id, 1)
- if not returned_to_inventory:
- if prepared_hotbar_placement:
- _inventory_layout.cancel_prepared_item_placement(decor_id)
- return false
- if target_hotbar_slot >= 0:
- _hotbar.assign_item(target_hotbar_slot, decor_id)
- _hotbar.select_slot(target_hotbar_slot)
- _suppress_local_submission = true
- if not _local_home.remove_placement(instance_id):
- _suppress_local_submission = false
- _restore_local_decor_transaction(transaction)
- return false
- _suppress_local_submission = false
- if not _commit_local_decor_transaction(transaction):
- return false
- return true
-
-
-func update_local_decor(
- instance_id: String,
- position: Vector3,
- yaw_radians: float,
- mode: PlayerHomeStateType.PlacementMode,
-) -> bool:
- if has_pending_local_decor_mutation():
- return false
- var normalized: Dictionary = get_valid_local_decor_transform(
- instance_id, position, yaw_radians, mode
- )
- if normalized.is_empty():
- return false
- var transaction: Dictionary = _capture_local_decor_transaction()
- if transaction.is_empty():
- return false
- _suppress_local_submission = true
- if not _local_home.update_placement(
- instance_id,
- normalized["position"],
- float(normalized["yaw"]),
- mode,
- ):
- _suppress_local_submission = false
- return false
- _suppress_local_submission = false
- if not _commit_local_decor_transaction(transaction):
- return false
- return true
-
-
-func get_valid_local_decor_transform(
- instance_id: String,
- position: Vector3,
- yaw_radians: float,
- mode: PlayerHomeStateType.PlacementMode,
-) -> Dictionary:
- if not can_local_decorate() or _world_service == null:
- return {}
- var placement: Dictionary = _local_home.get_placement(instance_id)
- if placement.is_empty():
- return {}
- var decor_id := StringName(str(placement.get("decor_id", "")))
- return _local_home.get_valid_update_transform(
- instance_id,
- position,
- yaw_radians,
- mode,
- Callable(
- self, "_candidate_clear_of_players"
- ).bind(
- _session.get_local_identity_fingerprint(), decor_id
- ),
- )
-
-
-func _candidate_clear_of_players(
- candidate: Dictionary,
- owner_fingerprint: String,
- decor_id: StringName,
-) -> bool:
- return (
- _world_service != null
- and _world_service.is_decor_placement_clear_of_players(
- owner_fingerprint,
- decor_id,
- candidate["position"],
- float(candidate["yaw"]),
- )
- )
-
-
-func request_enter(owner_fingerprint: String) -> bool:
- if (
- _session == null
- or not _session.is_gameplay_session_active()
- or not _manifests_by_fingerprint.has(owner_fingerprint)
- or get_local_space() != NetworkHomeProtocolType.WORLD_SPACE
- ):
- return false
- return _submit_transition(
- NetworkHomeProtocolType.TransitionAction.ENTER,
- owner_fingerprint,
- )
-
-
-func request_exit() -> bool:
- var owner_fingerprint: String = get_local_home_owner_fingerprint()
- if owner_fingerprint.is_empty():
- return false
- return _submit_transition(
- NetworkHomeProtocolType.TransitionAction.EXIT,
- owner_fingerprint,
- )
-
-
-func authorize_decor_purchase(
- peer_id: int,
- product_id: StringName,
- resulting_count: int,
- result_id: String,
-) -> void:
- if not _session.is_host() or peer_id <= 0 or result_id.is_empty():
- return
- var counts: Dictionary = _authorized_owned_counts.get(peer_id, {})
- counts[String(product_id)] = resulting_count
- _authorized_owned_counts[peer_id] = counts
- _authorization_result_owner[result_id] = peer_id
-
-
-func finalize_decor_purchase(
- peer_id: int,
- result_id: String,
- applied: bool,
-) -> void:
- if _authorization_result_owner.get(result_id, 0) != peer_id:
- return
- _authorization_result_owner.erase(result_id)
- if applied:
- return
- _authorized_owned_counts.erase(peer_id)
-
-
-func refresh_world_presentations() -> void:
- if _world_service == null:
- return
- _world_service.clear_presentations()
- if not _world_service.is_world_ready():
- return
- if not _session.is_host():
- for manifest: Dictionary in _manifests_by_fingerprint.values():
- _world_service.apply_manifest(manifest)
- return
- var local_fingerprint: String = _session.get_local_identity_fingerprint()
- if (
- not _session.is_dedicated_host()
- and not _manifests_by_fingerprint.has(local_fingerprint)
- ):
- publish_local_home()
- var fingerprints: Array[String] = []
- fingerprints.assign(_manifests_by_fingerprint.keys())
- fingerprints.sort()
- var reserved: Array[Transform3D] = []
- for fingerprint: String in fingerprints:
- var manifest: Dictionary = _manifests_by_fingerprint[fingerprint]
- var transform: Transform3D = _world_service.choose_exterior_transform(
- int(manifest["owner_peer_id"]), reserved
- )
- reserved.append(transform)
- manifest["exterior_transform"] = (
- NetworkHomeProtocolType.transform_to_array(transform)
- )
- _manifests_by_fingerprint[fingerprint] = manifest
- _broadcast_manifest(manifest)
- for fingerprint: String in fingerprints:
- var manifest: Dictionary = _manifests_by_fingerprint[fingerprint]
- _spawn_peer_at_exterior(
- int(manifest["owner_peer_id"]), fingerprint
- )
-
-
-func publish_local_home(
- allow_pending_decor_transaction: bool = false,
-) -> bool:
- if (
- _session == null
- or not _session.is_gameplay_session_active()
- or _session.is_dedicated_host()
- or _local_home == null
- ):
- return false
- # A purchase can update the owned decor count while an optimistic placement
- # is waiting on the host. Hold that follow-up manifest until the placement is
- # accepted or rolled back; otherwise it can carry the still-unverified
- # placement and turn one rejection into a second stale rejection.
- if (
- not allow_pending_decor_transaction
- and has_pending_local_decor_mutation()
- ):
- _publish_queued = true
- return true
- if _session.is_host() and (
- _world_service == null or not _world_service.is_world_ready()
- ):
- return false
- var home_data: Dictionary = _local_home.to_save_data()
- var revision: int = int(home_data.get("revision", -1))
- if revision >= 0 and revision == _last_submitted_local_home_revision:
- return true
- var request: Dictionary = {
- "request_id": _new_id("home"),
- "session_id": _session.get_session_id(),
- "owner_fingerprint": _session.get_local_identity_fingerprint(),
- "home": home_data,
- "digest": NetworkHomeProtocolType.manifest_digest(home_data),
- }
- request["sender_signature"] = _session.sign_local_action(
- "portable_home_manifest",
- NetworkHomeProtocolType.manifest_signature_fields(request),
- )
- _last_submitted_local_home_revision = revision
- if _session.is_host():
- _process_manifest_request(_session.get_local_peer_id(), request)
- else:
- submit_home_manifest.rpc_id(1, request)
- return true
-
-
-@rpc(
- "any_peer", "call_remote", "reliable",
- NetworkHomeProtocolType.RELIABLE_CHANNEL,
-)
-func submit_home_manifest(data: Dictionary) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if (
- _session == null
- or not _session.is_host()
- or not _session.is_authenticated_peer(sender_id)
- ):
- return
- _process_manifest_request(sender_id, data)
-
-
-func _process_manifest_request(peer_id: int, data: Dictionary) -> void:
- if (
- not NetworkHomeProtocolType.validate_manifest_request(data)
- or str(data["session_id"]) != _session.get_session_id()
- ):
- _reject_manifest(peer_id, "The RV update was invalid.")
- return
- var record := _session.get_peer_record(peer_id)
- var fingerprint: String = str(data["owner_fingerprint"])
- if (
- record == null
- or record.identity_fingerprint != fingerprint
- or not _session.verify_peer_action(
- peer_id,
- "portable_home_manifest",
- NetworkHomeProtocolType.manifest_signature_fields(data),
- data["sender_signature"],
- )
- ):
- _reject_manifest(peer_id, "The RV owner could not be verified.")
- return
- var home_data: Dictionary = PlayerHomeStateType.sanitize_save_data(
- data["home"]
- )
- var prior: Dictionary = _manifests_by_fingerprint.get(fingerprint, {})
- var is_first_manifest: bool = prior.is_empty()
- if not _valid_manifest_revision(peer_id, prior, home_data):
- _reject_manifest(peer_id, "The RV update was rejected by the host.")
- return
- if not _moved_decor_clear_of_players(fingerprint, prior, home_data):
- _reject_manifest(peer_id, "A player is in the way of that decor.")
- return
- var manifest: Dictionary
- if prior.is_empty():
- if _manifests_by_fingerprint.size() >= MAX_MANIFESTS:
- _reject_manifest(peer_id, "This room cannot place another RV.")
- return
- var reserved: Array[Transform3D] = []
- for existing: Dictionary in _manifests_by_fingerprint.values():
- reserved.append(NetworkHomeProtocolType.array_to_transform(
- existing["exterior_transform"]
- ))
- var exterior_transform: Transform3D = (
- _world_service.choose_exterior_transform(peer_id, reserved)
- )
- manifest = {
- "owner_peer_id": peer_id,
- "owner_fingerprint": fingerprint,
- "home": home_data,
- "exterior_transform": (
- NetworkHomeProtocolType.transform_to_array(exterior_transform)
- ),
- "interior_index": _next_interior_index,
- }
- _next_interior_index += 1
- else:
- manifest = prior.duplicate(true)
- manifest["home"] = home_data
- manifest["owner_peer_id"] = peer_id
- _manifests_by_fingerprint[fingerprint] = manifest
- _fingerprint_by_peer[peer_id] = fingerprint
- _consume_owned_authorizations(peer_id, home_data)
- if peer_id == _session.get_local_peer_id():
- _discard_local_decor_transactions_through(
- int(home_data.get("revision", -1))
- )
- if _last_submitted_local_home_revision == int(
- home_data.get("revision", -1)
- ):
- _last_submitted_local_home_revision = -1
- _flush_queued_local_home_publish()
- _broadcast_manifest(manifest)
- if is_first_manifest:
- _spawn_peer_at_exterior(peer_id, fingerprint)
-
-
-func _valid_manifest_revision(
- peer_id: int,
- prior_manifest: Dictionary,
- home_data: Dictionary,
-) -> bool:
- if prior_manifest.is_empty():
- # A portable RV is owned by the player's signed local save. A new host has
- # no prior room ledger with which to compare its first manifest.
- return true
- var prior_home: Dictionary = prior_manifest["home"]
- if (
- int(home_data["revision"]) <= int(prior_home["revision"])
- or int(home_data["tier"]) != int(prior_home["tier"])
- or str(home_data["exterior_id"]) != str(prior_home["exterior_id"])
- ):
- return false
- var prior_owned: Dictionary = prior_home["owned_decor"]
- var next_owned: Dictionary = home_data["owned_decor"]
- var all_ids: Dictionary[String, bool] = {}
- for value: Variant in prior_owned.keys():
- all_ids[str(value)] = true
- for value: Variant in next_owned.keys():
- all_ids[str(value)] = true
- var authorized: Dictionary = _authorized_owned_counts.get(peer_id, {})
- for product_id: String in all_ids:
- var prior_count: int = int(prior_owned.get(product_id, 0))
- var next_count: int = int(next_owned.get(product_id, 0))
- if prior_count == next_count:
- continue
- if next_count < prior_count or int(authorized.get(product_id, -1)) != next_count:
- return false
- return true
-
-
-func _moved_decor_clear_of_players(
- owner_fingerprint: String,
- prior_manifest: Dictionary,
- home_data: Dictionary,
-) -> bool:
- if _world_service == null:
- return false
- var prior_by_instance: Dictionary[String, Dictionary] = {}
- if not prior_manifest.is_empty():
- var prior_home: Dictionary = prior_manifest.get("home", {})
- for value: Variant in prior_home.get("placements", []):
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var prior_placement: Dictionary = value
- prior_by_instance[str(
- prior_placement.get("instance_id", "")
- )] = prior_placement
- for value: Variant in home_data.get("placements", []):
- if typeof(value) != TYPE_DICTIONARY:
- return false
- var placement: Dictionary = value
- var instance_id: String = str(placement.get("instance_id", ""))
- var prior_placement: Dictionary = prior_by_instance.get(
- instance_id, {}
- )
- if (
- not prior_placement.is_empty()
- and _same_decor_transform(prior_placement, placement)
- ):
- continue
- var decor_id := StringName(str(placement.get("decor_id", "")))
- var position: Vector3 = _placement_position(placement)
- if (
- not position.is_finite()
- or not _world_service.is_decor_placement_clear_of_players(
- owner_fingerprint,
- decor_id,
- position,
- float(placement.get("yaw", 0.0)),
- )
- ):
- return false
- return true
-
-
-static func _same_decor_transform(
- first: Dictionary,
- second: Dictionary,
-) -> bool:
- if str(first.get("decor_id", "")) != str(second.get("decor_id", "")):
- return false
- var first_position: Vector3 = _placement_position(first)
- var second_position: Vector3 = _placement_position(second)
- return (
- first_position.is_equal_approx(second_position)
- and is_equal_approx(
- float(first.get("yaw", 0.0)),
- float(second.get("yaw", 0.0)),
- )
- )
-
-
-static func _placement_position(placement: Dictionary) -> Vector3:
- var values: Array = placement.get("position", [])
- if values.size() != 3:
- return Vector3.INF
- return Vector3(
- float(values[0]), float(values[1]), float(values[2])
- )
-
-
-func _consume_owned_authorizations(peer_id: int, home_data: Dictionary) -> void:
- var authorized: Dictionary = _authorized_owned_counts.get(peer_id, {})
- if authorized.is_empty():
- return
- var owned: Dictionary = home_data["owned_decor"]
- for product_id: String in authorized.keys():
- if int(owned.get(product_id, -1)) == int(authorized[product_id]):
- authorized.erase(product_id)
- if authorized.is_empty():
- _authorized_owned_counts.erase(peer_id)
- else:
- _authorized_owned_counts[peer_id] = authorized
-
-
-func _broadcast_manifest(manifest: Dictionary) -> void:
- if _world_service != null and _world_service.is_world_ready():
- _world_service.apply_manifest(manifest)
- for peer_id: int in _session.get_authenticated_peer_ids():
- if peer_id == 1:
- continue
- receive_home_manifest.rpc_id(peer_id, manifest)
-
-
-@rpc(
- "authority", "call_remote", "reliable",
- NetworkHomeProtocolType.RELIABLE_CHANNEL,
-)
-func receive_home_manifest(data: Dictionary) -> void:
- if not NetworkHomeProtocolType.validate_manifest_broadcast(data):
- return
- var fingerprint: String = str(data["owner_fingerprint"])
- _manifests_by_fingerprint[fingerprint] = data.duplicate(true)
- _fingerprint_by_peer[int(data["owner_peer_id"])] = fingerprint
- if fingerprint == _session.get_local_identity_fingerprint():
- var home: Dictionary = data.get("home", {})
- _discard_local_decor_transactions_through(
- int(home.get("revision", -1))
- )
- if _last_submitted_local_home_revision == int(
- home.get("revision", -1)
- ):
- _last_submitted_local_home_revision = -1
- _flush_queued_local_home_publish()
- if _world_service != null and _world_service.is_world_ready():
- _world_service.apply_manifest(data)
-
-
-func _reject_manifest(peer_id: int, message: String) -> void:
- var fingerprint: String = _fingerprint_for_peer(peer_id)
- var canonical: Dictionary = _manifests_by_fingerprint.get(fingerprint, {})
- if peer_id == _session.get_local_peer_id():
- _apply_manifest_rejection(message, canonical)
- else:
- receive_home_manifest_rejection.rpc_id(peer_id, message, canonical)
-
-
-@rpc(
- "authority", "call_remote", "reliable",
- NetworkHomeProtocolType.RELIABLE_CHANNEL,
-)
-func receive_home_manifest_rejection(
- message: String,
- canonical: Dictionary,
-) -> void:
- _apply_manifest_rejection(message, canonical)
-
-
-func _apply_manifest_rejection(message: String, canonical: Dictionary) -> void:
- _last_submitted_local_home_revision = -1
- var canonical_home: Dictionary = canonical.get("home", {})
- var canonical_revision: int = int(canonical_home.get("revision", -1))
- var transaction: Dictionary = _get_local_decor_rejection_transaction(
- canonical_revision
- )
- if canonical.is_empty() or not NetworkHomeProtocolType.validate_manifest_broadcast(canonical):
- if not transaction.is_empty():
- _restore_local_decor_transaction(transaction)
- _pending_local_decor_transactions.clear()
- if _save_manager != null and not _save_manager.save_if_dirty():
- push_warning("Unable to save the restored RV decor state.")
- _flush_queued_local_home_publish()
- local_manifest_rejected.emit(message)
- return
- # A shop result can be accepted locally while its own home manifest is still
- # in flight. A delayed rejection for an earlier placement must not erase that
- # accepted purchase just because the host's canonical manifest predates it.
- var locally_owned_decor: Dictionary[StringName, int] = (
- _capture_local_owned_decor_counts()
- )
- # Only carry ownership forward across an actual pending decor-action
- # rejection. A standalone rejected manifest must remain authoritative rather
- # than repeatedly re-submitting an unverified owned-count increase.
- var has_owned_decor_increase: bool = (
- not transaction.is_empty()
- and _has_local_owned_decor_increase(
- locally_owned_decor,
- canonical_home,
- )
- )
- _suppress_local_submission = true
- var restored: bool = _local_home.restore_from_save_data(canonical_home)
- var owned_decor_restored: bool = (
- restored
- and (
- not has_owned_decor_increase
- or _restore_local_owned_decor_increases(locally_owned_decor)
- )
- )
- var reconciled: bool = (
- restored
- and owned_decor_restored
- and _reconcile_local_decor_inventory(transaction)
- )
- _suppress_local_submission = false
- _discard_local_decor_transactions_after(canonical_revision)
- if not reconciled:
- push_error("Unable to reconcile RV decor after a host rejection.")
- elif _save_manager != null and not _save_manager.save_if_dirty():
- push_warning("Unable to save the restored RV decor state.")
- receive_home_manifest(canonical)
- if reconciled and has_owned_decor_increase:
- # Submit the preserved purchase as a fresh revision after applying the
- # canonical placement rollback. This is deliberately separate from the
- # rejected action, so the host can validate the already-authorized count.
- if not publish_local_home():
- push_warning("Unable to publish the restored RV decor ownership.")
- local_manifest_rejected.emit(message)
-
-
-func _capture_local_decor_transaction() -> Dictionary:
- if (
- _local_home == null
- or _bag == null
- or _inventory_layout == null
- or _hotbar == null
- ):
- return {}
- var saved_items: Array[OwnedItemType] = []
- for item: OwnedItemType in _bag.get_all_items():
- saved_items.append(item.duplicate_record())
- return {
- "home": _local_home.to_save_data(),
- "bag_items": saved_items,
- "layout": _inventory_layout.to_save_data(),
- "hotbar_slots": _hotbar.get_slots(),
- "hotbar_fish_slots": _hotbar.get_fish_slots(),
- "selected_hotbar_slot": _hotbar.get_selected_slot(),
- }
-
-
-func _capture_local_owned_decor_counts() -> Dictionary[StringName, int]:
- var counts: Dictionary[StringName, int] = {}
- if _local_home == null:
- return counts
- for decor_id: StringName in DecorCatalogType.get_product_ids():
- counts[decor_id] = _local_home.get_owned_count(decor_id)
- return counts
-
-
-func _has_local_owned_decor_increase(
- local_counts: Dictionary[StringName, int],
- canonical_home: Dictionary,
-) -> bool:
- var canonical_owned: Dictionary = canonical_home.get("owned_decor", {})
- for decor_id: StringName in DecorCatalogType.get_product_ids():
- if int(local_counts.get(decor_id, 0)) > int(
- canonical_owned.get(String(decor_id), 0)
- ):
- return true
- return false
-
-
-func _restore_local_owned_decor_increases(
- local_counts: Dictionary[StringName, int],
-) -> bool:
- if _local_home == null:
- return false
- for decor_id: StringName in DecorCatalogType.get_product_ids():
- var local_count: int = int(local_counts.get(decor_id, 0))
- var canonical_count: int = _local_home.get_owned_count(decor_id)
- while canonical_count < local_count:
- if not _local_home.add_owned_decor(decor_id, 1):
- return false
- canonical_count += 1
- return true
-
-
-func _commit_local_decor_transaction(transaction: Dictionary) -> bool:
- if _save_manager == null or not _save_manager.save_if_dirty():
- _restore_local_decor_transaction(transaction)
- return false
- var revision: int = _local_home.get_revision()
- _pending_local_decor_transactions[revision] = transaction.duplicate(true)
- _trim_pending_local_decor_transactions()
- if publish_local_home(true):
- return true
- _pending_local_decor_transactions.erase(revision)
- _restore_local_decor_transaction(transaction)
- _flush_queued_local_home_publish()
- # The optimistic mutation was already written before we attempted to publish
- # it. If the session ended or could not accept the manifest, immediately
- # persist the restored state so a restart cannot resurrect the rejected
- # placement or consume its decor item.
- if _save_manager != null and not _save_manager.save_if_dirty():
- push_warning("Unable to save the restored RV decor state.")
- return false
-
-
-func _restore_local_decor_transaction(transaction: Dictionary) -> bool:
- if (
- transaction.is_empty()
- or _local_home == null
- or _bag == null
- or _inventory_layout == null
- or _hotbar == null
- ):
- return false
- var home_data: Variant = transaction.get("home", {})
- var layout_data: Variant = transaction.get("layout", {})
- var stored_items: Variant = transaction.get("bag_items", [])
- var stored_slots: Variant = transaction.get("hotbar_slots", [])
- var stored_fish_slots: Variant = transaction.get("hotbar_fish_slots", [])
- if (
- typeof(home_data) != TYPE_DICTIONARY
- or typeof(layout_data) != TYPE_DICTIONARY
- or typeof(stored_items) != TYPE_ARRAY
- or typeof(stored_slots) != TYPE_ARRAY
- or typeof(stored_fish_slots) != TYPE_ARRAY
- ):
- return false
- var items: Array[OwnedItemType] = []
- for value: Variant in stored_items as Array:
- if not value is OwnedItemType:
- return false
- items.append((value as OwnedItemType).duplicate_record())
- var slots: Array[StringName] = []
- for value: Variant in stored_slots as Array:
- slots.append(StringName(str(value)))
- var fish_slots: Array[StringName] = []
- for value: Variant in stored_fish_slots as Array:
- fish_slots.append(StringName(str(value)))
- var was_suppressed: bool = _suppress_local_submission
- _suppress_local_submission = true
- var home_restored: bool = _local_home.restore_from_save_data(
- home_data as Dictionary
- )
- var bag_restored: bool = _bag.replace_all_items(items)
- var layout_restored: bool = _inventory_layout.restore_from_save_data(
- layout_data as Dictionary
- )
- var hotbar_restored: bool = _hotbar.replace_state(
- slots,
- int(transaction.get("selected_hotbar_slot", 0)),
- fish_slots,
- false,
- )
- _suppress_local_submission = was_suppressed
- return home_restored and bag_restored and layout_restored and hotbar_restored
-
-
-func _reconcile_local_decor_inventory(transaction: Dictionary) -> bool:
- if _bag == null or _local_home == null:
- return false
- var preferred_slots: Dictionary[StringName, int] = (
- _decor_hotbar_slots_from_transaction(transaction)
- )
- for decor_id: StringName in DecorCatalogType.get_product_ids():
- var target_quantity: int = _local_home.get_available_count(decor_id)
- var current_quantity: int = _bag.get_quantity(decor_id)
- if current_quantity > target_quantity:
- if not _bag.remove_item(
- decor_id, current_quantity - target_quantity
- ):
- return false
- continue
- while current_quantity < target_quantity:
- var preferred_slot: int = int(
- preferred_slots.get(decor_id, -1)
- )
- if not _restore_decor_item_to_bag(decor_id, preferred_slot):
- return false
- current_quantity += 1
- return true
-
-
-func _decor_hotbar_slots_from_transaction(
- transaction: Dictionary,
-) -> Dictionary[StringName, int]:
- var result: Dictionary[StringName, int] = {}
- var stored_slots: Variant = transaction.get("hotbar_slots", [])
- if typeof(stored_slots) != TYPE_ARRAY:
- return result
- for slot_index: int in range((stored_slots as Array).size()):
- var decor_id := StringName(str((stored_slots as Array)[slot_index]))
- if DecorCatalogType.has_product(decor_id):
- result[decor_id] = slot_index
- return result
-
-
-func _restore_decor_item_to_bag(
- decor_id: StringName,
- preferred_hotbar_slot: int,
-) -> bool:
- if _bag == null:
- return false
- var prepared_slot: int = -1
- if (
- _inventory_layout != null
- and _hotbar != null
- and _inventory_layout.get_container(
- PlayerInventoryLayout.EntryKind.ITEM, decor_id
- ) < 0
- ):
- var candidate_slot: int = preferred_hotbar_slot
- if candidate_slot < 0:
- candidate_slot = _hotbar.find_first_empty_slot()
- if candidate_slot >= 0 and _inventory_layout.prepare_new_item_placement(
- decor_id,
- PlayerInventoryLayout.InventoryContainer.HOTBAR,
- candidate_slot,
- ):
- prepared_slot = candidate_slot
- var restored: bool = _bag.add_item(decor_id, 1)
- if not restored:
- if prepared_slot >= 0:
- _inventory_layout.cancel_prepared_item_placement(decor_id)
- if _bag.get_owned_item(decor_id) == null:
- restored = _bag.add_item_to_storage(decor_id, 1)
- if restored and prepared_slot >= 0:
- _hotbar.assign_item(prepared_slot, decor_id)
- _hotbar.select_slot(prepared_slot)
- return restored
-
-
-func _get_local_decor_rejection_transaction(
- canonical_revision: int,
-) -> Dictionary:
- var revisions: Array[int] = []
- revisions.assign(_pending_local_decor_transactions.keys())
- revisions.sort()
- for revision: int in revisions:
- if canonical_revision < 0 or revision > canonical_revision:
- return _pending_local_decor_transactions[revision].duplicate(true)
- return {}
-
-
-func _discard_local_decor_transactions_through(revision: int) -> void:
- for pending_revision: int in _pending_local_decor_transactions.keys():
- if pending_revision <= revision:
- _pending_local_decor_transactions.erase(pending_revision)
-
-
-func _discard_local_decor_transactions_after(revision: int) -> void:
- for pending_revision: int in _pending_local_decor_transactions.keys():
- if pending_revision > revision:
- _pending_local_decor_transactions.erase(pending_revision)
-
-
-func _trim_pending_local_decor_transactions() -> void:
- var revisions: Array[int] = []
- revisions.assign(_pending_local_decor_transactions.keys())
- revisions.sort()
- while revisions.size() > MAX_PENDING_LOCAL_DECOR_TRANSACTIONS:
- _pending_local_decor_transactions.erase(revisions.pop_front())
-
-
-func _submit_transition(action: int, owner_fingerprint: String) -> bool:
- var request: Dictionary = {
- "request_id": _new_id("home_transition"),
- "session_id": _session.get_session_id(),
- "action": action,
- "owner_fingerprint": owner_fingerprint,
- }
- request["sender_signature"] = _session.sign_local_action(
- "portable_home_transition",
- NetworkHomeProtocolType.transition_signature_fields(request),
- )
- if _session.is_host():
- _process_transition_request(_session.get_local_peer_id(), request)
- else:
- submit_home_transition.rpc_id(1, request)
- return true
-
-
-@rpc(
- "any_peer", "call_remote", "reliable",
- NetworkHomeProtocolType.RELIABLE_CHANNEL,
-)
-func submit_home_transition(data: Dictionary) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if (
- not _session.is_host()
- or not _session.is_authenticated_peer(sender_id)
- ):
- return
- _process_transition_request(sender_id, data)
-
-
-func _process_transition_request(peer_id: int, data: Dictionary) -> void:
- if (
- not NetworkHomeProtocolType.validate_transition_request(data)
- or str(data["session_id"]) != _session.get_session_id()
- or not _session.verify_peer_action(
- peer_id,
- "portable_home_transition",
- NetworkHomeProtocolType.transition_signature_fields(data),
- data["sender_signature"],
- )
- ):
- _send_transition_rejection(peer_id, "The RV transition was invalid.")
- return
- var owner_fingerprint: String = str(data["owner_fingerprint"])
- var action: int = int(data["action"])
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- var manifest: Dictionary = _manifests_by_fingerprint.get(
- owner_fingerprint, {}
- )
- if avatar == null or manifest.is_empty():
- _send_transition_rejection(peer_id, "That RV is unavailable.")
- return
- if action == NetworkHomeProtocolType.TransitionAction.ENTER:
- var record := _session.get_peer_record(peer_id)
- var owner_is_local: bool = (
- record != null and record.identity_fingerprint == owner_fingerprint
- )
- var privacy: int = int((manifest["home"] as Dictionary)["privacy"])
- if (
- (not owner_is_local and privacy != PlayerHomeStateType.Privacy.OPEN)
- or _session.get_peer_space(peer_id)
- != NetworkHomeProtocolType.WORLD_SPACE
- or not _world_service.is_avatar_near_exterior(
- avatar, owner_fingerprint
- )
- ):
- _send_transition_rejection(peer_id, "You cannot enter that RV.")
- return
- var interior_space: StringName = (
- NetworkHomeProtocolType.make_space_id(owner_fingerprint)
- )
- var visitor_slot: int = _reserve_interior_slot(
- interior_space, peer_id
- )
- _apply_host_transition(
- peer_id,
- interior_space,
- owner_fingerprint,
- _world_service.get_interior_spawn_transform(
- owner_fingerprint, visitor_slot
- ),
- )
- return
- if (
- _session.get_peer_space(peer_id)
- != NetworkHomeProtocolType.make_space_id(owner_fingerprint)
- or not _world_service.is_avatar_near_interior_exit(
- avatar, owner_fingerprint
- )
- ):
- _send_transition_rejection(peer_id, "Move closer to the RV door.")
- return
- _apply_host_transition(
- peer_id,
- NetworkHomeProtocolType.WORLD_SPACE,
- owner_fingerprint,
- _world_service.get_exterior_spawn_transform(owner_fingerprint),
- )
-
-
-func _spawn_peer_at_exterior(
- peer_id: int,
- owner_fingerprint: String,
-) -> void:
- if (
- not _session.is_host()
- or _session.get_peer_space(peer_id)
- != NetworkHomeProtocolType.WORLD_SPACE
- or _spawn_service.get_avatar(peer_id) == null
- ):
- return
- _apply_host_transition(
- peer_id,
- NetworkHomeProtocolType.WORLD_SPACE,
- owner_fingerprint,
- _world_service.get_exterior_spawn_transform(owner_fingerprint),
- )
-
-
-func _apply_host_transition(
- peer_id: int,
- space_id: StringName,
- owner_fingerprint: String,
- transform: Transform3D,
-) -> void:
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- var movement_sequence: int = 0
- if avatar != null:
- movement_sequence = int(
- avatar.make_network_snapshot(peer_id).get(
- "acknowledged_input", 0
- )
- )
- var transform_data: Array[float] = (
- NetworkHomeProtocolType.transform_to_array(transform)
- )
- if not NetworkHomeProtocolType.validate_transition_transform(
- transform_data
- ):
- push_error("Refused an out-of-bounds RV transition transform.")
- return
- var previous_space: StringName = _session.get_peer_space(peer_id)
- if (
- String(previous_space).begins_with("rv:")
- and previous_space != space_id
- ):
- _release_interior_slot(previous_space, peer_id)
- var result: Dictionary = {
- "peer_id": peer_id,
- "space_id": String(space_id),
- "owner_fingerprint": owner_fingerprint,
- "transform": transform_data,
- "movement_sequence": movement_sequence,
- }
- _apply_transition_result(result)
- for recipient_id: int in _session.get_authenticated_peer_ids():
- if recipient_id == 1:
- continue
- receive_home_transition.rpc_id(recipient_id, result)
-
-
-@rpc(
- "authority", "call_remote", "reliable",
- NetworkHomeProtocolType.RELIABLE_CHANNEL,
-)
-func receive_home_transition(data: Dictionary) -> void:
- if (
- typeof(data.get("peer_id")) != TYPE_INT
- or typeof(data.get("space_id")) not in [TYPE_STRING, TYPE_STRING_NAME]
- or typeof(data.get("movement_sequence", 0)) != TYPE_INT
- or int(data.get("movement_sequence", 0)) < 0
- or not NetworkIdentityCrypto.valid_fingerprint(
- data.get("owner_fingerprint")
- )
- or not NetworkHomeProtocolType.validate_transition_transform(
- data.get("transform")
- )
- ):
- return
- _apply_transition_result(data)
-
-
-func _apply_transition_result(data: Dictionary) -> void:
- var peer_id: int = int(data["peer_id"])
- var space_id := StringName(str(data["space_id"]))
- if not NetworkHomeProtocolType.validate_transition_transform(
- data.get("transform")
- ):
- return
- var target_transform: Transform3D = (
- NetworkHomeProtocolType.array_to_transform(data["transform"])
- )
- if not _session.set_peer_space(peer_id, space_id):
- return
- if not _session.apply_authoritative_space_transition(
- peer_id,
- target_transform,
- int(data.get("movement_sequence", 0)),
- ):
- return
- if peer_id == _session.get_local_peer_id():
- _world_service.set_local_space(space_id)
- local_space_changed.emit(space_id, str(data["owner_fingerprint"]))
- local_transition_finished.emit(true, "")
- _refresh_peer_visibility()
-
-
-func _send_transition_rejection(peer_id: int, message: String) -> void:
- if peer_id == _session.get_local_peer_id():
- local_transition_finished.emit(false, message)
- else:
- receive_home_transition_rejection.rpc_id(peer_id, message)
-
-
-@rpc(
- "authority", "call_remote", "reliable",
- NetworkHomeProtocolType.RELIABLE_CHANNEL,
-)
-func receive_home_transition_rejection(message: String) -> void:
- local_transition_finished.emit(
- false, message.left(NetworkHomeProtocolType.MAX_MESSAGE_LENGTH)
- )
-
-
-func _refresh_peer_visibility() -> void:
- var local_peer_id: int = _session.get_local_peer_id()
- if local_peer_id <= 0:
- return
- for peer_id: int in _spawn_service.get_peer_ids():
- if peer_id == local_peer_id:
- continue
- _spawn_service.set_peer_presentation_visible(
- peer_id, _session.peers_share_space(local_peer_id, peer_id)
- )
-
-
-func _on_local_home_changed() -> void:
- if _suppress_local_submission or _publish_queued:
- return
- _publish_queued = true
- call_deferred("_publish_queued_home")
-
-
-func _publish_queued_home() -> void:
- if has_pending_local_decor_mutation():
- return
- _publish_queued = false
- if _save_manager != null:
- _save_manager.save_if_dirty()
- publish_local_home()
-
-
-func _flush_queued_local_home_publish() -> void:
- if not _publish_queued or has_pending_local_decor_mutation():
- return
- call_deferred("_publish_queued_home")
-
-
-func _on_join_authenticated() -> void:
- publish_local_home()
-
-
-func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
- if not _session.is_host():
- return
- for manifest: Dictionary in _manifests_by_fingerprint.values():
- receive_home_manifest.rpc_id(peer_id, manifest)
- for existing_peer_id: int in _session.get_authenticated_peer_ids():
- if existing_peer_id == peer_id:
- continue
- var avatar: Player = _spawn_service.get_avatar(existing_peer_id)
- if avatar == null:
- continue
- var space_id: StringName = _session.get_peer_space(existing_peer_id)
- var owner_fingerprint: String = (
- String(space_id).trim_prefix("rv:")
- if String(space_id).begins_with("rv:")
- else _fingerprint_for_peer(existing_peer_id)
- )
- if owner_fingerprint.is_empty():
- continue
- receive_home_transition.rpc_id(peer_id, {
- "peer_id": existing_peer_id,
- "space_id": String(space_id),
- "owner_fingerprint": owner_fingerprint,
- "transform": NetworkHomeProtocolType.transform_to_array(
- avatar.global_transform
- ),
- })
-
-
-func _on_peer_removed(peer_id: int) -> void:
- _release_peer_interior_slots(peer_id)
- var fingerprint: String = _fingerprint_by_peer.get(peer_id, "")
- _fingerprint_by_peer.erase(peer_id)
- _authorized_owned_counts.erase(peer_id)
- if fingerprint.is_empty():
- return
- if _session.is_host():
- for occupant_id: int in _session.get_authenticated_peer_ids():
- if (
- occupant_id == peer_id
- or _session.get_peer_space(occupant_id)
- != NetworkHomeProtocolType.make_space_id(fingerprint)
- ):
- continue
- _apply_host_transition(
- occupant_id,
- NetworkHomeProtocolType.WORLD_SPACE,
- fingerprint,
- _world_service.get_exterior_spawn_transform(fingerprint),
- )
- _manifests_by_fingerprint.erase(fingerprint)
- _world_service.remove_presentation(fingerprint)
- if _session.is_host():
- for recipient_id: int in _session.get_authenticated_peer_ids():
- if recipient_id != 1:
- receive_home_removed.rpc_id(recipient_id, fingerprint)
-
-
-@rpc(
- "authority", "call_remote", "reliable",
- NetworkHomeProtocolType.RELIABLE_CHANNEL,
-)
-func receive_home_removed(owner_fingerprint: String) -> void:
- if not NetworkIdentityCrypto.valid_fingerprint(owner_fingerprint):
- return
- _manifests_by_fingerprint.erase(owner_fingerprint)
- _world_service.remove_presentation(owner_fingerprint)
-
-
-func _on_session_state_changed(state: NetworkSession.State) -> void:
- if state in [NetworkSession.State.PRIVATE_HOST, NetworkSession.State.OPEN_HOST]:
- if (
- not _session.is_dedicated_host()
- and _world_service != null
- and _world_service.is_world_ready()
- ):
- publish_local_home()
- return
- if state in [
- NetworkSession.State.INACTIVE,
- NetworkSession.State.DISCONNECTING,
- NetworkSession.State.CONNECTION_FAILED,
- NetworkSession.State.SERVER_LOST,
- ]:
- _manifests_by_fingerprint.clear()
- _fingerprint_by_peer.clear()
- _authorized_owned_counts.clear()
- _authorization_result_owner.clear()
- _publish_queued = false
- _last_submitted_local_home_revision = -1
- _pending_local_decor_transactions.clear()
- _interior_slots_by_space.clear()
- _next_interior_index = 0
- _world_service.clear_presentations()
- _world_service.set_local_space(NetworkHomeProtocolType.WORLD_SPACE)
- local_space_changed.emit(NetworkHomeProtocolType.WORLD_SPACE, "")
-
-
-func _fingerprint_for_peer(peer_id: int) -> String:
- if _fingerprint_by_peer.has(peer_id):
- return _fingerprint_by_peer[peer_id]
- var record := _session.get_peer_record(peer_id)
- return record.identity_fingerprint if record != null else ""
-
-
-func _reserve_interior_slot(space_id: StringName, peer_id: int) -> int:
- var slots: Dictionary = _interior_slots_by_space.get(space_id, {})
- if slots.has(peer_id):
- return int(slots[peer_id])
- var used_slots: Dictionary[int, bool] = {}
- for value: Variant in slots.values():
- used_slots[int(value)] = true
- var slot: int = 0
- while used_slots.has(slot):
- slot += 1
- slots[peer_id] = slot
- _interior_slots_by_space[space_id] = slots
- return slot
-
-
-func _release_interior_slot(space_id: StringName, peer_id: int) -> void:
- var slots: Dictionary = _interior_slots_by_space.get(space_id, {})
- if slots.is_empty():
- return
- slots.erase(peer_id)
- if slots.is_empty():
- _interior_slots_by_space.erase(space_id)
- else:
- _interior_slots_by_space[space_id] = slots
-
-
-func _release_peer_interior_slots(peer_id: int) -> void:
- for space_id: StringName in _interior_slots_by_space.keys():
- _release_interior_slot(space_id, peer_id)
-
-
-func _new_id(prefix: String) -> String:
- return "%s:%s" % [
- prefix, Crypto.new().generate_random_bytes(16).hex_encode(),
- ]
diff --git a/network/network_home_service.gd.uid b/network/network_home_service.gd.uid
deleted file mode 100644
index be233ed..0000000
--- a/network/network_home_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ong8kug36ku7
diff --git a/network/network_identity_crypto.gd b/network/network_identity_crypto.gd
deleted file mode 100644
index 9d4f09c..0000000
--- a/network/network_identity_crypto.gd
+++ /dev/null
@@ -1,184 +0,0 @@
-class_name NetworkIdentityCrypto
-extends RefCounted
-
-const ALGORITHM: String = "RSA-3072-SHA256"
-const FINGERPRINT_LENGTH: int = 64
-const MAX_PUBLIC_KEY_BYTES: int = 8192
-const MAX_SIGNATURE_BYTES: int = 1024
-const DOMAIN_PREFIX: String = "straywild"
-const LEGACY_DOMAIN_PREFIX: String = "NETFISHING"
-const IDENTITY_VERSION: String = "identity_v1"
-
-
-static func normalize_public_pem(value: String) -> String:
- var normalized := value.replace("\r\n", "\n").replace("\r", "\n").strip_edges()
- return normalized + "\n" if not normalized.is_empty() else ""
-
-
-static func fingerprint_public_pem(value: String) -> String:
- var normalized := normalize_public_pem(value)
- return normalized.sha256_text() if not normalized.is_empty() else ""
-
-
-static func valid_fingerprint(value: Variant) -> bool:
- if typeof(value) != TYPE_STRING:
- return false
- var text := str(value)
- if text.length() != FINGERPRINT_LENGTH:
- return false
- for character: String in text:
- if character not in "0123456789abcdef":
- return false
- return true
-
-
-static func format_fingerprint(value: String, groups: int = 5) -> String:
- if not valid_fingerprint(value):
- return "Unavailable"
- var visible := value.left(clampi(groups, 1, 8) * 4).to_upper()
- var parts := PackedStringArray()
- for offset: int in range(0, visible.length(), 4):
- parts.append(visible.substr(offset, 4))
- return "-".join(parts)
-
-
-static func compact_suffix(value: String) -> String:
- return value.right(6).to_upper() if valid_fingerprint(value) else "??????"
-
-
-static func secure_id(byte_count: int = 16) -> String:
- return Crypto.new().generate_random_bytes(byte_count).hex_encode()
-
-
-static func canonical_bytes(domain: String, fields: Array) -> PackedByteArray:
- return canonical_bytes_for_prefix(DOMAIN_PREFIX, domain, fields)
-
-
-static func canonical_bytes_for_prefix(
- prefix: String,
- domain: String,
- fields: Array,
-) -> PackedByteArray:
- var output := PackedByteArray()
- _append_string(output, prefix)
- _append_string(output, IDENTITY_VERSION)
- _append_string(output, domain)
- for value: Variant in fields:
- match typeof(value):
- TYPE_STRING, TYPE_STRING_NAME:
- output.append(1)
- _append_string(output, str(value))
- TYPE_INT:
- output.append(2)
- _append_i64(output, int(value))
- TYPE_BOOL:
- output.append(3)
- output.append(1 if bool(value) else 0)
- TYPE_PACKED_BYTE_ARRAY:
- output.append(4)
- _append_bytes(output, value)
- _:
- return PackedByteArray()
- return output
-
-
-static func digest(domain: String, fields: Array) -> PackedByteArray:
- var bytes := canonical_bytes(domain, fields)
- if bytes.is_empty():
- return PackedByteArray()
- var context := HashingContext.new()
- if context.start(HashingContext.HASH_SHA256) != OK:
- return PackedByteArray()
- context.update(bytes)
- return context.finish()
-
-
-static func sign_fields(
- private_key: CryptoKey, domain: String, fields: Array
-) -> PackedByteArray:
- if private_key == null:
- return PackedByteArray()
- var value := digest(domain, fields)
- return (
- Crypto.new().sign(HashingContext.HASH_SHA256, value, private_key)
- if not value.is_empty() else PackedByteArray()
- )
-
-
-static func verify_fields(
- public_key: CryptoKey,
- domain: String,
- fields: Array,
- signature: PackedByteArray,
-) -> bool:
- if (
- public_key == null
- or signature.is_empty()
- or signature.size() > MAX_SIGNATURE_BYTES
- ):
- return false
- var value := digest(domain, fields)
- return (
- not value.is_empty()
- and Crypto.new().verify(
- HashingContext.HASH_SHA256, value, signature, public_key
- )
- )
-
-
-static func verify_legacy_fields(
- public_key: CryptoKey,
- domain: String,
- fields: Array,
- signature: PackedByteArray,
-) -> bool:
- if (
- public_key == null
- or signature.is_empty()
- or signature.size() > MAX_SIGNATURE_BYTES
- ):
- return false
- var bytes: PackedByteArray = canonical_bytes_for_prefix(
- LEGACY_DOMAIN_PREFIX,
- domain,
- fields,
- )
- if bytes.is_empty():
- return false
- var context := HashingContext.new()
- if context.start(HashingContext.HASH_SHA256) != OK:
- return false
- context.update(bytes)
- return Crypto.new().verify(
- HashingContext.HASH_SHA256,
- context.finish(),
- signature,
- public_key,
- )
-
-
-static func load_public_key(public_pem: String) -> CryptoKey:
- var normalized := normalize_public_pem(public_pem)
- if normalized.is_empty() or normalized.to_utf8_buffer().size() > MAX_PUBLIC_KEY_BYTES:
- return null
- var key := CryptoKey.new()
- return key if key.load_from_string(normalized, true) == OK else null
-
-
-static func _append_string(output: PackedByteArray, value: String) -> void:
- _append_bytes(output, value.to_utf8_buffer())
-
-
-static func _append_bytes(output: PackedByteArray, value: PackedByteArray) -> void:
- _append_u32(output, value.size())
- output.append_array(value)
-
-
-static func _append_u32(output: PackedByteArray, value: int) -> void:
- for shift: int in [24, 16, 8, 0]:
- output.append((value >> shift) & 0xff)
-
-
-static func _append_i64(output: PackedByteArray, value: int) -> void:
- for shift: int in [56, 48, 40, 32, 24, 16, 8, 0]:
- output.append((value >> shift) & 0xff)
diff --git a/network/network_identity_crypto.gd.uid b/network/network_identity_crypto.gd.uid
deleted file mode 100644
index e11459e..0000000
--- a/network/network_identity_crypto.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bjb5n6ayqkb3q
diff --git a/network/network_item_protocol.gd b/network/network_item_protocol.gd
deleted file mode 100644
index 7ec5b7b..0000000
--- a/network/network_item_protocol.gd
+++ /dev/null
@@ -1,83 +0,0 @@
-class_name NetworkItemProtocol
-extends RefCounted
-
-const ITEM_USE_CAPABILITY: StringName = &"item_use_v1"
-const EQUIPMENT_CAPABILITY: StringName = &"equipment_v1"
-const RELIABLE_CHANNEL: int = NetworkProtocol.ITEM_RELIABLE_CHANNEL
-const MAX_ID_LENGTH: int = 96
-const MAX_ITEM_ID_LENGTH: int = 96
-const MAX_MESSAGE_LENGTH: int = 160
-
-
-static func validate_use_request(data: Variant) -> String:
- if typeof(data) != TYPE_DICTIONARY:
- return "Item use could not be completed."
- var value: Dictionary = data
- for key: String in [
- "request_id", "session_id", "item_id", "quantity",
- ]:
- if not value.has(key):
- return "Item use could not be completed."
- if (
- not _valid_id(value["request_id"])
- or not _valid_id(value["session_id"])
- or typeof(value["item_id"]) not in [TYPE_STRING, TYPE_STRING_NAME]
- or str(value["item_id"]).is_empty()
- or str(value["item_id"]).length() > MAX_ITEM_ID_LENGTH
- or typeof(value["quantity"]) != TYPE_INT
- or int(value["quantity"]) < 1
- or int(value["quantity"]) > 99
- ):
- return "Item use could not be completed."
- return ""
-
-
-static func validate_use_result(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- return (
- _valid_id(value.get("result_id"))
- and _valid_id(value.get("request_id"))
- and _valid_id(value.get("session_id"))
- and typeof(value.get("target_peer_id")) == TYPE_INT
- and typeof(value.get("accepted")) == TYPE_BOOL
- and typeof(value.get("item_id")) in [TYPE_STRING, TYPE_STRING_NAME]
- and not str(value["item_id"]).is_empty()
- and str(value["item_id"]).length() <= MAX_ITEM_ID_LENGTH
- and typeof(value.get("quantity")) == TYPE_INT
- and int(value["quantity"]) >= 0
- and int(value["quantity"]) <= 1
- and typeof(value.get("duration")) in [TYPE_FLOAT, TYPE_INT]
- and is_finite(float(value["duration"]))
- and float(value["duration"]) >= 0.0
- and typeof(value.get("message")) == TYPE_STRING
- and str(value["message"]).length() <= MAX_MESSAGE_LENGTH
- )
-
-
-static func validate_equipped_state(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- return (
- _valid_id(value.get("session_id"))
- and typeof(value.get("owner_peer_id")) == TYPE_INT
- and int(value["owner_peer_id"]) > 0
- and typeof(value.get("item_id")) in [TYPE_STRING, TYPE_STRING_NAME]
- and str(value["item_id"]).length() <= MAX_ITEM_ID_LENGTH
- and typeof(value.get("category")) == TYPE_INT
- and int(value["category"]) >= -1
- and int(value["category"]) < ItemData.Category.size()
- and typeof(value.get("revision")) == TYPE_INT
- and int(value["revision"]) >= 0
- and typeof(value.get("owns_item")) == TYPE_BOOL
- )
-
-
-static func _valid_id(value: Variant) -> bool:
- return (
- typeof(value) == TYPE_STRING
- and not str(value).is_empty()
- and str(value).length() <= MAX_ID_LENGTH
- )
diff --git a/network/network_item_protocol.gd.uid b/network/network_item_protocol.gd.uid
deleted file mode 100644
index 6d3a289..0000000
--- a/network/network_item_protocol.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bphibe6g81mhm
diff --git a/network/network_item_use_service.gd b/network/network_item_use_service.gd
deleted file mode 100644
index 024eca9..0000000
--- a/network/network_item_use_service.gd
+++ /dev/null
@@ -1,491 +0,0 @@
-class_name NetworkItemUseService
-extends Node
-
-const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
-
-const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
-const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
-const DecorCatalogType = preload("res://homes/decor_catalog.gd")
-
-const MAX_LEDGER_ENTRIES: int = 64
-
-signal local_item_use_pending(request_id: String)
-signal local_item_use_finished(accepted: bool, message: String)
-signal equipped_state_changed(peer_id: int, item_id: StringName, category: int)
-
-var _session: NetworkSession
-var _spawn_service: PlayerSpawnService
-var _catalog: ItemCatalog
-var _local_bag: PlayerBag
-var _local_effects: PlayerItemEffects
-var _save_manager: PlayerSaveManager
-var _requests: Dictionary[int, Dictionary] = {}
-var _pending_by_peer: Dictionary[int, String] = {}
-var _result_owners: Dictionary[String, int] = {}
-var _received_results: Dictionary[String, bool] = {}
-var _pending_local: Dictionary = {}
-var _equipped_states: Dictionary[int, Dictionary] = {}
-var _local_equipped_revision: int = 0
-var _reservations: PlayerAssetReservationService
-
-
-func setup(
- session: NetworkSession,
- spawn_service: PlayerSpawnService,
- catalog: ItemCatalog,
- local_bag: PlayerBag,
- local_effects: PlayerItemEffects,
- save_manager: PlayerSaveManager,
- reservations: PlayerAssetReservationService,
-) -> void:
- _session = session
- _spawn_service = spawn_service
- _catalog = catalog
- _local_bag = local_bag
- _local_effects = local_effects
- _save_manager = save_manager
- _reservations = reservations
- _session.peer_removed.connect(_on_peer_removed)
- _session.state_changed.connect(_on_session_state_changed)
- _session.peer_authenticated.connect(_on_peer_authenticated)
- _spawn_service.avatar_spawned.connect(_on_avatar_spawned)
-
-
-func request_use(item_id: StringName) -> String:
- if not _pending_local.is_empty():
- local_item_use_finished.emit(false, "An item use is already pending.")
- return ""
- var consumed_item_id: StringName = _consumed_item_id(item_id)
- if item_id == PlayerItemEffects.FISH_FINDER_ID and (
- _local_bag == null
- or not _local_bag.owns_item(item_id)
- or _local_bag.get_quantity(consumed_item_id) < 1
- ):
- local_item_use_finished.emit(
- false, PlayerItemEffects.FISH_FINDER_DEAD_MESSAGE
- )
- return ""
- if (
- _reservations != null
- and _reservations.get_available_item_quantity(consumed_item_id) < 1
- ):
- local_item_use_finished.emit(false, "Reserved in a letter.")
- return ""
- if (
- _session == null
- or not _session.is_gameplay_session_active()
- or (
- not _session.is_host()
- and not _session.supports_server_capability(
- NetworkItemProtocol.ITEM_USE_CAPABILITY
- )
- )
- ):
- local_item_use_finished.emit(false, "Item use is unavailable.")
- return ""
- var request_id := _new_id("item")
- var data := {
- "request_id": request_id,
- "session_id": _session.get_session_id(),
- "item_id": str(item_id),
- "quantity": _local_bag.get_quantity(consumed_item_id),
- }
- _pending_local = data.duplicate(true)
- local_item_use_pending.emit(request_id)
- if _session.is_host():
- _handle_use_request(_session.get_local_peer_id(), data)
- else:
- submit_item_use.rpc_id(1, data)
- return request_id
-
-
-@rpc("any_peer", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
-func submit_item_use(data: Dictionary) -> void:
- var sender_id := multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(sender_id):
- _handle_use_request(sender_id, data)
-
-
-func _handle_use_request(peer_id: int, data: Dictionary) -> void:
- var request_id := str(data.get("request_id", ""))
- var ledger: Dictionary = _requests.get(peer_id, {})
- if ledger.has(request_id):
- _send_result(peer_id, ledger[request_id])
- return
- var error := NetworkItemProtocol.validate_use_request(data)
- if (
- error.is_empty()
- and str(data["session_id"]) != _session.get_session_id()
- ):
- error = "Item use could not be completed."
- if error.is_empty() and _pending_by_peer.has(peer_id):
- error = "An item use is already pending."
- var item_id := StringName(str(data.get("item_id", "")))
- var item: ItemData = _catalog.get_item_by_id(item_id)
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- var duration: float = 0.0
- var is_fish_finder: bool = (
- item_id == PlayerItemEffects.FISH_FINDER_ID
- and item != null
- and item.category == ItemData.Category.TOOL
- and item.usable
- and item.equippable
- )
- if error.is_empty() and (
- item == null
- or not item.is_available()
- or (
- item.category != ItemData.Category.CONSUMABLE
- and not is_fish_finder
- )
- or not item.usable
- ):
- error = "That item cannot be used now."
- if error.is_empty() and int(data["quantity"]) < 1:
- error = (
- PlayerItemEffects.FISH_FINDER_DEAD_MESSAGE
- if is_fish_finder
- else "You do not have that item."
- )
- if error.is_empty() and (
- avatar == null
- or avatar.is_water_recovery_active()
- ):
- error = "That item cannot be used now."
- if error.is_empty():
- duration = avatar.item_effects.get_effect_duration(item_id)
- if duration <= 0.0:
- error = "That item cannot be used now."
- var result := {
- "result_id": _new_id("item_result"),
- "request_id": request_id if not request_id.is_empty() else "invalid",
- "session_id": _session.get_session_id(),
- "target_peer_id": peer_id,
- "accepted": error.is_empty(),
- "item_id": str(item_id) if not item_id.is_empty() else "invalid",
- "quantity": 1 if error.is_empty() else 0,
- "duration": duration if error.is_empty() else 0.0,
- "message": "Used %s." % item.display_name if error.is_empty() else error,
- }
- ledger[request_id] = result.duplicate(true)
- _bound(ledger)
- _requests[peer_id] = ledger
- _result_owners[result["result_id"]] = peer_id
- _pending_by_peer[peer_id] = request_id
- _send_result(peer_id, result)
-
-
-func _send_result(peer_id: int, result: Dictionary) -> void:
- if peer_id == _session.get_local_peer_id():
- _apply_result(result)
- else:
- receive_item_use_result.rpc_id(peer_id, result)
-
-
-@rpc("authority", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
-func receive_item_use_result(data: Dictionary) -> void:
- _apply_result(data)
-
-
-func _apply_result(data: Dictionary) -> void:
- if (
- not NetworkItemProtocol.validate_use_result(data)
- or str(data["session_id"]) != _session.get_session_id()
- or int(data["target_peer_id"]) != _session.get_local_peer_id()
- ):
- return
- var result_id: String = data["result_id"]
- if _received_results.has(result_id):
- _acknowledge(data, true, "")
- return
- if (
- _pending_local.is_empty()
- or str(data["request_id"]) != str(_pending_local.get("request_id", ""))
- ):
- return
- if not bool(data["accepted"]):
- _received_results[result_id] = true
- _pending_local.clear()
- local_item_use_finished.emit(false, str(data["message"]))
- _acknowledge(data, false, str(data["message"]))
- return
- var item_id := StringName(str(data["item_id"]))
- var consumed_item_id: StringName = _consumed_item_id(item_id)
- if (
- _reservations != null
- and _reservations.get_available_item_quantity(consumed_item_id) < 1
- ):
- _pending_local.clear()
- local_item_use_finished.emit(false, "Reserved in a letter.")
- _acknowledge(data, false, "Reserved in a letter.")
- return
- var bag_snapshot := _local_bag.get_all_items()
- if (
- _local_bag.get_quantity(consumed_item_id) < 1
- or not _local_bag.remove_item(consumed_item_id, 1)
- or not _save_manager.save_if_dirty()
- ):
- _local_bag.replace_all_items(bag_snapshot)
- _save_manager.save_if_dirty()
- _pending_local.clear()
- local_item_use_finished.emit(false, "Item use could not be completed.")
- _acknowledge(data, false, "Item use could not be completed.")
- return
- _received_results[result_id] = true
- _bound(_received_results)
- _pending_local.clear()
- local_item_use_finished.emit(true, str(data["message"]))
- _acknowledge(data, true, "")
-
-
-func _acknowledge(data: Dictionary, applied: bool, message: String) -> void:
- if _session.is_host():
- _handle_ack(
- _session.get_local_peer_id(),
- str(data["result_id"]),
- str(data["request_id"]),
- applied,
- message
- )
- else:
- acknowledge_item_use.rpc_id(
- 1, str(data["result_id"]), str(data["request_id"]), applied,
- message.left(NetworkItemProtocol.MAX_MESSAGE_LENGTH)
- )
-
-
-@rpc("any_peer", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
-func acknowledge_item_use(
- result_id: String,
- request_id: String,
- applied: bool,
- message: String,
-) -> void:
- var sender_id := multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(sender_id):
- _handle_ack(sender_id, result_id, request_id, applied, message)
-
-
-func _handle_ack(
- peer_id: int,
- result_id: String,
- request_id: String,
- applied: bool,
- _message: String,
-) -> void:
- if (
- _result_owners.get(result_id, 0) != peer_id
- or _pending_by_peer.get(peer_id, "") != request_id
- ):
- return
- _pending_by_peer.erase(peer_id)
- if not applied:
- return
- var result: Dictionary = _requests.get(peer_id, {}).get(request_id, {})
- var avatar := _spawn_service.get_avatar(peer_id)
- if avatar == null or result.is_empty():
- return
- var item_id := StringName(str(result["item_id"]))
- avatar.item_effects.activate_authoritative(item_id, float(result["duration"]))
- _broadcast_effect(peer_id, item_id, float(result["duration"]))
-
-
-func get_effects_for_peer(peer_id: int) -> PlayerItemEffects:
- var avatar := _spawn_service.get_avatar(peer_id)
- return avatar.item_effects if avatar != null else null
-
-
-func get_equipped_item_id(peer_id: int) -> StringName:
- var state: Dictionary = _equipped_states.get(peer_id, {})
- if state.is_empty() or not bool(state.get("owns_item", false)):
- return StringName()
- return StringName(str(state.get("item_id", "")))
-
-
-func submit_local_equipped(item_id: StringName, owns_item: bool) -> void:
- if _session == null or not _session.is_gameplay_session_active():
- return
- _local_equipped_revision += 1
- var item: ItemData = _catalog.get_item_by_id(item_id)
- var can_equip: bool = (
- item != null and item.is_available() and owns_item
- )
- var data := {
- "session_id": _session.get_session_id(),
- "owner_peer_id": _session.get_local_peer_id(),
- "item_id": str(item_id) if can_equip else "",
- "category": int(item.category) if can_equip else -1,
- "revision": _local_equipped_revision,
- "owns_item": can_equip,
- }
- if _session.is_host():
- _handle_equipped_state(_session.get_local_peer_id(), data)
- elif _session.supports_server_capability(
- NetworkItemProtocol.EQUIPMENT_CAPABILITY
- ):
- submit_equipped_state.rpc_id(1, data)
-
-
-@rpc("any_peer", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
-func submit_equipped_state(data: Dictionary) -> void:
- var sender_id := multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(sender_id):
- _handle_equipped_state(sender_id, data)
-
-
-func _handle_equipped_state(peer_id: int, data: Dictionary) -> void:
- if (
- not NetworkItemProtocol.validate_equipped_state(data)
- or str(data["session_id"]) != _session.get_session_id()
- or int(data["owner_peer_id"]) != peer_id
- ):
- return
- var previous: Dictionary = _equipped_states.get(peer_id, {})
- if int(data["revision"]) <= int(previous.get("revision", -1)):
- return
- var item_id := StringName(str(data["item_id"]))
- var item: ItemData = _catalog.get_item_by_id(item_id)
- if not item_id.is_empty() and (
- item == null
- or not item.is_available()
- or not bool(data["owns_item"])
- or int(item.category) != int(data["category"])
- ):
- return
- _equipped_states[peer_id] = data.duplicate(true)
- _apply_equipped(data)
- receive_equipped_state.rpc(data)
-
-
-@rpc("authority", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
-func receive_equipped_state(data: Dictionary) -> void:
- if NetworkItemProtocol.validate_equipped_state(data):
- _equipped_states[int(data["owner_peer_id"])] = data.duplicate(true)
- _apply_equipped(data)
-
-
-func _apply_equipped(data: Dictionary) -> void:
- var peer_id: int = data["owner_peer_id"]
- var avatar := _spawn_service.get_avatar(peer_id)
- if avatar != null:
- var item_id := StringName(str(data["item_id"]))
- var item: ItemData = _catalog.get_item_by_id(item_id)
- avatar.set_active_fishing_rod(
- (
- item as FishingRodDataType
- if (
- item != null
- and item.is_available()
- and int(data["category"]) == ItemData.Category.ROD
- and bool(data["owns_item"])
- )
- else null
- ),
- true,
- )
- avatar.set_active_art_kit(
- item.icon if item != null else null,
- item_id == ArtShopStockType.ART_KIT_ITEM_ID and bool(data["owns_item"]),
- )
- avatar.set_active_catching_net(
- item_id == FishingShopStockType.CRAB_NET_ID
- and bool(data["owns_item"]),
- )
- avatar.set_active_shovel(
- item_id == FishingShopStockType.STANDARD_SHOVEL_ID
- and bool(data["owns_item"]),
- )
- avatar.set_active_decor_package(
- DecorCatalogType.has_product(item_id)
- and bool(data["owns_item"]),
- )
- equipped_state_changed.emit(
- peer_id, StringName(str(data["item_id"])), int(data["category"])
- )
-
-
-func _broadcast_effect(peer_id: int, item_id: StringName, duration: float) -> void:
- var data := {
- "session_id": _session.get_session_id(),
- "owner_peer_id": peer_id,
- "item_id": str(item_id),
- "remaining": duration,
- }
- _apply_effect_snapshot(data)
- receive_effect_snapshot.rpc(data)
-
-
-@rpc("authority", "call_remote", "reliable", NetworkItemProtocol.RELIABLE_CHANNEL)
-func receive_effect_snapshot(data: Dictionary) -> void:
- _apply_effect_snapshot(data)
-
-
-func _apply_effect_snapshot(data: Dictionary) -> void:
- if (
- typeof(data.get("owner_peer_id")) != TYPE_INT
- or typeof(data.get("item_id")) != TYPE_STRING
- or typeof(data.get("remaining")) not in [TYPE_FLOAT, TYPE_INT]
- or str(data.get("session_id", "")) != _session.get_session_id()
- ):
- return
- var avatar := _spawn_service.get_avatar(int(data["owner_peer_id"]))
- if avatar != null:
- avatar.item_effects.activate_authoritative(
- StringName(data["item_id"]), float(data["remaining"])
- )
-
-
-func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
- if not _session.is_host():
- return
- for state: Dictionary in _equipped_states.values():
- receive_equipped_state.rpc_id(peer_id, state)
-
-
-func _on_avatar_spawned(peer_id: int, _avatar: Player) -> void:
- var state: Dictionary = _equipped_states.get(peer_id, {})
- if not state.is_empty():
- _apply_equipped(state)
-
-
-func _on_peer_removed(peer_id: int) -> void:
- _requests.erase(peer_id)
- _pending_by_peer.erase(peer_id)
- _equipped_states.erase(peer_id)
-
-
-func _on_session_state_changed(state: NetworkSession.State) -> void:
- if state in [
- NetworkSession.State.INACTIVE,
- NetworkSession.State.DISCONNECTING,
- NetworkSession.State.CONNECTION_FAILED,
- NetworkSession.State.SERVER_LOST,
- ]:
- var pending := not _pending_local.is_empty()
- _requests.clear()
- _pending_by_peer.clear()
- _result_owners.clear()
- _received_results.clear()
- _equipped_states.clear()
- _pending_local.clear()
- if pending:
- local_item_use_finished.emit(false, "Connection lost.")
-
-
-func _bound(values: Dictionary) -> void:
- while values.size() > MAX_LEDGER_ENTRIES:
- values.erase(values.keys().front())
-
-
-func _new_id(prefix: String) -> String:
- return "%s:%s" % [
- prefix, Crypto.new().generate_random_bytes(16).hex_encode(),
- ]
-
-
-func _consumed_item_id(item_id: StringName) -> StringName:
- return (
- PlayerItemEffects.BATTERIES_ID
- if item_id == PlayerItemEffects.FISH_FINDER_ID
- else item_id
- )
diff --git a/network/network_item_use_service.gd.uid b/network/network_item_use_service.gd.uid
deleted file mode 100644
index 7558e08..0000000
--- a/network/network_item_use_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dx21uu253nmpw
diff --git a/network/network_job_service.gd b/network/network_job_service.gd
deleted file mode 100644
index ebf5a6e..0000000
--- a/network/network_job_service.gd
+++ /dev/null
@@ -1,155 +0,0 @@
-class_name NetworkJobService
-extends Node
-
-const MAX_SESSION_ID_LENGTH: int = 96
-const BOARD_REQUEST_INTERVAL_SECONDS: float = 1.5
-
-var _session: NetworkSession
-var _jobs: PlayerJobService
-var _sequence: int = 0
-var _last_received_sequence: int = -1
-var _board_request_accumulator: float = 0.0
-
-
-func setup(session: NetworkSession, jobs: PlayerJobService) -> void:
- _session = session
- _jobs = jobs
- _session.state_changed.connect(_on_session_state_changed)
- _session.peer_authenticated.connect(_on_peer_authenticated)
- _jobs.board_changed.connect(_on_board_changed)
- set_process(true)
-
-
-func _process(delta: float) -> void:
- if (
- _session == null
- or _jobs == null
- or not _session.is_joined_client()
- or not _session.supports_server_capability(
- NetworkProtocol.JOBS_CAPABILITY
- )
- or _jobs.has_active_board()
- ):
- _board_request_accumulator = 0.0
- return
- _board_request_accumulator += delta
- if _board_request_accumulator < BOARD_REQUEST_INTERVAL_SECONDS:
- return
- _board_request_accumulator = 0.0
- _request_remote_board()
-
-
-func _on_session_state_changed(state: NetworkSession.State) -> void:
- _sequence = 0
- _last_received_sequence = -1
- _board_request_accumulator = 0.0
- if state == NetworkSession.State.JOINED_CLIENT:
- if _session.supports_server_capability(NetworkProtocol.JOBS_CAPABILITY):
- _request_remote_board()
- else:
- _jobs.clear_remote_board()
- elif state in [
- NetworkSession.State.INACTIVE,
- NetworkSession.State.DISCONNECTING,
- NetworkSession.State.CONNECTION_FAILED,
- NetworkSession.State.SERVER_LOST,
- ]:
- _jobs.clear_remote_board()
-
-
-func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
- if (
- _session == null
- or not _session.is_host()
- or not _session.peer_supports_capability(
- peer_id, NetworkProtocol.JOBS_CAPABILITY
- )
- ):
- return
- _send_board(peer_id)
-
-
-func _on_board_changed() -> void:
- if _session == null or not _session.is_host():
- return
- for peer_id: int in _session.get_authenticated_peer_ids():
- if (
- peer_id != _session.get_local_peer_id()
- and _session.peer_supports_capability(
- peer_id, NetworkProtocol.JOBS_CAPABILITY
- )
- ):
- _send_board(peer_id)
-
-
-func _send_board(peer_id: int) -> void:
- var board: Dictionary = _jobs.get_host_board_network_data()
- if board.is_empty() or not PlayerJobService.validate_board(board):
- return
- _sequence += 1
- receive_job_board.rpc_id(peer_id, {
- "session_id": _session.get_session_id(),
- "sequence": _sequence,
- "board": board,
- })
-
-
-func _request_remote_board() -> void:
- if (
- _session != null
- and _session.is_joined_client()
- and _session.supports_server_capability(
- NetworkProtocol.JOBS_CAPABILITY
- )
- ):
- request_job_board.rpc_id(1)
-
-
-@rpc("any_peer", "call_remote", "reliable", 0)
-func request_job_board() -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if (
- _session == null
- or not _session.is_host()
- or sender_id <= 1
- or not _session.is_authenticated_peer(sender_id)
- or not _session.peer_supports_capability(
- sender_id, NetworkProtocol.JOBS_CAPABILITY
- )
- ):
- return
- _send_board(sender_id)
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_job_board(data: Dictionary) -> void:
- if (
- _session == null
- or _jobs == null
- or not _session.is_joined_client()
- or not _session.supports_server_capability(NetworkProtocol.JOBS_CAPABILITY)
- or not validate_snapshot(data)
- or str(data.get("session_id", "")) != _session.get_session_id()
- ):
- return
- var sequence: int = int(data.get("sequence", -1))
- if sequence <= _last_received_sequence:
- return
- var board: Dictionary = data.get("board", {})
- if _jobs.apply_remote_board(board):
- _last_received_sequence = sequence
- _board_request_accumulator = 0.0
-
-
-static func validate_snapshot(value: Variant) -> bool:
- if typeof(value) != TYPE_DICTIONARY:
- return false
- var data: Dictionary = value
- return (
- typeof(data.get("session_id")) == TYPE_STRING
- and not str(data.get("session_id", "")).is_empty()
- and str(data.get("session_id", "")).length() <= MAX_SESSION_ID_LENGTH
- and typeof(data.get("sequence")) == TYPE_INT
- and int(data.get("sequence", -1)) >= 0
- and PlayerJobService.validate_board(data.get("board"))
- )
diff --git a/network/network_job_service.gd.uid b/network/network_job_service.gd.uid
deleted file mode 100644
index d50e8dc..0000000
--- a/network/network_job_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://drlyytr2o3uw8
diff --git a/network/network_mail_protocol.gd b/network/network_mail_protocol.gd
deleted file mode 100644
index f6441c7..0000000
--- a/network/network_mail_protocol.gd
+++ /dev/null
@@ -1,165 +0,0 @@
-class_name NetworkMailProtocol
-extends RefCounted
-
-const CAPABILITY: StringName = &"mail_v1"
-const RELIABLE_CHANNEL: int = NetworkProtocol.MAIL_RELIABLE_CHANNEL
-const MAX_ID_LENGTH: int = 96
-const MAX_BODY_CHARACTERS: int = 2000
-const MAX_BODY_BYTES: int = 8000
-const MAX_RECIPIENT_LETTERS: int = 100
-const MAX_SESSION_LETTERS: int = 200
-
-const GREETINGS: PackedStringArray = ["dear", "to", "hey", "greetings"]
-const SALUTATIONS: PackedStringArray = [
- "love", "from", "cheers", "salutations", "good_luck_have_fun",
-]
-
-enum State {
- SENT_UNREAD,
- READ,
- ACCEPTANCE_PENDING,
- ACCEPTED,
- DECLINED,
- ATTACHMENT_RECALLED,
- CANCELLED,
- SESSION_ENDED,
-}
-
-
-static func sanitize_body(value: Variant) -> String:
- if typeof(value) != TYPE_STRING:
- return ""
- var body := str(value).strip_edges(false, true)
- if (
- body.strip_edges().is_empty()
- or body.length() > MAX_BODY_CHARACTERS
- or body.to_utf8_buffer().size() > MAX_BODY_BYTES
- ):
- return ""
- for index: int in body.length():
- var codepoint := body.unicode_at(index)
- if codepoint < 32 and codepoint not in [9, 10, 13]:
- return ""
- if codepoint == 127:
- return ""
- return body.replace("\r\n", "\n").replace("\r", "\n").replace("\t", " ")
-
-
-static func valid_id(value: Variant) -> bool:
- return (
- typeof(value) == TYPE_STRING
- and not str(value).is_empty()
- and str(value).length() <= MAX_ID_LENGTH
- )
-
-
-static func validate_send_request(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- return (
- valid_id(value.get("request_id"))
- and valid_id(value.get("session_id"))
- and typeof(value.get("recipient_peer_id")) == TYPE_INT
- and int(value["recipient_peer_id"]) > 0
- and typeof(value.get("greeting_id")) == TYPE_STRING
- and str(value["greeting_id"]) in GREETINGS
- and not sanitize_body(value.get("body")).is_empty()
- and typeof(value.get("salutation_id")) == TYPE_STRING
- and str(value["salutation_id"]) in SALUTATIONS
- and typeof(value.get("reservation_id")) == TYPE_STRING
- and str(value["reservation_id"]).length() <= MAX_ID_LENGTH
- and typeof(value.get("attachment")) == TYPE_DICTIONARY
- and Dictionary(value["attachment"]).size() <= 3
- and NetworkIdentityCrypto.valid_fingerprint(
- value.get("sender_fingerprint")
- )
- and NetworkIdentityCrypto.valid_fingerprint(
- value.get("recipient_fingerprint")
- )
- and typeof(value.get("sender_signature")) == TYPE_PACKED_BYTE_ARRAY
- )
-
-
-static func validate_mail(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- return (
- valid_id(value.get("mail_id"))
- and valid_id(value.get("session_id"))
- and typeof(value.get("sequence")) == TYPE_INT
- and typeof(value.get("sender_peer_id")) == TYPE_INT
- and typeof(value.get("recipient_peer_id")) == TYPE_INT
- and typeof(value.get("sender_display_name")) == TYPE_STRING
- and str(value["sender_display_name"]).length() <= 24
- and str(value.get("greeting_id", "")) in GREETINGS
- and not sanitize_body(value.get("body")).is_empty()
- and str(value.get("salutation_id", "")) in SALUTATIONS
- and typeof(value.get("attachment")) == TYPE_DICTIONARY
- and Dictionary(value["attachment"]).size() <= 3
- and typeof(value.get("state")) == TYPE_INT
- and int(value["state"]) >= 0
- and int(value["state"]) < State.size()
- and NetworkIdentityCrypto.valid_fingerprint(
- value.get("sender_fingerprint")
- )
- and NetworkIdentityCrypto.valid_fingerprint(
- value.get("recipient_fingerprint")
- )
- and typeof(value.get("sender_signature")) == TYPE_PACKED_BYTE_ARRAY
- and typeof(value.get("sender_public_key")) == TYPE_STRING
- and str(value["sender_public_key"]).to_utf8_buffer().size()
- <= NetworkIdentityCrypto.MAX_PUBLIC_KEY_BYTES
- )
-
-
-static func mail_signature_fields(data: Dictionary) -> Array:
- var result: Array = [
- str(data.get("session_id", "")),
- str(data.get("request_id", "")),
- str(data.get("sender_fingerprint", "")),
- str(data.get("recipient_fingerprint", "")),
- str(data.get("greeting_id", "")),
- sanitize_body(data.get("body")),
- str(data.get("salutation_id", "")),
- str(data.get("reservation_id", "")),
- ]
- result.append_array(attachment_signature_fields(data.get("attachment", {})))
- return result
-
-
-static func attachment_signature_fields(value: Variant) -> Array:
- if typeof(value) != TYPE_DICTIONARY:
- return [0]
- var attachment: Dictionary = value
- var type := int(attachment.get("type", 0))
- var result: Array = [type]
- match type:
- 1:
- result.append(int(attachment.get("amount", 0)))
- 2:
- var fish: Dictionary = attachment.get("catch", {})
- result.append(str(attachment.get("catch_id", "")))
- result.append(str(fish.get("fish_id", "")))
- result.append(str(fish.get("weight_lb", "")))
- result.append(str(fish.get("display_scale", "")))
- result.append(int(fish.get("quality", -1)))
- result.append(int(fish.get("sale_value", 0)))
- result.append(bool(fish.get("is_favorited", false)))
- 3:
- result.append(str(attachment.get("item_id", "")))
- result.append(int(attachment.get("quantity", 0)))
- return result
-
-
-static func acceptance_signature_fields(
- data: Dictionary, sender_fingerprint: String, recipient_fingerprint: String
-) -> Array:
- return [
- str(data.get("session_id", "")),
- str(data.get("request_id", "")),
- str(data.get("mail_id", "")),
- sender_fingerprint,
- recipient_fingerprint,
- ]
diff --git a/network/network_mail_protocol.gd.uid b/network/network_mail_protocol.gd.uid
deleted file mode 100644
index 0359e82..0000000
--- a/network/network_mail_protocol.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://5ttl0pytgtf7
diff --git a/network/network_mail_service.gd b/network/network_mail_service.gd
deleted file mode 100644
index 675a5aa..0000000
--- a/network/network_mail_service.gd
+++ /dev/null
@@ -1,1123 +0,0 @@
-class_name NetworkMailService
-extends Node
-
-signal mailbox_changed
-signal unread_count_changed(count: int)
-signal operation_finished(success: bool, message: String)
-signal peers_changed
-
-const MAX_LEDGER: int = 256
-const FishQualityType = preload("res://fish/fish_quality.gd")
-
-var _session: NetworkSession
-var _reservations: PlayerAssetReservationService
-var _wallet: PlayerWallet
-var _inventory: FishInventory
-var _bag: PlayerBag
-var _collection_log: CollectionLog
-var _cooler_capacity: PlayerCoolerCapacity
-var _item_catalog: ItemCatalog
-var _fish_catalog: FishPool
-var _save_manager: PlayerSaveManager
-
-var _letters: Dictionary[String, Dictionary] = {}
-var _local_letters: Dictionary[String, Dictionary] = {}
-var _send_ledger: Dictionary[int, Dictionary] = {}
-var _action_ledger: Dictionary[String, bool] = {}
-var _sequence := 0
-var _pending_local_send: Dictionary = {}
-var _pending_transfers: Dictionary[String, Dictionary] = {}
-var _local_removal_snapshots: Dictionary[String, Dictionary] = {}
-var _received_awards: Dictionary[String, bool] = {}
-var _relationship_policy: NetworkPlayerListService
-var _archived_local_letters: Dictionary[String, bool] = {}
-var _deleted_local_letters: Dictionary[String, bool] = {}
-
-
-func setup(
- session: NetworkSession,
- reservations: PlayerAssetReservationService,
- wallet: PlayerWallet,
- inventory: FishInventory,
- bag: PlayerBag,
- collection_log: CollectionLog,
- cooler_capacity: PlayerCoolerCapacity,
- item_catalog: ItemCatalog,
- fish_catalog: FishPool,
- save_manager: PlayerSaveManager,
-) -> void:
- _session = session
- _reservations = reservations
- _wallet = wallet
- _inventory = inventory
- _bag = bag
- _collection_log = collection_log
- _cooler_capacity = cooler_capacity
- _item_catalog = item_catalog
- _fish_catalog = fish_catalog
- _save_manager = save_manager
- _session.peer_authenticated.connect(func(_id: int, _name: String) -> void:
- peers_changed.emit()
- )
- _session.peer_removed.connect(_on_peer_removed)
- _session.peer_display_name_changed.connect(
- func(_id: int, _name: String) -> void: peers_changed.emit()
- )
- _session.state_changed.connect(_on_session_state_changed)
-
-
-func set_relationship_policy(policy: NetworkPlayerListService) -> void:
- _relationship_policy = policy
-
-
-func refresh_relationship_filters() -> void:
- _emit_mailbox()
- peers_changed.emit()
-
-
-func get_local_letters(archived: bool = false) -> Array[Dictionary]:
- var values: Array[Dictionary] = []
- for letter: Dictionary in _local_letters.values():
- if (
- _session.get_local_peer_id() in [
- int(letter["sender_peer_id"]),
- int(letter["recipient_peer_id"]),
- ]
- and not _deleted_local_letters.has(str(letter["mail_id"]))
- and _archived_local_letters.has(str(letter["mail_id"])) == archived
- and not _letter_is_locally_blocked(letter)
- ):
- values.append(letter.duplicate(true))
- values.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
- var a_unread := int(a["state"]) == NetworkMailProtocol.State.SENT_UNREAD
- var b_unread := int(b["state"]) == NetworkMailProtocol.State.SENT_UNREAD
- return a_unread != b_unread if a_unread != b_unread else (
- int(a["sequence"]) > int(b["sequence"])
- )
- )
- return values
-
-
-func get_letter(mail_id: String) -> Dictionary:
- if _deleted_local_letters.has(mail_id):
- return {}
- return _local_letters.get(mail_id, {}).duplicate(true)
-
-
-func archive_local_letter(mail_id: String, archived: bool = true) -> bool:
- if not _local_letters.has(mail_id) or _deleted_local_letters.has(mail_id):
- return false
- if archived:
- _archived_local_letters[mail_id] = true
- else:
- _archived_local_letters.erase(mail_id)
- _emit_mailbox()
- return true
-
-
-func delete_local_letter(mail_id: String) -> bool:
- var letter: Dictionary = _local_letters.get(mail_id, {})
- if letter.is_empty() or _incoming_gift_is_unresolved(letter):
- return false
- _deleted_local_letters[mail_id] = true
- _archived_local_letters.erase(mail_id)
- _emit_mailbox()
- return true
-
-
-func is_letter_archived(mail_id: String) -> bool:
- return _archived_local_letters.has(mail_id)
-
-
-func get_unread_count() -> int:
- var count := 0
- for letter: Dictionary in _local_letters.values():
- if (
- int(letter["recipient_peer_id"]) == _session.get_local_peer_id()
- and int(letter["state"]) == NetworkMailProtocol.State.SENT_UNREAD
- and not _archived_local_letters.has(str(letter["mail_id"]))
- and not _deleted_local_letters.has(str(letter["mail_id"]))
- and not _letter_is_locally_blocked(letter)
- ):
- count += 1
- return count
-
-
-func is_local_recipient(letter: Dictionary) -> bool:
- return (
- int(letter.get("recipient_peer_id", 0))
- == _session.get_local_peer_id()
- )
-
-
-func get_local_display_name() -> String:
- var record := _session.get_peer_record(_session.get_local_peer_id())
- return record.display_name if record != null else "Player"
-
-
-func get_recipient_choices() -> Array[Dictionary]:
- var choices: Array[Dictionary] = []
- var local_id := _session.get_local_peer_id()
- for peer_id: int in _session.get_authenticated_peer_ids():
- if peer_id == local_id:
- continue
- var record := _session.get_peer_record(peer_id)
- if record != null and (
- _relationship_policy == null
- or not _relationship_policy.is_locally_blocked(
- record.identity_fingerprint
- )
- ):
- choices.append({
- "peer_id": peer_id,
- "name": record.display_name,
- "fingerprint": record.identity_fingerprint,
- })
- choices.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
- return str(a["name"]).naturalnocasecmp_to(str(b["name"])) < 0
- )
- var counts: Dictionary[String, int] = {}
- for choice: Dictionary in choices:
- var normalized := str(choice["name"]).to_lower()
- counts[normalized] = int(counts.get(normalized, 0)) + 1
- for choice: Dictionary in choices:
- var display_name: String = choice["name"]
- if int(counts.get(display_name.to_lower(), 0)) > 1:
- choice["label"] = "%s · %s" % [
- display_name,
- NetworkIdentityCrypto.compact_suffix(choice["fingerprint"]),
- ]
- else:
- choice["label"] = display_name
- return choices
-
-
-func send_letter(
- recipient_peer_id: int,
- greeting_id: String,
- body: String,
- salutation_id: String,
- attachment: Dictionary,
-) -> bool:
- if (
- not _pending_local_send.is_empty()
- or not _session.is_gameplay_session_active()
- or recipient_peer_id == _session.get_local_peer_id()
- or not _session.is_authenticated_peer(recipient_peer_id)
- or (
- _relationship_policy != null
- and _relationship_policy.is_locally_blocked(
- _session.get_peer_record(recipient_peer_id).identity_fingerprint
- )
- )
- or (
- not _session.is_host()
- and not _session.supports_server_capability(
- NetworkMailProtocol.CAPABILITY
- )
- )
- ):
- operation_finished.emit(false, "That player is no longer connected.")
- return false
- var request_id := _new_id("mail_request")
- var reservation_id := ""
- if int(attachment.get("type", 0)) != 0:
- reservation_id = _new_id("reservation")
- if not _reservations.reserve(reservation_id, attachment):
- operation_finished.emit(false, "That gift is not available.")
- return false
- var request := {
- "request_id": request_id,
- "session_id": _session.get_session_id(),
- "recipient_peer_id": recipient_peer_id,
- "greeting_id": greeting_id,
- "body": NetworkMailProtocol.sanitize_body(body),
- "salutation_id": salutation_id,
- "reservation_id": reservation_id,
- "attachment": attachment.duplicate(true),
- "sender_fingerprint": _session.get_local_identity_fingerprint(),
- "recipient_fingerprint": (
- _session.get_peer_record(recipient_peer_id).identity_fingerprint
- ),
- }
- request["sender_signature"] = _session.sign_local_action(
- "mail_send", NetworkMailProtocol.mail_signature_fields(request)
- )
- if not NetworkMailProtocol.validate_send_request(request):
- if not reservation_id.is_empty():
- _reservations.release(reservation_id)
- operation_finished.emit(false, "Letter could not be sent.")
- return false
- _pending_local_send = request.duplicate(true)
- if _session.is_host():
- _handle_send(_session.get_local_peer_id(), request)
- else:
- submit_mail.rpc_id(1, request)
- return true
-
-
-@rpc("any_peer", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
-func submit_mail(data: Dictionary) -> void:
- var sender := multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(sender):
- _handle_send(sender, data)
-
-
-func _handle_send(sender: int, data: Dictionary) -> void:
- var request_id := str(data.get("request_id", ""))
- var ledger: Dictionary = _send_ledger.get(sender, {})
- if ledger.has(request_id):
- _deliver_send_result(sender, ledger[request_id])
- return
- var error := ""
- if (
- not NetworkMailProtocol.validate_send_request(data)
- or str(data.get("session_id", "")) != _session.get_session_id()
- ):
- error = "Letter could not be sent."
- var recipient := int(data.get("recipient_peer_id", 0))
- if error.is_empty() and (
- recipient == sender or not _session.is_authenticated_peer(recipient)
- ):
- error = "That player is no longer connected."
- var sender_record := _session.get_peer_record(sender)
- var recipient_record := _session.get_peer_record(recipient)
- if error.is_empty() and (
- sender_record == null
- or recipient_record == null
- or sender_record.identity_fingerprint
- != str(data.get("sender_fingerprint", ""))
- or recipient_record.identity_fingerprint
- != str(data.get("recipient_fingerprint", ""))
- or not _session.verify_peer_action(
- sender,
- "mail_send",
- NetworkMailProtocol.mail_signature_fields(data),
- data.get("sender_signature", PackedByteArray()),
- )
- ):
- error = "Letter identity could not be verified."
- var silently_dropped := (
- error.is_empty()
- and _relationship_policy != null
- and _relationship_policy.pair_is_blocked(
- sender_record.identity_fingerprint,
- recipient_record.identity_fingerprint,
- )
- )
- if (
- error.is_empty()
- and not silently_dropped
- and _letters.size() >= NetworkMailProtocol.MAX_SESSION_LETTERS
- ):
- error = "The session mailbox is full."
- var recipient_count := 0
- for letter: Dictionary in _letters.values():
- if int(letter["recipient_peer_id"]) == recipient:
- recipient_count += 1
- if (
- error.is_empty()
- and not silently_dropped
- and recipient_count >= NetworkMailProtocol.MAX_RECIPIENT_LETTERS
- ):
- error = "That inbox is full."
- var attachment: Dictionary = data.get("attachment", {})
- if error.is_empty() and not _validate_attachment_structure(attachment):
- error = "That gift is not valid."
- var result := {
- "request_id": request_id if not request_id.is_empty() else "invalid",
- "accepted": error.is_empty(),
- "message": "Letter sent." if error.is_empty() else error,
- "reservation_id": str(data.get("reservation_id", "")),
- "silent_drop": silently_dropped,
- }
- if error.is_empty() and silently_dropped:
- _sequence += 1
- var sender_copy := _build_letter(
- data, sender, recipient, sender_record, recipient_record, _sequence
- )
- result["mail"] = sender_copy
- _deliver_private_letter(sender, sender_copy)
- _request_release(sender, str(data.get("reservation_id", "")))
- elif error.is_empty():
- _sequence += 1
- var letter := _build_letter(
- data, sender, recipient, sender_record, recipient_record, _sequence
- )
- _letters[letter["mail_id"]] = letter
- result["mail"] = letter
- _deliver_private_letter(sender, letter)
- _deliver_private_letter(recipient, letter)
- ledger[result["request_id"]] = result.duplicate(true)
- _bound(ledger)
- _send_ledger[sender] = ledger
- _deliver_send_result(sender, result)
-
-
-func _build_letter(
- data: Dictionary,
- sender: int,
- recipient: int,
- sender_record: PeerRegistry.PeerRecord,
- recipient_record: PeerRegistry.PeerRecord,
- sequence: int,
-) -> Dictionary:
- return {
- "mail_id": _new_id("mail"),
- "session_id": _session.get_session_id(),
- "sequence": sequence,
- "sender_peer_id": sender,
- "sender_display_name": sender_record.display_name,
- "recipient_peer_id": recipient,
- "recipient_display_name": recipient_record.display_name,
- "greeting_id": str(data["greeting_id"]),
- "body": NetworkMailProtocol.sanitize_body(data["body"]),
- "salutation_id": str(data["salutation_id"]),
- "reservation_id": str(data["reservation_id"]),
- "attachment": (data.get("attachment", {}) as Dictionary).duplicate(true),
- "state": NetworkMailProtocol.State.SENT_UNREAD,
- "request_id": str(data["request_id"]),
- "sender_fingerprint": data["sender_fingerprint"],
- "recipient_fingerprint": data["recipient_fingerprint"],
- "sender_signature": data["sender_signature"],
- "sender_public_key": sender_record.identity_public_key,
- }
-
-
-func _deliver_private_letter(peer_id: int, letter: Dictionary) -> void:
- if peer_id == _session.get_local_peer_id():
- _receive_letter(letter)
- elif _session.is_authenticated_peer(peer_id):
- receive_private_letter.rpc_id(peer_id, letter)
-
-
-@rpc("authority", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
-func receive_private_letter(letter: Dictionary) -> void:
- _receive_letter(letter)
-
-
-func _receive_letter(letter: Dictionary) -> void:
- if (
- not NetworkMailProtocol.validate_mail(letter)
- or str(letter["session_id"]) != _session.get_session_id()
- or _session.get_local_peer_id() not in [
- int(letter["sender_peer_id"]), int(letter["recipient_peer_id"]),
- ]
- ):
- return
- var sender_public := NetworkIdentityCrypto.normalize_public_pem(
- str(letter.get("sender_public_key", ""))
- )
- if (
- not _session.matches_authenticated_session_identity(
- str(letter["sender_fingerprint"]), sender_public
- )
- or not NetworkIdentityCrypto.verify_fields(
- NetworkIdentityCrypto.load_public_key(sender_public),
- "mail_send",
- NetworkMailProtocol.mail_signature_fields(letter),
- letter["sender_signature"],
- )
- ):
- return
- _local_letters[letter["mail_id"]] = letter.duplicate(true)
- _emit_mailbox()
-
-
-func cancel_unaccepted_between(first: String, second: String) -> void:
- if not _session.is_host():
- return
- for letter: Dictionary in _letters.values():
- if not (
- str(letter.get("sender_fingerprint", "")) in [first, second]
- and str(letter.get("recipient_fingerprint", "")) in [first, second]
- ):
- continue
- if int(letter.get("state", -1)) not in [
- NetworkMailProtocol.State.SENT_UNREAD,
- NetworkMailProtocol.State.READ,
- ]:
- continue
- letter["state"] = NetworkMailProtocol.State.CANCELLED
- _request_release(
- int(letter["sender_peer_id"]), str(letter.get("reservation_id", ""))
- )
- _update_participants(letter)
-
-
-func _deliver_send_result(peer_id: int, result: Dictionary) -> void:
- if peer_id == _session.get_local_peer_id():
- _receive_send_result(result)
- else:
- receive_send_result.rpc_id(peer_id, result)
-
-
-@rpc("authority", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
-func receive_send_result(result: Dictionary) -> void:
- _receive_send_result(result)
-
-
-func _receive_send_result(result: Dictionary) -> void:
- if (
- _pending_local_send.is_empty()
- or str(result.get("request_id", ""))
- != str(_pending_local_send.get("request_id", ""))
- ):
- return
- if (
- not bool(result.get("accepted", false))
- or bool(result.get("silent_drop", false))
- ):
- var reservation_id := str(_pending_local_send.get("reservation_id", ""))
- if not reservation_id.is_empty():
- _reservations.release(reservation_id)
- _pending_local_send.clear()
- operation_finished.emit(
- bool(result.get("accepted", false)),
- str(result.get("message", "Letter could not be sent."))
- )
-
-
-func mark_read(mail_id: String) -> void:
- var request := _action_request(mail_id)
- if request.is_empty():
- return
- if _session.is_host():
- _handle_read(_session.get_local_peer_id(), request)
- else:
- submit_read.rpc_id(1, request)
-
-
-@rpc("any_peer", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
-func submit_read(data: Dictionary) -> void:
- var sender := multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(sender):
- _handle_read(sender, data)
-
-
-func _handle_read(peer_id: int, data: Dictionary) -> void:
- var action_id := str(data.get("request_id", ""))
- if _action_ledger.has(action_id):
- return
- var letter: Dictionary = _letters.get(str(data.get("mail_id", "")), {})
- if (
- letter.is_empty()
- or int(letter["recipient_peer_id"]) != peer_id
- or str(data.get("session_id", "")) != _session.get_session_id()
- ):
- return
- _action_ledger[action_id] = true
- if int(letter["state"]) == NetworkMailProtocol.State.SENT_UNREAD:
- letter["state"] = NetworkMailProtocol.State.READ
- _update_participants(letter)
-
-
-func accept_gift(mail_id: String) -> void:
- var request := _action_request(mail_id)
- if request.is_empty():
- return
- var letter: Dictionary = _local_letters.get(mail_id, {})
- request["recipient_signature"] = _session.sign_local_action(
- "mail_accept",
- NetworkMailProtocol.acceptance_signature_fields(
- request,
- str(letter.get("sender_fingerprint", "")),
- str(letter.get("recipient_fingerprint", "")),
- ),
- )
- if _session.is_host():
- _handle_accept(_session.get_local_peer_id(), request)
- else:
- submit_accept.rpc_id(1, request)
-
-
-@rpc("any_peer", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
-func submit_accept(data: Dictionary) -> void:
- var sender := multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(sender):
- _handle_accept(sender, data)
-
-
-func _handle_accept(peer_id: int, data: Dictionary) -> void:
- var action_id := str(data.get("request_id", ""))
- var letter: Dictionary = _letters.get(str(data.get("mail_id", "")), {})
- if _action_ledger.has(action_id):
- return
- if (
- letter.is_empty()
- or int(letter["recipient_peer_id"]) != peer_id
- or int(letter["state"]) not in [
- NetworkMailProtocol.State.SENT_UNREAD,
- NetworkMailProtocol.State.READ,
- ]
- or int(letter["attachment"].get("type", 0)) == 0
- or not _session.is_authenticated_peer(int(letter["sender_peer_id"]))
- ):
- return
- var recipient_record := _session.get_peer_record(peer_id)
- if (
- recipient_record == null
- or recipient_record.identity_fingerprint
- != str(letter.get("recipient_fingerprint", ""))
- or not _session.verify_peer_action(
- peer_id,
- "mail_accept",
- NetworkMailProtocol.acceptance_signature_fields(
- data,
- str(letter.get("sender_fingerprint", "")),
- str(letter.get("recipient_fingerprint", "")),
- ),
- data.get("recipient_signature", PackedByteArray()),
- )
- ):
- return
- _action_ledger[action_id] = true
- var transfer_id := _new_id("mail_transfer")
- letter["state"] = NetworkMailProtocol.State.ACCEPTANCE_PENDING
- letter["transfer_id"] = transfer_id
- _pending_transfers[transfer_id] = {
- "letter": letter,
- "recipient_prepared": false,
- "sender_removed": false,
- }
- _update_participants(letter)
- _request_prepare(int(letter["recipient_peer_id"]), transfer_id, letter)
-
-
-func _request_prepare(peer_id: int, transfer_id: String, letter: Dictionary) -> void:
- if peer_id == _session.get_local_peer_id():
- _prepare_recipient(transfer_id, letter)
- else:
- prepare_recipient.rpc_id(peer_id, transfer_id, letter)
-
-
-@rpc("authority", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
-func prepare_recipient(transfer_id: String, letter: Dictionary) -> void:
- _prepare_recipient(transfer_id, letter)
-
-
-func _prepare_recipient(transfer_id: String, letter: Dictionary) -> void:
- var recipient_ready: bool = _can_receive(letter["attachment"])
- _send_phase_ack("recipient_prepared", transfer_id, recipient_ready)
-
-
-func _request_sender_commit(peer_id: int, transfer_id: String, letter: Dictionary) -> void:
- if peer_id == _session.get_local_peer_id():
- _commit_sender(transfer_id, letter)
- else:
- commit_sender.rpc_id(peer_id, transfer_id, letter)
-
-
-@rpc("authority", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
-func commit_sender(transfer_id: String, letter: Dictionary) -> void:
- _commit_sender(transfer_id, letter)
-
-
-func _commit_sender(transfer_id: String, letter: Dictionary) -> void:
- var reservation_id: String = letter["reservation_id"]
- var snapshot := _capture_assets()
- var applied := (
- _reservations.has_reservation(reservation_id)
- and _reservations.commit_removal(reservation_id, transfer_id)
- and _save_manager.save_if_dirty()
- )
- if not applied:
- _restore_assets(snapshot)
- _save_manager.save_if_dirty()
- else:
- _local_removal_snapshots[transfer_id] = snapshot
- _send_phase_ack("sender_removed", transfer_id, applied)
-
-
-func _request_recipient_award(
- peer_id: int, transfer_id: String, letter: Dictionary
-) -> void:
- if peer_id == _session.get_local_peer_id():
- _award_recipient(transfer_id, letter)
- else:
- award_recipient.rpc_id(peer_id, transfer_id, letter)
-
-
-@rpc("authority", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
-func award_recipient(transfer_id: String, letter: Dictionary) -> void:
- _award_recipient(transfer_id, letter)
-
-
-func _award_recipient(transfer_id: String, letter: Dictionary) -> void:
- if _received_awards.has(transfer_id):
- _send_phase_ack("recipient_awarded", transfer_id, true)
- return
- var snapshot := _capture_assets()
- var applied := _apply_award(letter["attachment"])
- if applied:
- applied = _save_manager.save_if_dirty()
- if not applied:
- _restore_assets(snapshot)
- _save_manager.save_if_dirty()
- else:
- _received_awards[transfer_id] = true
- _bound(_received_awards)
- _send_phase_ack("recipient_awarded", transfer_id, applied)
-
-
-func _send_phase_ack(phase: String, transfer_id: String, applied: bool) -> void:
- var letter := _find_local_transfer_letter(transfer_id)
- var fields := _gift_phase_fields(phase, transfer_id, applied, letter)
- var fingerprint := _session.get_local_identity_fingerprint()
- var signature := _session.sign_local_action(
- "gift_%s" % phase, fields
- )
- if _session.is_host():
- _handle_phase_ack(
- _session.get_local_peer_id(),
- phase,
- transfer_id,
- applied,
- fingerprint,
- signature,
- )
- else:
- acknowledge_transfer_phase.rpc_id(
- 1, phase, transfer_id, applied, fingerprint, signature
- )
-
-
-@rpc("any_peer", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
-func acknowledge_transfer_phase(
- phase: String,
- transfer_id: String,
- applied: bool,
- fingerprint: String,
- signature: PackedByteArray,
-) -> void:
- var sender := multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(sender):
- _handle_phase_ack(
- sender, phase, transfer_id, applied, fingerprint, signature
- )
-
-
-func _handle_phase_ack(
- peer_id: int,
- phase: String,
- transfer_id: String,
- applied: bool,
- fingerprint: String,
- signature: PackedByteArray,
-) -> void:
- var transfer: Dictionary = _pending_transfers.get(transfer_id, {})
- if transfer.is_empty():
- return
- var letter: Dictionary = transfer["letter"]
- var record := _session.get_peer_record(peer_id)
- if (
- record == null
- or record.identity_fingerprint != fingerprint
- or not _session.verify_peer_action(
- peer_id,
- "gift_%s" % phase,
- _gift_phase_fields(phase, transfer_id, applied, letter),
- signature,
- )
- ):
- return
- if phase == "recipient_prepared":
- if peer_id != int(letter["recipient_peer_id"]):
- return
- if not applied:
- _finish_transfer(transfer_id, false, "Gift could not be transferred.")
- return
- transfer["recipient_prepared"] = true
- _request_sender_commit(int(letter["sender_peer_id"]), transfer_id, letter)
- elif phase == "sender_removed":
- if peer_id != int(letter["sender_peer_id"]):
- return
- if not applied:
- _finish_transfer(transfer_id, false, "Gift returned to sender.")
- return
- transfer["sender_removed"] = true
- _request_recipient_award(
- int(letter["recipient_peer_id"]), transfer_id, letter
- )
- elif phase == "recipient_awarded":
- if peer_id != int(letter["recipient_peer_id"]):
- return
- if applied:
- _finish_transfer(transfer_id, true, "Gift accepted.")
- else:
- _request_sender_rollback(
- int(letter["sender_peer_id"]), transfer_id
- )
- _finish_transfer(transfer_id, false, "Gift could not be transferred.")
-
-
-func _find_local_transfer_letter(transfer_id: String) -> Dictionary:
- for value: Dictionary in _local_letters.values():
- if str(value.get("transfer_id", "")) == transfer_id:
- return value
- return {}
-
-
-func _gift_phase_fields(
- phase: String,
- transfer_id: String,
- applied: bool,
- letter: Dictionary,
-) -> Array:
- return [
- _session.get_session_id(),
- transfer_id,
- str(letter.get("mail_id", "")),
- str(letter.get("sender_fingerprint", "")),
- str(letter.get("recipient_fingerprint", "")),
- phase,
- applied,
- ]
-
-
-func _request_sender_rollback(peer_id: int, transfer_id: String) -> void:
- if peer_id == _session.get_local_peer_id():
- _rollback_sender(transfer_id)
- else:
- rollback_sender.rpc_id(peer_id, transfer_id)
-
-
-@rpc("authority", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
-func rollback_sender(transfer_id: String) -> void:
- _rollback_sender(transfer_id)
-
-
-func _rollback_sender(transfer_id: String) -> void:
- var snapshot: Dictionary = _local_removal_snapshots.get(transfer_id, {})
- if not snapshot.is_empty():
- _restore_assets(snapshot)
- _save_manager.save_if_dirty()
- _local_removal_snapshots.erase(transfer_id)
-
-
-func _finish_transfer(transfer_id: String, accepted: bool, message: String) -> void:
- var transfer: Dictionary = _pending_transfers.get(transfer_id, {})
- if transfer.is_empty():
- return
- var letter: Dictionary = transfer["letter"]
- letter["state"] = (
- NetworkMailProtocol.State.ACCEPTED
- if accepted else NetworkMailProtocol.State.ATTACHMENT_RECALLED
- )
- _pending_transfers.erase(transfer_id)
- _update_participants(letter)
- operation_finished.emit(accepted, message)
-
-
-func decline_gift(mail_id: String) -> void:
- var request := _action_request(mail_id)
- if request.is_empty():
- return
- if _session.is_host():
- _handle_decline(_session.get_local_peer_id(), request)
- else:
- submit_decline.rpc_id(1, request)
-
-
-@rpc("any_peer", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
-func submit_decline(data: Dictionary) -> void:
- var sender := multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(sender):
- _handle_decline(sender, data)
-
-
-func _handle_decline(peer_id: int, data: Dictionary) -> void:
- var action_id := str(data.get("request_id", ""))
- var letter: Dictionary = _letters.get(str(data.get("mail_id", "")), {})
- if _action_ledger.has(action_id):
- return
- if (
- letter.is_empty()
- or int(letter["recipient_peer_id"]) != peer_id
- or int(letter["state"]) not in [
- NetworkMailProtocol.State.SENT_UNREAD,
- NetworkMailProtocol.State.READ,
- ]
- ):
- return
- _action_ledger[action_id] = true
- letter["state"] = NetworkMailProtocol.State.DECLINED
- _request_release(int(letter["sender_peer_id"]), letter["reservation_id"])
- _update_participants(letter)
-
-
-func _request_release(peer_id: int, reservation_id: String) -> void:
- if reservation_id.is_empty():
- return
- if peer_id == _session.get_local_peer_id():
- _reservations.release(reservation_id)
- else:
- release_reservation.rpc_id(peer_id, reservation_id)
-
-
-@rpc("authority", "call_remote", "reliable", NetworkMailProtocol.RELIABLE_CHANNEL)
-func release_reservation(reservation_id: String) -> void:
- _reservations.release(reservation_id)
-
-
-func _update_participants(letter: Dictionary) -> void:
- _letters[letter["mail_id"]] = letter
- _deliver_private_letter(int(letter["sender_peer_id"]), letter)
- _deliver_private_letter(int(letter["recipient_peer_id"]), letter)
-
-
-func _action_request(mail_id: String) -> Dictionary:
- if mail_id.is_empty() or not _local_letters.has(mail_id):
- return {}
- return {
- "request_id": _new_id("mail_action"),
- "mail_id": mail_id,
- "session_id": _session.get_session_id(),
- }
-
-
-func _validate_attachment_structure(attachment: Dictionary) -> bool:
- if typeof(attachment.get("type")) != TYPE_INT:
- return false
- match int(attachment["type"]):
- PlayerAssetReservationService.AttachmentType.NONE:
- return attachment.size() == 1
- PlayerAssetReservationService.AttachmentType.FISH_COIN:
- return (
- attachment.size() == 2
- and
- typeof(attachment.get("amount")) == TYPE_INT
- and int(attachment["amount"]) >= 1
- and int(attachment["amount"]) <= 1000000000
- )
- PlayerAssetReservationService.AttachmentType.FISH:
- if not (
- attachment.size() == 3
- and
- typeof(attachment.get("catch_id")) == TYPE_STRING
- and typeof(attachment.get("catch")) == TYPE_DICTIONARY
- ):
- return false
- var fish_catch := _decode_catch(attachment)
- return (
- fish_catch != null
- and str(fish_catch.catch_id) == str(attachment["catch_id"])
- and not bool(
- Dictionary(attachment["catch"]).get("is_favorited", false)
- )
- )
- PlayerAssetReservationService.AttachmentType.CONSUMABLE:
- var item := _item_catalog.get_item_by_id(
- StringName(str(attachment.get("item_id", "")))
- )
- return (
- attachment.size() == 3
- and
- item != null
- and item.category == ItemData.Category.CONSUMABLE
- and typeof(attachment.get("quantity")) == TYPE_INT
- and int(attachment["quantity"]) >= 1
- and int(attachment["quantity"]) <= item.max_stack
- )
- return false
-
-
-func _can_receive(attachment: Dictionary) -> bool:
- match int(attachment.get("type", 0)):
- PlayerAssetReservationService.AttachmentType.FISH_COIN:
- return _wallet.can_credit(int(attachment["amount"]))
- PlayerAssetReservationService.AttachmentType.FISH:
- return (
- _inventory.can_accept_catch(
- StringName(str(attachment["catch_id"]))
- )
- and not _inventory.contains_catch_id(
- StringName(str(attachment["catch_id"]))
- )
- and _decode_catch(attachment) != null
- )
- PlayerAssetReservationService.AttachmentType.CONSUMABLE:
- return _bag.can_add_item(
- StringName(str(attachment["item_id"])),
- int(attachment["quantity"])
- )
- return false
-
-
-func _apply_award(attachment: Dictionary) -> bool:
- if not _can_receive(attachment):
- return false
- match int(attachment["type"]):
- PlayerAssetReservationService.AttachmentType.FISH_COIN:
- return _wallet.credit(int(attachment["amount"]))
- PlayerAssetReservationService.AttachmentType.FISH:
- var fish_catch := _decode_catch(attachment)
- _inventory.add_catch(fish_catch)
- _collection_log.mark_quality_discovered(
- fish_catch.fish_id,
- fish_catch.quality,
- )
- return _inventory.contains_catch_id(fish_catch.catch_id)
- PlayerAssetReservationService.AttachmentType.CONSUMABLE:
- return _bag.add_item(
- StringName(str(attachment["item_id"])),
- int(attachment["quantity"])
- )
- return false
-
-
-func _decode_catch(attachment: Dictionary) -> FishCatch:
- var data: Dictionary = attachment.get("catch", {})
- var fish: FishData = _fish_catalog.get_fish_by_id(
- StringName(str(data.get("fish_id", "")))
- )
- var fish_catch: FishCatch = FishCatch.from_network_dict(data, fish)
- if fish_catch == null or fish == null:
- return null
- if (
- fish_catch.weight_lb < fish.get_minimum_weight()
- or fish_catch.weight_lb > fish.get_maximum_weight()
- or not is_equal_approx(
- fish_catch.display_scale,
- fish.get_display_scale_for_weight(fish_catch.weight_lb),
- )
- or fish_catch.sale_value != FishQualityType.apply_sale_value(
- fish.get_sale_value_for_weight(fish_catch.weight_lb),
- fish_catch.quality,
- )
- ):
- return null
- return fish_catch
-
-
-func _capture_assets() -> Dictionary:
- return {
- "wallet": _wallet.get_balance(),
- "bag": _bag.get_all_items(),
- "unlocked_bait_ids": _bag.get_unlocked_bait_ids(),
- "catches": _inventory.get_all_catches(),
- "next_sequence": _inventory.get_next_catch_sequence(),
- "discovered": _collection_log.get_discovered_ids(),
- "quality_masks": _collection_log.get_discovered_quality_masks(),
- }
-
-
-func _restore_assets(snapshot: Dictionary) -> void:
- _wallet.restore_balance(int(snapshot["wallet"]))
- _bag.replace_all_items(snapshot["bag"])
- _bag.replace_unlocked_bait_ids(snapshot["unlocked_bait_ids"])
- _inventory.replace_all_catches(
- snapshot["catches"], int(snapshot["next_sequence"])
- )
- _collection_log.replace_discovery_state(
- snapshot["discovered"],
- snapshot["quality_masks"],
- )
-
-
-func _emit_mailbox() -> void:
- mailbox_changed.emit()
- unread_count_changed.emit(get_unread_count())
-
-
-func _letter_is_locally_blocked(letter: Dictionary) -> bool:
- if _relationship_policy == null:
- return false
- var local_id := _session.get_local_peer_id()
- var other_fingerprint := (
- str(letter.get("sender_fingerprint", ""))
- if int(letter.get("sender_peer_id", 0)) != local_id
- else str(letter.get("recipient_fingerprint", ""))
- )
- return _relationship_policy.is_locally_blocked(other_fingerprint)
-
-
-func _incoming_gift_is_unresolved(letter: Dictionary) -> bool:
- return (
- is_local_recipient(letter)
- and int(letter.get("attachment", {}).get("type", 0))
- != PlayerAssetReservationService.AttachmentType.NONE
- and int(letter.get("state", -1)) in [
- NetworkMailProtocol.State.SENT_UNREAD,
- NetworkMailProtocol.State.READ,
- NetworkMailProtocol.State.ACCEPTANCE_PENDING,
- ]
- )
-
-
-func _on_peer_removed(peer_id: int) -> void:
- if not _session.is_host():
- # The host delivers any unresolved final state. Keep local copies so
- # disconnect feedback cannot disappear or overwrite settled mail.
- peers_changed.emit()
- return
- for transfer_id: String in _pending_transfers.keys().duplicate():
- var transfer: Dictionary = _pending_transfers.get(transfer_id, {})
- var transfer_letter: Dictionary = transfer.get("letter", {})
- if peer_id not in [
- int(transfer_letter.get("sender_peer_id", 0)),
- int(transfer_letter.get("recipient_peer_id", 0)),
- ]:
- continue
- if bool(transfer.get("sender_removed", false)):
- var sender_id := int(transfer_letter.get("sender_peer_id", 0))
- if _session.is_authenticated_peer(sender_id):
- _request_sender_rollback(sender_id, transfer_id)
- _finish_transfer(transfer_id, false, "Gift returned to sender.")
- for letter: Dictionary in _letters.values():
- if int(letter["recipient_peer_id"]) == peer_id:
- if int(letter["state"]) in [
- NetworkMailProtocol.State.SENT_UNREAD,
- NetworkMailProtocol.State.READ,
- ]:
- letter["state"] = NetworkMailProtocol.State.CANCELLED
- _request_release(
- int(letter["sender_peer_id"]), letter["reservation_id"]
- )
- _deliver_private_letter(
- int(letter["sender_peer_id"]), letter
- )
- elif (
- int(letter["sender_peer_id"]) == peer_id
- and int(letter["state"]) in [
- NetworkMailProtocol.State.SENT_UNREAD,
- NetworkMailProtocol.State.READ,
- ]
- ):
- letter["state"] = NetworkMailProtocol.State.ATTACHMENT_RECALLED
- _update_participants(letter)
- _send_ledger.erase(peer_id)
- peers_changed.emit()
-
-
-func _on_session_state_changed(state: NetworkSession.State) -> void:
- if state in [
- NetworkSession.State.INACTIVE,
- NetworkSession.State.DISCONNECTING,
- NetworkSession.State.CONNECTION_FAILED,
- NetworkSession.State.SERVER_LOST,
- ]:
- _reservations.release_all()
- _letters.clear()
- _local_letters.clear()
- _send_ledger.clear()
- _action_ledger.clear()
- _pending_local_send.clear()
- _pending_transfers.clear()
- _local_removal_snapshots.clear()
- _received_awards.clear()
- _archived_local_letters.clear()
- _deleted_local_letters.clear()
- _sequence = 0
- _emit_mailbox()
-
-
-func _bound(values: Dictionary) -> void:
- while values.size() > MAX_LEDGER:
- values.erase(values.keys().front())
-
-
-func _new_id(prefix: String) -> String:
- return "%s:%s" % [
- prefix, Crypto.new().generate_random_bytes(16).hex_encode(),
- ]
diff --git a/network/network_mail_service.gd.uid b/network/network_mail_service.gd.uid
deleted file mode 100644
index 4a16d79..0000000
--- a/network/network_mail_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://t235k67v0dno
diff --git a/network/network_player_animation_protocol.gd b/network/network_player_animation_protocol.gd
deleted file mode 100644
index 40fe1c8..0000000
--- a/network/network_player_animation_protocol.gd
+++ /dev/null
@@ -1,102 +0,0 @@
-class_name NetworkPlayerAnimationProtocol
-extends RefCounted
-
-const FORMAT_VERSION: int = 1
-const MAX_STATE_ID_LENGTH: int = 64
-const MAX_ACTION_SEQUENCE: int = 2147483647
-const MAX_ACTION_ELAPSED_SECONDS: float = 86400.0
-
-const LOCOMOTION_IDLE: StringName = &"idle"
-const LOCOMOTION_WALKING: StringName = &"walking"
-const LOCOMOTION_RUNNING: StringName = &"running"
-const LOCOMOTION_SNEAKING: StringName = &"sneaking"
-
-
-static func make_state(
- locomotion_id: StringName,
- grounded: bool,
- action_id: StringName = &"",
- action_sequence: int = 0,
- action_elapsed: float = 0.0,
- action_paused: bool = false,
-) -> Dictionary:
- return {
- "format_version": FORMAT_VERSION,
- "locomotion_id": String(locomotion_id),
- "grounded": grounded,
- "action": make_action_state(
- action_id,
- action_sequence,
- action_elapsed,
- action_paused,
- ),
- }
-
-
-static func make_action_state(
- action_id: StringName = &"",
- action_sequence: int = 0,
- action_elapsed: float = 0.0,
- paused: bool = false,
-) -> Dictionary:
- return {
- "id": String(action_id),
- "sequence": action_sequence,
- "elapsed": action_elapsed,
- "paused": paused,
- }
-
-
-static func validate_state(value: Variant) -> bool:
- if typeof(value) != TYPE_DICTIONARY:
- return false
- var state: Dictionary = value
- return (
- typeof(state.get("format_version")) == TYPE_INT
- and int(state["format_version"]) == FORMAT_VERSION
- and valid_state_id(state.get("locomotion_id"), false)
- and typeof(state.get("grounded")) == TYPE_BOOL
- and validate_action_state(state.get("action"))
- )
-
-
-static func validate_action_state(value: Variant) -> bool:
- if typeof(value) != TYPE_DICTIONARY:
- return false
- var action: Dictionary = value
- if (
- not valid_state_id(action.get("id"), true)
- or typeof(action.get("sequence")) != TYPE_INT
- or typeof(action.get("elapsed")) not in [TYPE_FLOAT, TYPE_INT]
- or typeof(action.get("paused", false)) != TYPE_BOOL
- ):
- return false
- var sequence: int = int(action["sequence"])
- var elapsed: float = float(action["elapsed"])
- return (
- sequence >= 0
- and sequence <= MAX_ACTION_SEQUENCE
- and is_finite(elapsed)
- and elapsed >= 0.0
- and elapsed <= MAX_ACTION_ELAPSED_SECONDS
- )
-
-
-static func valid_state_id(value: Variant, allow_empty: bool) -> bool:
- if typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME]:
- return false
- var state_id: String = str(value)
- if state_id.is_empty():
- return allow_empty
- if state_id.length() > MAX_STATE_ID_LENGTH:
- return false
- for index: int in state_id.length():
- var codepoint: int = state_id.unicode_at(index)
- var valid_character: bool = (
- codepoint >= 97 and codepoint <= 122
- or codepoint >= 48 and codepoint <= 57
- or codepoint == 95
- )
- if not valid_character:
- return false
- return true
diff --git a/network/network_player_animation_protocol.gd.uid b/network/network_player_animation_protocol.gd.uid
deleted file mode 100644
index 466ad02..0000000
--- a/network/network_player_animation_protocol.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cmaaep4n3p2tq
diff --git a/network/network_player_list_service.gd b/network/network_player_list_service.gd
deleted file mode 100644
index 677df05..0000000
--- a/network/network_player_list_service.gd
+++ /dev/null
@@ -1,1147 +0,0 @@
-class_name NetworkPlayerListService
-extends Node
-
-signal entries_changed
-signal moderation_finished(success: bool, message: String)
-signal friend_request_received(
- request_id: String,
- fingerprint: String,
- display_name: String,
-)
-signal friend_action_finished(success: bool, message: String)
-
-const MAX_PENDING_FRIEND_REQUESTS := 16
-const FRIEND_REQUEST_ID_LENGTH := 32
-
-var _session: NetworkSession
-var _relationships: PlayerRelationshipStore
-var _bans: HostBanStore
-var _known: KnownPlayerStore
-var _spawn: PlayerSpawnService
-var _chat: NetworkChatService
-var _mail: NetworkMailService
-var _surface_drawing: NetworkSurfaceDrawingService
-var _revision := 0
-var _host_block_pairs: Dictionary[String, bool] = {}
-var _peer_fingerprints: Dictionary[int, String] = {}
-var _remote_bans: Array[Dictionary] = []
-var _ban_snapshot_requested: bool = false
-var _ban_snapshot_loaded: bool = false
-var _pending_outgoing_friends: Dictionary[String, Dictionary] = {}
-var _pending_incoming_friends: Dictionary[String, Dictionary] = {}
-var _host_friend_routes: Dictionary[String, Dictionary] = {}
-
-
-func setup(
- session: NetworkSession,
- relationships: PlayerRelationshipStore,
- bans: HostBanStore,
- known: KnownPlayerStore,
- spawn: PlayerSpawnService,
- chat: NetworkChatService,
- mail: NetworkMailService,
-) -> void:
- _session = session
- _relationships = relationships
- _bans = bans
- _known = known
- _spawn = spawn
- _chat = chat
- _mail = mail
- _session.peer_authenticated.connect(_on_peer_authenticated)
- _session.peer_removed.connect(_on_peer_removed)
- _session.peer_display_name_changed.connect(func(_id: int, _name: String) -> void: _changed())
- _session.peer_count_changed.connect(
- func(_count: int, _maximum: int) -> void: _sync_authenticated_peers()
- )
- _session.state_changed.connect(_on_session_state_changed)
- _session.operator_status_changed.connect(_on_operator_status_changed)
- _relationships.relationship_changed.connect(_on_relationship_changed)
- _bans.bans_changed.connect(_changed)
- _chat.set_relationship_store(_relationships)
- _mail.set_relationship_policy(self)
- for peer_id: int in _session.get_authenticated_peer_ids():
- _on_peer_authenticated(peer_id, "")
-
-
-func get_entries() -> Array[PlayerListEntry]:
- var result: Array[PlayerListEntry] = []
- var local_id := _session.get_local_peer_id()
- for peer_id: int in _session.get_authenticated_peer_ids():
- var record := _session.get_peer_record(peer_id)
- if record == null or not record.identity_authenticated:
- continue
- var blocked := _relationships.is_blocked(record.identity_fingerprint)
- if blocked and peer_id != local_id:
- continue
- var entry := PlayerListEntry.new()
- entry.peer_id = peer_id
- entry.full_fingerprint = record.identity_fingerprint
- entry.compact_fingerprint = NetworkIdentityCrypto.compact_suffix(record.identity_fingerprint)
- entry.display_name = record.display_name
- entry.is_host = peer_id == 1
- entry.is_operator = _session.is_peer_operator(peer_id)
- entry.is_local_player = peer_id == local_id
- entry.continuity_state = _known.identity_status(record.identity_fingerprint, record.display_name)
- entry.ping_to_host_ms = _session.get_peer_rtt_ms(peer_id)
- entry.muted = _relationships.is_muted(record.identity_fingerprint)
- entry.blocked = blocked
- entry.is_friend = _relationships.is_friend(record.identity_fingerprint)
- entry.can_request_friend = (
- not entry.is_local_player
- and not entry.is_friend
- and not entry.blocked
- and _session.supports_server_capability(
- NetworkProtocol.FRIENDS_CAPABILITY
- )
- and _session.peer_supports_capability(
- peer_id, NetworkProtocol.FRIENDS_CAPABILITY
- )
- )
- entry.can_kick = (
- _session.can_local_moderate()
- and peer_id != local_id
- and peer_id != 1
- and (_session.is_host() or not entry.is_operator)
- )
- entry.can_ban = entry.can_kick
- entry.can_clear_art = entry.can_kick
- entry.can_manage_operator = (
- _session.can_manage_operators()
- and peer_id != local_id
- and peer_id != 1
- )
- entry.revision = _revision
- result.append(entry)
- result.sort_custom(_entry_before)
- return result
-
-
-func get_connected_count() -> int:
- return _session.get_player_count()
-
-
-func get_max_players() -> int:
- return _session.get_session_max_players()
-
-
-func is_local_host() -> bool:
- return _session.is_host()
-
-
-func is_local_moderator() -> bool:
- return _session.can_local_moderate()
-
-
-func can_manage_operators() -> bool:
- return _session.can_manage_operators()
-
-
-func is_open_host() -> bool:
- return _session.is_open_host()
-
-
-func set_host_open(is_open: bool) -> bool:
- return _session.set_host_open(is_open)
-
-
-func get_relationships() -> Array[Dictionary]:
- return _relationships.get_records()
-
-
-func get_friends() -> Array[Dictionary]:
- return _relationships.get_friends()
-
-
-func get_bans() -> Array[Dictionary]:
- if _session.is_host():
- return _bans.get_bans(_session.get_host_identity_fingerprint())
- if not _session.is_local_operator():
- return []
- _request_ban_snapshot()
- return _remote_bans.duplicate(true)
-
-
-func set_surface_drawing_service(
- service: NetworkSurfaceDrawingService,
-) -> void:
- _surface_drawing = service
- if (
- _surface_drawing != null
- and not _surface_drawing.session_artwork_changed.is_connected(
- _on_session_artwork_changed
- )
- ):
- _surface_drawing.session_artwork_changed.connect(
- _on_session_artwork_changed
- )
- _changed()
-
-
-func get_session_artwork_counts() -> Vector2i:
- if _surface_drawing == null:
- return Vector2i.ZERO
- return Vector2i(
- _surface_drawing.get_canvas_count(),
- _surface_drawing.get_painted_cell_count(),
- )
-
-
-func reset_session_artwork() -> bool:
- if _session.is_host():
- return _reset_session_artwork_on_host()
- if not _session.is_local_operator():
- return false
- request_reset_session_artwork.rpc_id(1)
- return true
-
-
-func set_muted(fingerprint: String, display_name: String, value: bool) -> bool:
- if fingerprint == _session.get_local_identity_fingerprint():
- return false
- return _relationships.set_muted(fingerprint, display_name, value)
-
-
-func set_blocked(fingerprint: String, display_name: String, value: bool) -> bool:
- if fingerprint == _session.get_local_identity_fingerprint():
- return false
- return _relationships.set_blocked(fingerprint, display_name, value)
-
-
-func remove_friend(fingerprint: String, display_name: String) -> bool:
- if fingerprint == _session.get_local_identity_fingerprint():
- return false
- var ok := _relationships.remove_friend(fingerprint, display_name)
- friend_action_finished.emit(
- ok,
- "Friend removed." if ok else "Friend could not be removed.",
- )
- return ok
-
-
-func send_friend_request(
- peer_id: int,
- fingerprint: String,
- display_name: String,
-) -> bool:
- if (
- _pending_outgoing_friends.size() >= MAX_PENDING_FRIEND_REQUESTS
- or fingerprint == _session.get_local_identity_fingerprint()
- or _relationships.is_blocked(fingerprint)
- or _relationships.is_friend(fingerprint)
- or not _valid_friend_target(peer_id, fingerprint)
- ):
- friend_action_finished.emit(
- false, "This person needs to be online to do this."
- )
- return false
- var local_capabilities: Dictionary = (
- _relationships.create_friend_capabilities()
- )
- if local_capabilities.is_empty():
- friend_action_finished.emit(false, "Friend request could not be created.")
- return false
- var request_id := NetworkIdentityCrypto.secure_id(16)
- _pending_outgoing_friends[request_id] = {
- "target_peer_id": peer_id,
- "target_fingerprint": fingerprint,
- "target_display_name": display_name,
- "local_capabilities": local_capabilities,
- }
- var public_capabilities := _public_friend_capabilities(local_capabilities)
- if _session.is_host():
- _route_friend_request(
- _session.get_local_peer_id(), peer_id, request_id, public_capabilities
- )
- else:
- request_friendship.rpc_id(
- 1, peer_id, request_id, public_capabilities
- )
- friend_action_finished.emit(true, "Friend request sent.")
- return true
-
-
-func respond_friend_request(request_id: String, accepted: bool) -> bool:
- var pending: Dictionary = _pending_incoming_friends.get(request_id, {})
- if pending.is_empty():
- friend_action_finished.emit(
- false, "This friend request is no longer available."
- )
- return false
- _pending_incoming_friends.erase(request_id)
- var local_capabilities: Dictionary = {}
- var public_capabilities: Dictionary = {}
- var response_accepted := accepted
- if accepted:
- local_capabilities = _relationships.create_friend_capabilities()
- response_accepted = (
- not local_capabilities.is_empty()
- and _relationships.add_friend(
- str(pending["requester_fingerprint"]),
- str(pending["requester_display_name"]),
- local_capabilities,
- pending["remote_capabilities"],
- )
- )
- if response_accepted:
- public_capabilities = _public_friend_capabilities(local_capabilities)
- if _session.is_host():
- _route_friend_response(
- _session.get_local_peer_id(),
- request_id,
- response_accepted,
- public_capabilities,
- )
- else:
- respond_friendship.rpc_id(
- 1, request_id, response_accepted, public_capabilities
- )
- friend_action_finished.emit(
- response_accepted or not accepted,
- "Friend added."
- if response_accepted
- else "Friend request declined."
- if not accepted
- else "Friend could not be saved.",
- )
- return response_accepted or not accepted
-
-
-@rpc("any_peer", "call_remote", "reliable", 0)
-func request_friendship(
- target_peer_id: int,
- request_id: String,
- public_capabilities: Dictionary,
-) -> void:
- _route_friend_request(
- multiplayer.get_remote_sender_id(),
- target_peer_id,
- request_id,
- public_capabilities,
- )
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_friend_request(
- request_id: String,
- requester_fingerprint: String,
- requester_display_name: String,
- public_capabilities: Dictionary,
-) -> void:
- _receive_friend_request_local(
- request_id,
- requester_fingerprint,
- requester_display_name,
- public_capabilities,
- )
-
-
-@rpc("any_peer", "call_remote", "reliable", 0)
-func respond_friendship(
- request_id: String,
- accepted: bool,
- public_capabilities: Dictionary,
-) -> void:
- _route_friend_response(
- multiplayer.get_remote_sender_id(),
- request_id,
- accepted,
- public_capabilities,
- )
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_friend_result(
- request_id: String,
- accepted: bool,
- public_capabilities: Dictionary,
- message: String,
-) -> void:
- _receive_friend_result_local(
- request_id, accepted, public_capabilities, message
- )
-
-
-func kick(peer_id: int, fingerprint: String, revision: int) -> bool:
- if _session.is_host():
- return _kick_on_host(peer_id, fingerprint, revision, true)
- if not _session.is_local_operator():
- moderation_finished.emit(false, "Only the host or an operator can remove players.")
- return false
- request_kick.rpc_id(1, peer_id, fingerprint)
- return true
-
-
-func clear_art(peer_id: int, fingerprint: String, revision: int) -> bool:
- if _session.is_host():
- return _clear_art_on_host(
- peer_id, fingerprint, revision, true
- )
- if not _session.is_local_operator():
- moderation_finished.emit(
- false, "Only the host or an operator can clear player artwork."
- )
- return false
- request_clear_art.rpc_id(1, peer_id, fingerprint)
- return true
-
-
-func ban(
- peer_id: int,
- fingerprint: String,
- display_name: String,
- revision: int,
-) -> bool:
- if _session.is_host():
- return _ban_on_host(
- peer_id, fingerprint, display_name, revision, true
- )
- if not _session.is_local_operator():
- moderation_finished.emit(false, "Only the host or an operator can ban players.")
- return false
- request_ban.rpc_id(1, peer_id, fingerprint)
- return true
-
-
-func unban(fingerprint: String) -> bool:
- if _session.is_host():
- var ok: bool = _bans.unban(
- _session.get_host_identity_fingerprint(), fingerprint
- )
- moderation_finished.emit(
- ok, "Player unbanned." if ok else "Ban could not be removed."
- )
- return ok
- if not _session.is_local_operator():
- moderation_finished.emit(false, "Only the host or an operator can remove bans.")
- return false
- request_unban.rpc_id(1, fingerprint)
- return true
-
-
-func set_operator(
- peer_id: int,
- fingerprint: String,
- enabled: bool,
- revision: int,
-) -> bool:
- if (
- not _session.can_manage_operators()
- or not _valid_moderation_target(
- peer_id, fingerprint, revision, true, true
- )
- ):
- moderation_finished.emit(false, "That player is no longer connected.")
- return false
- var ok: bool = _session.set_peer_operator(peer_id, fingerprint, enabled)
- moderation_finished.emit(
- ok,
- ("Player is now an operator." if enabled else "Operator access removed.")
- if ok
- else "Operator access could not be changed.",
- )
- return ok
-
-
-@rpc("any_peer", "call_remote", "reliable", 0)
-func request_kick(peer_id: int, fingerprint: String) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if not _valid_operator_sender(sender_id):
- return
- var ok: bool = _kick_on_host(peer_id, fingerprint, -1, false)
- _send_moderation_result(
- sender_id, ok, "Player removed." if ok else "Player could not be removed."
- )
-
-
-@rpc("any_peer", "call_remote", "reliable", 0)
-func request_clear_art(peer_id: int, fingerprint: String) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if not _valid_operator_sender(sender_id):
- return
- var ok: bool = _clear_art_on_host(
- peer_id, fingerprint, -1, false
- )
- _send_moderation_result(
- sender_id,
- ok,
- "Player artwork cleared."
- if ok
- else "Player artwork could not be cleared.",
- )
-
-
-@rpc("any_peer", "call_remote", "reliable", 0)
-func request_ban(peer_id: int, fingerprint: String) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if not _valid_operator_sender(sender_id):
- return
- var record: PeerRegistry.PeerRecord = _session.get_peer_record(peer_id)
- var display_name: String = record.display_name if record != null else "Player"
- var ok: bool = _ban_on_host(
- peer_id, fingerprint, display_name, -1, false
- )
- _send_moderation_result(
- sender_id, ok, "Player banned." if ok else "Player could not be banned."
- )
- _send_ban_snapshot(sender_id)
-
-
-@rpc("any_peer", "call_remote", "reliable", 0)
-func request_unban(fingerprint: String) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if (
- not _valid_operator_sender(sender_id)
- or not NetworkIdentityCrypto.valid_fingerprint(fingerprint)
- ):
- return
- var ok: bool = _bans.unban(
- _session.get_host_identity_fingerprint(), fingerprint
- )
- _send_moderation_result(
- sender_id, ok, "Player unbanned." if ok else "Ban could not be removed."
- )
- _send_ban_snapshot(sender_id)
-
-
-@rpc("any_peer", "call_remote", "reliable", 0)
-func request_reset_session_artwork() -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if not _valid_operator_sender(sender_id):
- return
- var ok: bool = _reset_session_artwork_on_host(false)
- _send_moderation_result(
- sender_id,
- ok,
- "Session artwork cleared."
- if ok
- else "Session artwork could not be cleared.",
- )
-
-
-@rpc("any_peer", "call_remote", "reliable", 0)
-func request_ban_snapshot() -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if _valid_operator_sender(sender_id):
- _send_ban_snapshot(sender_id)
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_moderation_result(success: bool, message: String) -> void:
- if not _session.is_joined_client():
- return
- moderation_finished.emit(success, message.left(120))
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_ban_snapshot(records: Array) -> void:
- if not _session.is_local_operator() or records.size() > HostBanStore.MAX_BANS:
- return
- var sanitized: Array[Dictionary] = []
- for value: Variant in records:
- if typeof(value) != TYPE_DICTIONARY:
- return
- var record: Dictionary = value
- var fingerprint: String = str(record.get("target_fingerprint", ""))
- var display_name: String = str(
- record.get("last_known_display_name", "Player")
- ).strip_edges().left(NetworkProtocol.MAX_DISPLAY_NAME_LENGTH)
- if (
- not NetworkIdentityCrypto.valid_fingerprint(fingerprint)
- or display_name.is_empty()
- or typeof(record.get("banned_unix")) != TYPE_INT
- ):
- return
- sanitized.append({
- "target_fingerprint": fingerprint,
- "last_known_display_name": display_name,
- "banned_unix": int(record["banned_unix"]),
- })
- _remote_bans = sanitized
- _ban_snapshot_requested = false
- _ban_snapshot_loaded = true
- _changed()
-
-
-func is_locally_blocked(fingerprint: String) -> bool:
- return _relationships.is_blocked(fingerprint)
-
-
-func pair_is_blocked(first: String, second: String) -> bool:
- if first.is_empty() or second.is_empty():
- return false
- return _host_block_pairs.has(_pair_key(first, second))
-
-
-@rpc("any_peer", "call_remote", "reliable", 0)
-func submit_block_policy(target_fingerprint: String, blocked: bool) -> void:
- var sender := multiplayer.get_remote_sender_id()
- if not _session.is_host() or not _session.is_authenticated_peer(sender):
- return
- var record := _session.get_peer_record(sender)
- if (
- record == null
- or not NetworkIdentityCrypto.valid_fingerprint(target_fingerprint)
- or record.identity_fingerprint == target_fingerprint
- ):
- return
- _set_host_pair(record.identity_fingerprint, target_fingerprint, blocked)
-
-
-func _on_relationship_changed(fingerprint: String) -> void:
- var blocked := _relationships.is_blocked(fingerprint)
- var avatar_peer := _peer_for_fingerprint(fingerprint)
- if avatar_peer > 0:
- _spawn.set_peer_presentation_visible(avatar_peer, not blocked)
- var local_fingerprint := _session.get_local_identity_fingerprint()
- if _session.is_host():
- _set_host_pair(local_fingerprint, fingerprint, blocked)
- elif _session.is_gameplay_session_active():
- submit_block_policy.rpc_id(1, fingerprint, blocked)
- _chat.refresh_relationship_filters()
- _mail.refresh_relationship_filters()
- _changed()
-
-
-func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
- var record := _session.get_peer_record(peer_id)
- if record != null:
- _peer_fingerprints[peer_id] = record.identity_fingerprint
- var blocked := _relationships.is_blocked(record.identity_fingerprint)
- _spawn.set_peer_presentation_visible(peer_id, not blocked)
- if blocked and peer_id != _session.get_local_peer_id():
- var local_fingerprint := _session.get_local_identity_fingerprint()
- if _session.is_host():
- _set_host_pair(
- local_fingerprint, record.identity_fingerprint, true
- )
- else:
- submit_block_policy.rpc_id(
- 1, record.identity_fingerprint, true
- )
- _changed()
-
-
-func _sync_authenticated_peers() -> void:
- for peer_id: int in _session.get_authenticated_peer_ids():
- if not _peer_fingerprints.has(peer_id):
- _on_peer_authenticated(peer_id, "")
- _changed()
-
-
-func _on_peer_removed(peer_id: int) -> void:
- var fingerprint := str(_peer_fingerprints.get(peer_id, ""))
- _peer_fingerprints.erase(peer_id)
- if _session.is_host() and not fingerprint.is_empty():
- for key: String in _host_block_pairs.keys():
- if fingerprint in key.split(":"):
- _host_block_pairs.erase(key)
- for request_id: String in _host_friend_routes.keys():
- var route: Dictionary = _host_friend_routes[request_id]
- if int(route.get("target_peer_id", 0)) == peer_id:
- _send_friend_result_to_peer(
- int(route.get("requester_peer_id", 0)),
- request_id,
- false,
- {},
- "This person needs to be online to do this.",
- )
- if (
- int(route.get("target_peer_id", 0)) == peer_id
- or int(route.get("requester_peer_id", 0)) == peer_id
- ):
- _host_friend_routes.erase(request_id)
- for request_id: String in _pending_outgoing_friends.keys():
- var pending: Dictionary = _pending_outgoing_friends[request_id]
- if str(pending.get("target_fingerprint", "")) == fingerprint:
- _pending_outgoing_friends.erase(request_id)
- friend_action_finished.emit(
- false, "This person needs to be online to do this."
- )
- for request_id: String in _pending_incoming_friends.keys():
- var pending: Dictionary = _pending_incoming_friends[request_id]
- if str(pending.get("requester_fingerprint", "")) == fingerprint:
- _pending_incoming_friends.erase(request_id)
- _changed()
-
-
-func _on_session_state_changed(state: NetworkSession.State) -> void:
- if state in [
- NetworkSession.State.INACTIVE,
- NetworkSession.State.DISCONNECTING,
- NetworkSession.State.CONNECTION_FAILED,
- NetworkSession.State.SERVER_LOST,
- ]:
- _host_block_pairs.clear()
- _peer_fingerprints.clear()
- _remote_bans.clear()
- _ban_snapshot_requested = false
- _ban_snapshot_loaded = false
- _pending_outgoing_friends.clear()
- _pending_incoming_friends.clear()
- _host_friend_routes.clear()
- elif state == NetworkSession.State.JOINED_CLIENT:
- _request_ban_snapshot()
- _changed()
-
-
-func _on_operator_status_changed(peer_id: int, enabled: bool) -> void:
- if peer_id == _session.get_local_peer_id():
- if enabled:
- _request_ban_snapshot()
- else:
- _remote_bans.clear()
- _ban_snapshot_requested = false
- _ban_snapshot_loaded = false
- _changed()
-
-
-func _set_host_pair(first: String, second: String, blocked: bool) -> void:
- var key := _pair_key(first, second)
- if blocked:
- _host_block_pairs[key] = true
- _mail.cancel_unaccepted_between(first, second)
- else:
- _host_block_pairs.erase(key)
-
-
-func _pair_key(first: String, second: String) -> String:
- return "%s:%s" % ([first, second] if first < second else [second, first])
-
-
-func _peer_for_fingerprint(fingerprint: String) -> int:
- for peer_id: int in _session.get_authenticated_peer_ids():
- var record := _session.get_peer_record(peer_id)
- if record != null and record.identity_fingerprint == fingerprint:
- return peer_id
- return 0
-
-
-func _valid_friend_target(peer_id: int, fingerprint: String) -> bool:
- if (
- not _session.is_gameplay_session_active()
- or not _session.supports_server_capability(
- NetworkProtocol.FRIENDS_CAPABILITY
- )
- or not _session.peer_supports_capability(
- peer_id, NetworkProtocol.FRIENDS_CAPABILITY
- )
- ):
- return false
- var record: PeerRegistry.PeerRecord = _session.get_peer_record(peer_id)
- return (
- record != null
- and record.identity_authenticated
- and record.identity_fingerprint == fingerprint
- )
-
-
-func _route_friend_request(
- requester_peer_id: int,
- target_peer_id: int,
- request_id: String,
- public_capabilities: Dictionary,
-) -> void:
- if (
- not _session.is_host()
- or not _valid_friend_request_id(request_id)
- or not _valid_public_friend_capabilities(public_capabilities)
- or not _session.is_authenticated_peer(requester_peer_id)
- ):
- return
- var requester: PeerRegistry.PeerRecord = (
- _session.get_peer_record(requester_peer_id)
- )
- var target: PeerRegistry.PeerRecord = _session.get_peer_record(target_peer_id)
- var can_route := (
- requester != null
- and target != null
- and requester_peer_id != target_peer_id
- and _valid_friend_target(
- target_peer_id, target.identity_fingerprint
- )
- and _session.peer_supports_capability(
- requester_peer_id, NetworkProtocol.FRIENDS_CAPABILITY
- )
- and not pair_is_blocked(
- requester.identity_fingerprint, target.identity_fingerprint
- )
- and _host_friend_routes.size() < (
- MAX_PENDING_FRIEND_REQUESTS * 8
- )
- )
- if not can_route:
- _send_friend_result_to_peer(
- requester_peer_id,
- request_id,
- false,
- {},
- "This person needs to be online to do this.",
- )
- return
- var requester_pending_count := 0
- for route: Dictionary in _host_friend_routes.values():
- if int(route.get("requester_peer_id", 0)) == requester_peer_id:
- requester_pending_count += 1
- if requester_pending_count >= MAX_PENDING_FRIEND_REQUESTS:
- _send_friend_result_to_peer(
- requester_peer_id,
- request_id,
- false,
- {},
- "Too many friend requests are pending.",
- )
- return
- _host_friend_routes[request_id] = {
- "requester_peer_id": requester_peer_id,
- "target_peer_id": target_peer_id,
- }
- if target_peer_id == _session.get_local_peer_id():
- _receive_friend_request_local(
- request_id,
- requester.identity_fingerprint,
- requester.display_name,
- public_capabilities,
- )
- else:
- receive_friend_request.rpc_id(
- target_peer_id,
- request_id,
- requester.identity_fingerprint,
- requester.display_name,
- public_capabilities,
- )
-
-
-func _receive_friend_request_local(
- request_id: String,
- requester_fingerprint: String,
- requester_display_name: String,
- public_capabilities: Dictionary,
-) -> void:
- if (
- not _valid_friend_request_id(request_id)
- or not _valid_public_friend_capabilities(public_capabilities)
- or not NetworkIdentityCrypto.valid_fingerprint(requester_fingerprint)
- or not NetworkProfilePreferences.is_valid_display_name(
- requester_display_name
- )
- or _pending_incoming_friends.size()
- >= MAX_PENDING_FRIEND_REQUESTS
- or _relationships.is_blocked(requester_fingerprint)
- or _relationships.is_friend(requester_fingerprint)
- ):
- _decline_friend_request(request_id)
- return
- _pending_incoming_friends[request_id] = {
- "requester_fingerprint": requester_fingerprint,
- "requester_display_name": requester_display_name,
- "remote_capabilities": public_capabilities.duplicate(true),
- }
- friend_request_received.emit(
- request_id, requester_fingerprint, requester_display_name
- )
-
-
-func _decline_friend_request(request_id: String) -> void:
- if _session.is_host():
- _route_friend_response(
- _session.get_local_peer_id(), request_id, false, {}
- )
- elif _session.is_joined_client():
- respond_friendship.rpc_id(1, request_id, false, {})
-
-
-func _route_friend_response(
- responder_peer_id: int,
- request_id: String,
- accepted: bool,
- public_capabilities: Dictionary,
-) -> void:
- if not _session.is_host():
- return
- var route: Dictionary = _host_friend_routes.get(request_id, {})
- if (
- route.is_empty()
- or int(route.get("target_peer_id", 0)) != responder_peer_id
- ):
- return
- _host_friend_routes.erase(request_id)
- var response_accepted := (
- accepted and _valid_public_friend_capabilities(public_capabilities)
- )
- _send_friend_result_to_peer(
- int(route["requester_peer_id"]),
- request_id,
- response_accepted,
- public_capabilities if response_accepted else {},
- "Friend request accepted."
- if response_accepted
- else "Friend request declined."
- if not accepted
- else "Friend request could not be completed.",
- )
-
-
-func _send_friend_result_to_peer(
- peer_id: int,
- request_id: String,
- accepted: bool,
- public_capabilities: Dictionary,
- message: String,
-) -> void:
- if peer_id == _session.get_local_peer_id():
- _receive_friend_result_local(
- request_id, accepted, public_capabilities, message
- )
- elif _session.is_authenticated_peer(peer_id):
- receive_friend_result.rpc_id(
- peer_id,
- request_id,
- accepted,
- public_capabilities,
- message.left(120),
- )
-
-
-func _receive_friend_result_local(
- request_id: String,
- accepted: bool,
- public_capabilities: Dictionary,
- message: String,
-) -> void:
- var pending: Dictionary = _pending_outgoing_friends.get(request_id, {})
- if pending.is_empty():
- return
- _pending_outgoing_friends.erase(request_id)
- var success := false
- if accepted and _valid_public_friend_capabilities(public_capabilities):
- success = _relationships.add_friend(
- str(pending["target_fingerprint"]),
- str(pending["target_display_name"]),
- pending["local_capabilities"],
- public_capabilities,
- )
- message = "Friend added." if success else "Friend could not be saved."
- friend_action_finished.emit(success, message.left(120))
-
-
-func _public_friend_capabilities(capabilities: Dictionary) -> Dictionary:
- return {
- "presence_channel": str(capabilities.get("presence_channel", "")),
- "invite_token": str(capabilities.get("invite_token", "")),
- }
-
-
-func _valid_public_friend_capabilities(capabilities: Dictionary) -> bool:
- return (
- _valid_friend_hex(str(capabilities.get("presence_channel", "")))
- and _valid_friend_hex(str(capabilities.get("invite_token", "")))
- )
-
-
-func _valid_friend_request_id(request_id: String) -> bool:
- return _valid_friend_hex(request_id, FRIEND_REQUEST_ID_LENGTH)
-
-
-func _valid_friend_hex(
- value: String,
- expected_length: int = PlayerRelationshipStore.SOCIAL_TOKEN_LENGTH,
-) -> bool:
- if value.length() != expected_length:
- return false
- for character: String in value:
- if character not in "0123456789abcdef":
- return false
- return true
-
-
-func _valid_moderation_target(
- peer_id: int,
- fingerprint: String,
- revision: int,
- require_revision: bool,
- can_target_operator: bool,
-) -> bool:
- if (
- not _session.is_host()
- or (require_revision and revision != _revision)
- or peer_id == 1
- or (not can_target_operator and _session.is_peer_operator(peer_id))
- ):
- return false
- var record := _session.get_peer_record(peer_id)
- return record != null and record.identity_fingerprint == fingerprint
-
-
-func _valid_operator_sender(peer_id: int) -> bool:
- return (
- _session.is_host()
- and peer_id > 1
- and _session.is_authenticated_peer(peer_id)
- and _session.is_peer_operator(peer_id)
- )
-
-
-func _kick_on_host(
- peer_id: int,
- fingerprint: String,
- revision: int,
- local_host_request: bool,
-) -> bool:
- if not _valid_moderation_target(
- peer_id,
- fingerprint,
- revision,
- local_host_request,
- local_host_request,
- ):
- if local_host_request:
- moderation_finished.emit(false, "That player is no longer connected.")
- return false
- var ok: bool = _session.kick_authenticated_peer(peer_id, fingerprint)
- if local_host_request:
- moderation_finished.emit(
- ok, "Player removed." if ok else "Player could not be removed."
- )
- return ok
-
-
-func _ban_on_host(
- peer_id: int,
- fingerprint: String,
- display_name: String,
- revision: int,
- local_host_request: bool,
-) -> bool:
- if not _valid_moderation_target(
- peer_id,
- fingerprint,
- revision,
- local_host_request,
- local_host_request,
- ):
- if local_host_request:
- moderation_finished.emit(false, "That player is no longer connected.")
- return false
- var record: PeerRegistry.PeerRecord = _session.get_peer_record(peer_id)
- var trusted_display_name: String = (
- record.display_name if record != null else display_name
- )
- var host_fingerprint: String = _session.get_host_identity_fingerprint()
- if not _bans.ban(host_fingerprint, fingerprint, trusted_display_name):
- if local_host_request:
- moderation_finished.emit(false, "Ban could not be saved.")
- return false
- if _surface_drawing != null:
- _surface_drawing.clear_artwork_by_fingerprint(fingerprint)
- var ok: bool = _session.kick_authenticated_peer(peer_id, fingerprint, true)
- if local_host_request:
- moderation_finished.emit(ok, "Player banned." if ok else "Ban saved.")
- return true
-
-
-func _clear_art_on_host(
- peer_id: int,
- fingerprint: String,
- revision: int,
- local_host_request: bool,
-) -> bool:
- if not _valid_moderation_target(
- peer_id,
- fingerprint,
- revision,
- local_host_request,
- local_host_request,
- ):
- if local_host_request:
- moderation_finished.emit(
- false, "That player is no longer connected."
- )
- return false
- if _surface_drawing == null:
- if local_host_request:
- moderation_finished.emit(
- false, "Player artwork could not be cleared."
- )
- return false
- var affected_layers: int = (
- _surface_drawing.clear_artwork_by_fingerprint(fingerprint)
- )
- var ok: bool = affected_layers >= 0
- if local_host_request:
- moderation_finished.emit(
- ok,
- "Player artwork cleared."
- if affected_layers > 0
- else "No shared artwork from that player was found."
- if ok
- else "Player artwork could not be cleared.",
- )
- return ok
-
-
-func _reset_session_artwork_on_host(
- emit_local_result: bool = true,
-) -> bool:
- if not _session.is_host() or _surface_drawing == null:
- return false
- var ok: bool = _surface_drawing.clear_session_artwork()
- if emit_local_result:
- moderation_finished.emit(
- ok,
- "Session artwork cleared."
- if ok
- else "Session artwork could not be cleared.",
- )
- return ok
-
-
-func _send_moderation_result(
- peer_id: int,
- success: bool,
- message: String,
-) -> void:
- receive_moderation_result.rpc_id(peer_id, success, message.left(120))
-
-
-func _send_ban_snapshot(peer_id: int) -> void:
- if not _valid_operator_sender(peer_id):
- return
- receive_ban_snapshot.rpc_id(
- peer_id,
- _bans.get_bans(_session.get_host_identity_fingerprint()),
- )
-
-
-func _request_ban_snapshot() -> void:
- if (
- _session.is_local_operator()
- and not _ban_snapshot_requested
- and not _ban_snapshot_loaded
- ):
- _ban_snapshot_requested = true
- request_ban_snapshot.rpc_id(1)
-
-
-func _entry_before(a: PlayerListEntry, b: PlayerListEntry) -> bool:
- if a.is_host != b.is_host:
- return a.is_host
- if a.is_operator != b.is_operator:
- return a.is_operator
- if a.is_local_player != b.is_local_player:
- return a.is_local_player
- var compared := a.display_name.naturalnocasecmp_to(b.display_name)
- return compared < 0 if compared != 0 else a.full_fingerprint < b.full_fingerprint
-
-
-func _changed() -> void:
- _revision += 1
- entries_changed.emit()
-
-
-func _on_session_artwork_changed(
- _canvas_count: int,
- _painted_cell_count: int,
-) -> void:
- _changed()
diff --git a/network/network_player_list_service.gd.uid b/network/network_player_list_service.gd.uid
deleted file mode 100644
index be485b6..0000000
--- a/network/network_player_list_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c71playerlist0001
diff --git a/network/network_profile_preferences.gd b/network/network_profile_preferences.gd
deleted file mode 100644
index b6cac63..0000000
--- a/network/network_profile_preferences.gd
+++ /dev/null
@@ -1,227 +0,0 @@
-class_name NetworkProfilePreferences
-extends Node
-
-const FORMAT_VERSION: int = 1
-const VoiceProfilesType = preload(
- "res://player/animalese_voice_profiles.gd"
-)
-var profile_id: String = ""
-var display_name: String = "Player"
-var voice_id: String = VoiceProfilesType.DEFAULT_ID
-var speech_speed_id: String = VoiceProfilesType.DEFAULT_SPEED_ID
-var call_id: String = VoiceProfilesType.DEFAULT_CALL_ID
-var sample_set_id: String = VoiceProfilesType.DEFAULT_SAMPLE_SET_ID
-var created_at_unix: int = 0
-var _profile_path := ""
-var _expected_hash := ""
-var _data_root: PlayerDataRoot
-var _future_version := false
-
-
-func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
- _profile_path = path
- _data_root = data_root
- _expected_hash = ""
- _future_version = false
-
-
-func _temp_path() -> String:
- return _profile_path + ".tmp"
-
-
-func _backup_path() -> String:
- return _profile_path + ".backup"
-
-
-func load_or_create() -> bool:
- if _profile_path.is_empty():
- return false
- _recover_interrupted_write()
- if FileAccess.file_exists(_profile_path):
- if _load_existing():
- _expected_hash = PortableFileGuard.hash_file(_profile_path)
- return true
- if _future_version:
- return false
- var corrupt_path: String = (
- "%s.corrupt.%d.json"
- % [_profile_path.get_basename(), int(Time.get_unix_time_from_system())]
- )
- if not _rename(_profile_path, corrupt_path):
- push_warning("Invalid network profile was preserved and not overwritten.")
- return false
- profile_id = Crypto.new().generate_random_bytes(16).hex_encode()
- if profile_id.is_empty():
- profile_id = "%d-%d" % [
- int(Time.get_unix_time_from_system()),
- Time.get_ticks_usec(),
- ]
- display_name = "Player"
- voice_id = VoiceProfilesType.DEFAULT_ID
- speech_speed_id = VoiceProfilesType.DEFAULT_SPEED_ID
- call_id = VoiceProfilesType.DEFAULT_CALL_ID
- sample_set_id = VoiceProfilesType.DEFAULT_SAMPLE_SET_ID
- created_at_unix = int(Time.get_unix_time_from_system())
- return _save_atomic()
-
-
-func set_display_name(value: String) -> bool:
- return set_profile_identity(
- value,
- voice_id,
- speech_speed_id,
- call_id,
- sample_set_id,
- )
-
-
-func set_profile_identity(
- value: String,
- selected_voice_id: String,
- selected_speech_speed_id: String,
- selected_call_id: String,
- selected_sample_set_id: String = VoiceProfilesType.DEFAULT_SAMPLE_SET_ID,
-) -> bool:
- var clean_name: String = value.strip_edges()
- if (
- not is_valid_display_name(clean_name)
- or not VoiceProfilesType.is_valid(selected_voice_id)
- or not VoiceProfilesType.is_valid_speed(selected_speech_speed_id)
- or not VoiceProfilesType.is_valid_call(selected_call_id)
- or not VoiceProfilesType.is_valid_sample_set(selected_sample_set_id)
- ):
- return false
- var previous_name: String = display_name
- var previous_voice_id: String = voice_id
- var previous_speech_speed_id: String = speech_speed_id
- var previous_call_id: String = call_id
- var previous_sample_set_id: String = sample_set_id
- display_name = clean_name
- voice_id = selected_voice_id
- speech_speed_id = selected_speech_speed_id
- call_id = selected_call_id
- sample_set_id = selected_sample_set_id
- if _save_atomic():
- return true
- display_name = previous_name
- voice_id = previous_voice_id
- speech_speed_id = previous_speech_speed_id
- call_id = previous_call_id
- sample_set_id = previous_sample_set_id
- return false
-
-
-static func is_valid_display_name(value: String) -> bool:
- var clean_name := value.strip_edges()
- if clean_name.is_empty() or clean_name.length() > 24:
- return false
- for index: int in clean_name.length():
- var codepoint: int = clean_name.unicode_at(index)
- if codepoint < 32 or codepoint == 127:
- return false
- return true
-
-
-func _load_existing() -> bool:
- var file := FileAccess.open(_profile_path, FileAccess.READ)
- if file == null:
- return false
- var json := JSON.new()
- var error: Error = json.parse(file.get_as_text())
- file.close()
- if error != OK or typeof(json.data) != TYPE_DICTIONARY:
- return false
- var data: Dictionary = json.data
- if (
- typeof(data.get("format_version")) in [TYPE_INT, TYPE_FLOAT]
- and int(data.get("format_version")) > FORMAT_VERSION
- ):
- _future_version = true
- return false
- if (
- data.get("format_version") != FORMAT_VERSION
- or typeof(data.get("profile_id")) != TYPE_STRING
- or typeof(data.get("display_name")) != TYPE_STRING
- or typeof(data.get("created_at_unix")) not in [TYPE_INT, TYPE_FLOAT]
- ):
- return false
- var loaded_id: String = data["profile_id"]
- var loaded_name: String = data["display_name"]
- if (
- loaded_id.is_empty()
- or loaded_id.length() > NetworkProtocol.MAX_PROFILE_ID_LENGTH
- or loaded_name.is_empty()
- or loaded_name.length() > NetworkProtocol.MAX_DISPLAY_NAME_LENGTH
- ):
- return false
- profile_id = loaded_id
- display_name = loaded_name
- voice_id = VoiceProfilesType.sanitized_id(
- str(data.get("voice_id", VoiceProfilesType.DEFAULT_ID))
- )
- speech_speed_id = VoiceProfilesType.sanitized_speed_id(
- str(data.get("speech_speed_id", VoiceProfilesType.DEFAULT_SPEED_ID))
- )
- call_id = VoiceProfilesType.sanitized_call_id(
- str(data.get("call_id", VoiceProfilesType.DEFAULT_CALL_ID))
- )
- sample_set_id = VoiceProfilesType.sanitized_sample_set_id(
- str(data.get(
- "sample_set_id",
- VoiceProfilesType.DEFAULT_SAMPLE_SET_ID,
- ))
- )
- created_at_unix = int(data["created_at_unix"])
- return true
-
-
-func _save_atomic() -> bool:
- var data: Dictionary = {
- "format_version": FORMAT_VERSION,
- "profile_id": profile_id,
- "display_name": display_name,
- "voice_id": voice_id,
- "speech_speed_id": speech_speed_id,
- "call_id": call_id,
- "sample_set_id": sample_set_id,
- "created_at_unix": created_at_unix,
- }
- var result := PortableFileGuard.write_guarded(
- _profile_path,
- JSON.stringify(data, "\t").to_utf8_buffer(),
- _expected_hash,
- _data_root.conflict_directory(),
- _data_root.device_id,
- )
- if bool(result.get("conflict", false)):
- _data_root.report_conflict(
- str(result.get("message", "")),
- str(result.get("conflict_path", "")),
- )
- if bool(result.get("ok", false)):
- _expected_hash = str(result["hash"])
- return bool(result.get("ok", false))
-
-
-func _recover_interrupted_write() -> void:
- if FileAccess.file_exists(_profile_path):
- _remove_if_present(_temp_path())
- _remove_if_present(_backup_path())
- return
- if FileAccess.file_exists(_backup_path()):
- _rename(_backup_path(), _profile_path)
- _remove_if_present(_temp_path())
-
-
-func _rename(from_path: String, to_path: String) -> bool:
- return DirAccess.rename_absolute(
- ProjectSettings.globalize_path(from_path),
- ProjectSettings.globalize_path(to_path)
- ) == OK
-
-
-func _remove_if_present(path: String) -> bool:
- return (
- not FileAccess.file_exists(path)
- or DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) == OK
- )
diff --git a/network/network_profile_preferences.gd.uid b/network/network_profile_preferences.gd.uid
deleted file mode 100644
index 8e27e50..0000000
--- a/network/network_profile_preferences.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://db8ua5psoql0w
diff --git a/network/network_profile_protocol.gd b/network/network_profile_protocol.gd
deleted file mode 100644
index 5c1e879..0000000
--- a/network/network_profile_protocol.gd
+++ /dev/null
@@ -1,84 +0,0 @@
-class_name NetworkProfileProtocol
-extends RefCounted
-
-const RELIABLE_CHANNEL: int = 0
-const MAX_SUGGESTIONS: int = 3
-const MAX_REQUEST_ID_LENGTH: int = 64
-
-
-static func valid_request_id(value: Variant) -> bool:
- return (
- typeof(value) == TYPE_STRING
- and not str(value).is_empty()
- and str(value).length() <= MAX_REQUEST_ID_LENGTH
- )
-
-
-static func valid_snapshot(value: Variant) -> bool:
- return CharacterCustomizationCatalog.validate_snapshot(value)
-
-
-static func valid_check_request(data: Variant) -> bool:
- return (
- typeof(data) == TYPE_DICTIONARY
- and valid_request_id(data.get("request_id"))
- and typeof(data.get("session_id")) == TYPE_STRING
- and typeof(data.get("display_name")) == TYPE_STRING
- and NetworkProfilePreferences.is_valid_display_name(data["display_name"])
- )
-
-
-static func valid_apply_request(data: Variant) -> bool:
- return (
- valid_check_request(data)
- and typeof(data.get("appearance")) == TYPE_DICTIONARY
- and valid_snapshot(data["appearance"])
- and typeof(data.get("use_anyway")) == TYPE_BOOL
- and NetworkIdentityCrypto.valid_fingerprint(
- data.get("sender_fingerprint")
- )
- and typeof(data.get("sender_signature")) == TYPE_PACKED_BYTE_ARRAY
- )
-
-
-static func valid_appearance_preview_request(data: Variant) -> bool:
- return (
- typeof(data) == TYPE_DICTIONARY
- and valid_request_id(data.get("request_id"))
- and typeof(data.get("session_id")) == TYPE_STRING
- and not str(data.get("session_id", "")).is_empty()
- and typeof(data.get("appearance")) == TYPE_DICTIONARY
- and valid_snapshot(data["appearance"])
- and NetworkIdentityCrypto.valid_fingerprint(
- data.get("sender_fingerprint")
- )
- and typeof(data.get("sender_signature")) == TYPE_PACKED_BYTE_ARRAY
- )
-
-
-static func signature_fields(data: Dictionary) -> Array:
- var appearance: Dictionary = data.get("appearance", {})
- var result: Array = [
- str(data.get("session_id", "")),
- str(data.get("request_id", "")),
- str(data.get("sender_fingerprint", "")),
- str(data.get("display_name", "")),
- ]
- result.append_array(
- CharacterCustomizationCatalog.appearance_signature_values(appearance)
- )
- result.append(bool(data.get("use_anyway", false)))
- return result
-
-
-static func appearance_preview_signature_fields(data: Dictionary) -> Array:
- var appearance: Dictionary = data.get("appearance", {})
- var result: Array = [
- str(data.get("session_id", "")),
- str(data.get("request_id", "")),
- str(data.get("sender_fingerprint", "")),
- ]
- result.append_array(
- CharacterCustomizationCatalog.appearance_signature_values(appearance)
- )
- return result
diff --git a/network/network_profile_protocol.gd.uid b/network/network_profile_protocol.gd.uid
deleted file mode 100644
index 8a5943c..0000000
--- a/network/network_profile_protocol.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dhv483bfl4get
diff --git a/network/network_profile_service.gd b/network/network_profile_service.gd
deleted file mode 100644
index ff967f1..0000000
--- a/network/network_profile_service.gd
+++ /dev/null
@@ -1,730 +0,0 @@
-class_name NetworkProfileService
-extends Node
-
-const VoiceProfilesType = preload(
- "res://player/animalese_voice_profiles.gd"
-)
-
-signal conflict_result(
- request_id: String,
- has_conflict: bool,
- suggestions: PackedStringArray,
-)
-signal apply_finished(accepted: bool, message: String)
-signal profile_snapshot_changed(
- peer_id: int,
- display_name: String,
- appearance: Dictionary,
-)
-
-var _session: NetworkSession
-var _preferences: NetworkProfilePreferences
-var _appearance_store: PlayerAppearanceStore
-var _spawn_service: PlayerSpawnService
-var _pending_apply: Dictionary[String, Dictionary] = {}
-var _pending_voice_ids: Dictionary[String, String] = {}
-var _pending_speech_speed_ids: Dictionary[String, String] = {}
-var _pending_call_ids: Dictionary[String, String] = {}
-var _pending_sample_set_ids: Dictionary[String, String] = {}
-var _host_pending_apply: Dictionary[String, Dictionary] = {}
-var _latest_check_id: String = ""
-var _latest_check_name: String = ""
-
-
-func setup(
- session: NetworkSession,
- preferences: NetworkProfilePreferences,
- appearance_store: PlayerAppearanceStore,
- spawn_service: PlayerSpawnService,
-) -> void:
- _session = session
- _preferences = preferences
- _appearance_store = appearance_store
- _spawn_service = spawn_service
- _appearance_store.load_preferences()
- _session.peer_authenticated.connect(_on_peer_authenticated)
- _session.peer_removed.connect(_on_peer_removed)
- _session.peer_display_name_changed.connect(_on_peer_display_name_changed)
- _session.join_authenticated.connect(_on_join_authenticated)
- _session.state_changed.connect(_on_session_state_changed)
- _apply_to_avatar(1, _appearance_store.get_snapshot())
- _apply_local_voice_to_avatar()
-
-
-func reload_persisted_profile() -> bool:
- if (
- not _preferences.load_or_create()
- or not _appearance_store.load_preferences()
- ):
- return false
- _apply_to_avatar(1, _appearance_store.get_snapshot())
- _apply_local_voice_to_avatar()
- return true
-
-
-func get_persisted_name() -> String:
- return _preferences.display_name
-
-
-func get_persisted_appearance() -> Dictionary:
- return _appearance_store.get_snapshot()
-
-
-func get_persisted_voice_id() -> String:
- return _preferences.voice_id
-
-
-func get_persisted_speech_speed_id() -> String:
- return _preferences.speech_speed_id
-
-
-func get_persisted_call_id() -> String:
- return _preferences.call_id
-
-
-func get_persisted_sample_set_id() -> String:
- return _preferences.sample_set_id
-
-
-func get_identity_fingerprint() -> String:
- return (
- _session.get_local_identity_fingerprint()
- if _session != null else ""
- )
-
-
-func preview_appearance(appearance: Dictionary) -> bool:
- if not CharacterCustomizationCatalog.validate_snapshot(appearance):
- return false
- var snapshot := CharacterCustomizationCatalog.sanitized_snapshot(appearance)
- var local_peer_id: int = (
- _session.get_local_peer_id() if _session != null else 1
- )
- _apply_to_avatar(local_peer_id, snapshot)
- if _session == null or not _session.is_gameplay_session_active():
- return true
- _session.apply_canonical_profile(
- local_peer_id,
- _preferences.display_name,
- snapshot,
- )
- var authorization := _make_appearance_preview_authorization(snapshot)
- var local_record := _session.get_peer_record(local_peer_id)
- if local_record != null:
- local_record.profile_authorization = authorization.duplicate(true)
- if _session.is_host():
- _broadcast_appearance_preview_to_supported_peers(
- local_peer_id,
- snapshot,
- )
- elif _session.supports_server_capability(
- NetworkProtocol.APPEARANCE_PREVIEW_CAPABILITY
- ):
- submit_appearance_preview.rpc_id(1, authorization)
- return true
-
-
-func _make_appearance_preview_authorization(
- appearance: Dictionary,
-) -> Dictionary:
- var request: Dictionary = {
- "domain": "appearance_preview",
- "request_id": _new_id(),
- "session_id": _session.get_session_id(),
- "appearance": appearance.duplicate(true),
- "sender_fingerprint": _session.get_local_identity_fingerprint(),
- }
- request["sender_signature"] = _session.sign_local_action(
- "appearance_preview",
- NetworkProfileProtocol.appearance_preview_signature_fields(request),
- )
- return request
-
-
-func restore_persisted_appearance() -> void:
- preview_appearance(_appearance_store.get_snapshot())
-
-
-func request_name_check(display_name: String) -> String:
- var request_id := _new_id()
- _latest_check_id = request_id
- var clean_name := display_name.strip_edges()
- _latest_check_name = clean_name
- if not NetworkProfilePreferences.is_valid_display_name(clean_name):
- conflict_result.emit(request_id, false, PackedStringArray())
- return request_id
- if _session == null or not _session.is_gameplay_session_active():
- conflict_result.emit(request_id, false, PackedStringArray())
- elif _session.is_host():
- var result := _make_conflict_result(
- _session.get_local_peer_id(), clean_name, request_id
- )
- _emit_conflict_result(result)
- elif _session.supports_server_capability(&"profile_v1"):
- submit_name_check.rpc_id(1, {
- "request_id": request_id,
- "session_id": _session.get_session_id(),
- "display_name": clean_name,
- })
- else:
- conflict_result.emit(request_id, false, PackedStringArray())
- return request_id
-
-
-func apply_profile(
- display_name: String,
- appearance: Dictionary,
- use_anyway: bool,
- voice_id: String = VoiceProfilesType.DEFAULT_ID,
- speech_speed_id: String = VoiceProfilesType.DEFAULT_SPEED_ID,
- call_id: String = VoiceProfilesType.DEFAULT_CALL_ID,
- sample_set_id: String = VoiceProfilesType.DEFAULT_SAMPLE_SET_ID,
-) -> bool:
- var clean_name := display_name.strip_edges()
- if (
- not NetworkProfilePreferences.is_valid_display_name(clean_name)
- or not CharacterCustomizationCatalog.validate_snapshot(appearance)
- or not VoiceProfilesType.is_valid(voice_id)
- or not VoiceProfilesType.is_valid_speed(speech_speed_id)
- or not VoiceProfilesType.is_valid_call(call_id)
- or not VoiceProfilesType.is_valid_sample_set(sample_set_id)
- ):
- apply_finished.emit(false, "Check the player name and appearance choices.")
- return false
- var request_id := _new_id()
- var request := {
- "request_id": request_id,
- "session_id": _session.get_session_id() if _session != null else "",
- "display_name": clean_name,
- "appearance": appearance.duplicate(true),
- "use_anyway": use_anyway,
- "sender_fingerprint": _session.get_local_identity_fingerprint(),
- }
- request["sender_signature"] = _session.sign_local_action(
- "profile_update", NetworkProfileProtocol.signature_fields(request)
- )
- _pending_apply[request_id] = request
- _pending_voice_ids[request_id] = voice_id
- _pending_speech_speed_ids[request_id] = speech_speed_id
- _pending_call_ids[request_id] = call_id
- _pending_sample_set_ids[request_id] = sample_set_id
- if _session == null or not _session.is_gameplay_session_active():
- _apply_local_result(request_id, true, "", false, PackedStringArray())
- elif _session.is_host():
- _process_apply_request(_session.get_local_peer_id(), request)
- elif _session.supports_server_capability(&"profile_v1"):
- submit_profile_apply.rpc_id(1, request)
- else:
- _apply_local_result(request_id, true, "", false, PackedStringArray())
- return true
-
-
-@rpc("any_peer", "call_remote", "reliable", NetworkProfileProtocol.RELIABLE_CHANNEL)
-func submit_name_check(data: Dictionary) -> void:
- var sender_id := multiplayer.get_remote_sender_id()
- if (
- not _session.is_host()
- or not _session.is_authenticated_peer(sender_id)
- or not NetworkProfileProtocol.valid_check_request(data)
- or str(data["session_id"]) != _session.get_session_id()
- ):
- return
- receive_name_check.rpc_id(
- sender_id,
- _make_conflict_result(sender_id, str(data["display_name"]), str(data["request_id"])),
- )
-
-
-@rpc("authority", "call_remote", "reliable", NetworkProfileProtocol.RELIABLE_CHANNEL)
-func receive_name_check(data: Dictionary) -> void:
- if (
- typeof(data.get("request_id")) != TYPE_STRING
- or typeof(data.get("has_conflict")) != TYPE_BOOL
- or typeof(data.get("suggestions")) != TYPE_ARRAY
- ):
- return
- _emit_conflict_result(data)
-
-
-@rpc("any_peer", "call_remote", "reliable", NetworkProfileProtocol.RELIABLE_CHANNEL)
-func submit_profile_apply(data: Dictionary) -> void:
- var sender_id := multiplayer.get_remote_sender_id()
- if (
- not _session.is_host()
- or not _session.is_authenticated_peer(sender_id)
- or not NetworkProfileProtocol.valid_apply_request(data)
- or str(data["session_id"]) != _session.get_session_id()
- ):
- return
- var record := _session.get_peer_record(sender_id)
- if (
- record == null
- or record.identity_fingerprint != str(data["sender_fingerprint"])
- or not _session.verify_peer_action(
- sender_id,
- "profile_update",
- NetworkProfileProtocol.signature_fields(data),
- data["sender_signature"],
- )
- ):
- return
- _process_apply_request(sender_id, data)
-
-
-@rpc("any_peer", "call_remote", "reliable", NetworkProfileProtocol.RELIABLE_CHANNEL)
-func submit_appearance_preview(data: Dictionary) -> void:
- var sender_id := multiplayer.get_remote_sender_id()
- if (
- not _session.is_host()
- or not _session.is_authenticated_peer(sender_id)
- or not NetworkProfileProtocol.valid_appearance_preview_request(data)
- or str(data["session_id"]) != _session.get_session_id()
- ):
- return
- var record := _session.get_peer_record(sender_id)
- if (
- record == null
- or record.identity_fingerprint != str(data["sender_fingerprint"])
- or not _session.verify_peer_action(
- sender_id,
- "appearance_preview",
- NetworkProfileProtocol.appearance_preview_signature_fields(data),
- data["sender_signature"],
- )
- ):
- return
- var appearance := CharacterCustomizationCatalog.sanitized_snapshot(
- data["appearance"]
- )
- var authorization := data.duplicate(true)
- authorization["domain"] = "appearance_preview"
- record.profile_authorization = authorization
- _session.apply_canonical_profile(
- sender_id,
- record.display_name,
- appearance,
- )
- _apply_to_avatar(sender_id, appearance)
- _broadcast_appearance_preview_to_supported_peers(sender_id, appearance)
-
-
-func _process_apply_request(peer_id: int, data: Dictionary) -> void:
- var display_name: String = str(data["display_name"]).strip_edges()
- var conflict: bool = _name_conflicts(peer_id, display_name)
- var accepted: bool = not conflict or bool(data["use_anyway"])
- var suggestions: PackedStringArray = (
- _make_suggestions(peer_id, display_name)
- if conflict
- else PackedStringArray()
- )
- if peer_id == _session.get_local_peer_id():
- _apply_local_result(
- str(data["request_id"]), accepted, "", conflict, suggestions
- )
- else:
- if accepted:
- _host_pending_apply[str(data["request_id"])] = {
- "peer_id": peer_id,
- "display_name": display_name,
- "appearance": Dictionary(data["appearance"]).duplicate(true),
- "authorization": {
- "request_id": data["request_id"],
- "session_id": data["session_id"],
- "sender_fingerprint": data["sender_fingerprint"],
- "sender_signature": data["sender_signature"],
- "use_anyway": data["use_anyway"],
- },
- }
- receive_profile_result.rpc_id(peer_id, {
- "request_id": data["request_id"],
- "accepted": accepted,
- "conflict": conflict,
- "suggestions": Array(suggestions),
- "message": "" if accepted else "That name is already in use in this game.",
- })
-
-
-@rpc("any_peer", "call_remote", "reliable", NetworkProfileProtocol.RELIABLE_CHANNEL)
-func confirm_profile_saved(request_id: String) -> void:
- var sender_id := multiplayer.get_remote_sender_id()
- var pending: Dictionary = _host_pending_apply.get(request_id, {})
- if (
- not _session.is_host()
- or pending.is_empty()
- or int(pending["peer_id"]) != sender_id
- ):
- return
- _host_pending_apply.erase(request_id)
- var display_name: String = str(pending["display_name"])
- var appearance: Dictionary = Dictionary(pending["appearance"])
- _session.apply_canonical_profile(sender_id, display_name, appearance)
- var record: PeerRegistry.PeerRecord = _session.get_peer_record(sender_id)
- if record != null:
- record.profile_authorization = pending.get("authorization", {}).duplicate(true)
- _apply_to_avatar(sender_id, appearance)
- broadcast_profile_snapshot.rpc(
- sender_id,
- display_name,
- appearance,
- pending.get("authorization", {}),
- )
-
-
-@rpc("authority", "call_remote", "reliable", NetworkProfileProtocol.RELIABLE_CHANNEL)
-func receive_profile_result(data: Dictionary) -> void:
- if (
- typeof(data.get("request_id")) != TYPE_STRING
- or typeof(data.get("accepted")) != TYPE_BOOL
- or typeof(data.get("conflict")) != TYPE_BOOL
- or typeof(data.get("suggestions")) != TYPE_ARRAY
- ):
- return
- var suggestions := PackedStringArray(data["suggestions"])
- _apply_local_result(
- str(data["request_id"]),
- bool(data["accepted"]),
- str(data.get("message", "")),
- bool(data["conflict"]),
- suggestions,
- )
-
-
-func _apply_local_result(
- request_id: String,
- accepted: bool,
- message: String,
- conflict: bool,
- suggestions: PackedStringArray,
-) -> void:
- var request: Dictionary = _pending_apply.get(request_id, {})
- if request.is_empty():
- return
- if not accepted:
- _pending_apply.erase(request_id)
- _pending_voice_ids.erase(request_id)
- _pending_speech_speed_ids.erase(request_id)
- _pending_call_ids.erase(request_id)
- _pending_sample_set_ids.erase(request_id)
- conflict_result.emit(request_id, conflict, suggestions)
- apply_finished.emit(false, message)
- return
- var previous_name := _preferences.display_name
- var previous_voice_id := _preferences.voice_id
- var previous_speech_speed_id := _preferences.speech_speed_id
- var previous_call_id := _preferences.call_id
- var previous_sample_set_id := _preferences.sample_set_id
- var requested_voice_id: String = _pending_voice_ids.get(
- request_id,
- VoiceProfilesType.DEFAULT_ID,
- )
- var requested_speech_speed_id: String = _pending_speech_speed_ids.get(
- request_id,
- VoiceProfilesType.DEFAULT_SPEED_ID,
- )
- var requested_call_id: String = _pending_call_ids.get(
- request_id,
- VoiceProfilesType.DEFAULT_CALL_ID,
- )
- var requested_sample_set_id: String = _pending_sample_set_ids.get(
- request_id,
- VoiceProfilesType.DEFAULT_SAMPLE_SET_ID,
- )
- if not _preferences.set_profile_identity(
- str(request["display_name"]),
- requested_voice_id,
- requested_speech_speed_id,
- requested_call_id,
- requested_sample_set_id,
- ):
- _pending_apply.erase(request_id)
- _pending_voice_ids.erase(request_id)
- _pending_speech_speed_ids.erase(request_id)
- _pending_call_ids.erase(request_id)
- _pending_sample_set_ids.erase(request_id)
- apply_finished.emit(false, "Profile could not be saved.")
- return
- if not _appearance_store.save_snapshot(request["appearance"]):
- _preferences.set_profile_identity(
- previous_name,
- previous_voice_id,
- previous_speech_speed_id,
- previous_call_id,
- previous_sample_set_id,
- )
- _pending_apply.erase(request_id)
- _pending_voice_ids.erase(request_id)
- _pending_speech_speed_ids.erase(request_id)
- _pending_call_ids.erase(request_id)
- _pending_sample_set_ids.erase(request_id)
- apply_finished.emit(false, "Profile could not be saved.")
- return
- _pending_apply.erase(request_id)
- _pending_voice_ids.erase(request_id)
- _pending_speech_speed_ids.erase(request_id)
- _pending_call_ids.erase(request_id)
- _pending_sample_set_ids.erase(request_id)
- if _session != null:
- _session.set_local_appearance_snapshot(_appearance_store.get_snapshot())
- if _session != null and _session.is_gameplay_session_active():
- if _session.is_host():
- _session.apply_canonical_profile(
- _session.get_local_peer_id(),
- _preferences.display_name,
- _appearance_store.get_snapshot(),
- )
- var record := _session.get_peer_record(
- _session.get_local_peer_id()
- )
- if record != null:
- record.profile_authorization = request.duplicate(true)
- broadcast_profile_snapshot.rpc(
- _session.get_local_peer_id(),
- _preferences.display_name,
- _appearance_store.get_snapshot(),
- request,
- )
- elif _session.supports_server_capability(&"profile_v1"):
- confirm_profile_saved.rpc_id(1, request_id)
- elif _session.supports_server_capability(&"chat_v1"):
- _session.update_local_display_name(_preferences.display_name)
- _apply_to_avatar(
- _session.get_local_peer_id() if _session != null else 1,
- _appearance_store.get_snapshot(),
- )
- _apply_local_voice_to_avatar()
- apply_finished.emit(true, "Profile saved.")
-
-
-@rpc("authority", "call_remote", "reliable", NetworkProfileProtocol.RELIABLE_CHANNEL)
-func broadcast_profile_snapshot(
- peer_id: int,
- display_name: String,
- appearance: Dictionary,
- authorization: Dictionary = {},
-) -> void:
- if (
- not NetworkProfilePreferences.is_valid_display_name(display_name)
- or not CharacterCustomizationCatalog.validate_snapshot(appearance)
- ):
- return
- if (
- not authorization.is_empty()
- and not _session.verify_profile_authorization(
- peer_id,
- display_name,
- appearance,
- authorization,
- )
- ):
- return
- _session.apply_canonical_profile(peer_id, display_name, appearance)
- _apply_to_avatar(peer_id, appearance)
- profile_snapshot_changed.emit(peer_id, display_name, appearance.duplicate(true))
-
-
-@rpc("authority", "call_remote", "reliable", NetworkProfileProtocol.RELIABLE_CHANNEL)
-func broadcast_appearance_preview(
- peer_id: int,
- appearance: Dictionary,
-) -> void:
- if not CharacterCustomizationCatalog.validate_snapshot(appearance):
- return
- var record := _session.get_peer_record(peer_id)
- if record == null:
- return
- var snapshot := CharacterCustomizationCatalog.sanitized_snapshot(appearance)
- _session.apply_canonical_profile(peer_id, record.display_name, snapshot)
- _apply_to_avatar(peer_id, snapshot)
- profile_snapshot_changed.emit(
- peer_id,
- record.display_name,
- snapshot.duplicate(true),
- )
-
-
-func _broadcast_appearance_preview_to_supported_peers(
- peer_id: int,
- appearance: Dictionary,
-) -> void:
- if _session == null or not _session.is_host():
- return
- for recipient_id: int in _session.get_authenticated_peer_ids():
- if (
- recipient_id == _session.get_local_peer_id()
- or not _session.peer_supports_capability(
- recipient_id,
- NetworkProtocol.APPEARANCE_PREVIEW_CAPABILITY,
- )
- ):
- continue
- broadcast_appearance_preview.rpc_id(
- recipient_id,
- peer_id,
- appearance,
- )
-
-
-func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
- if not _session.is_host():
- return
- for existing_id: int in _session.get_authenticated_peer_ids():
- var record := _session.get_peer_record(existing_id)
- if record == null:
- continue
- broadcast_profile_snapshot.rpc_id(
- peer_id,
- existing_id,
- record.display_name,
- record.appearance_snapshot,
- record.profile_authorization,
- )
- if _session.peer_supports_capability(
- peer_id,
- NetworkProtocol.APPEARANCE_PREVIEW_CAPABILITY,
- ):
- broadcast_appearance_preview.rpc_id(
- peer_id,
- existing_id,
- record.appearance_snapshot,
- )
-
-
-func _on_join_authenticated() -> void:
- _session.set_local_appearance_snapshot(_appearance_store.get_snapshot())
- _apply_to_avatar(
- _session.get_local_peer_id(), _appearance_store.get_snapshot()
- )
- _apply_local_voice_to_avatar()
-
-
-func _on_peer_removed(_peer_id: int) -> void:
- for request_id: String in _host_pending_apply.keys():
- if int(_host_pending_apply[request_id].get("peer_id", 0)) == _peer_id:
- _host_pending_apply.erase(request_id)
- call_deferred("_refresh_latest_check")
-
-
-func _on_peer_display_name_changed(
- _peer_id: int,
- _display_name: String,
-) -> void:
- call_deferred("_refresh_latest_check")
-
-
-func _refresh_latest_check() -> void:
- if (
- _latest_check_name.is_empty()
- or _session == null
- or not _session.is_gameplay_session_active()
- ):
- return
- var peer := multiplayer.multiplayer_peer
- if (
- _session.is_joined_client()
- and (
- peer == null
- or peer.get_connection_status()
- != MultiplayerPeer.CONNECTION_CONNECTED
- )
- ):
- return
- request_name_check(_latest_check_name)
-
-
-func _on_session_state_changed(state: NetworkSession.State) -> void:
- if state == NetworkSession.State.INACTIVE:
- _pending_apply.clear()
- _pending_voice_ids.clear()
- _pending_speech_speed_ids.clear()
- _pending_call_ids.clear()
- _pending_sample_set_ids.clear()
- _host_pending_apply.clear()
- _latest_check_id = ""
- _latest_check_name = ""
- _session.set_local_appearance_snapshot(_appearance_store.get_snapshot())
- _apply_to_avatar(1, _appearance_store.get_snapshot())
- return
- if state in [
- NetworkSession.State.PRIVATE_HOST,
- NetworkSession.State.OPEN_HOST,
- NetworkSession.State.JOINED_CLIENT,
- ]:
- var local_peer_id := _session.get_local_peer_id()
- _apply_to_avatar(local_peer_id, _appearance_store.get_snapshot())
- _apply_local_voice_to_avatar()
-
-
-func _emit_conflict_result(data: Dictionary) -> void:
- var request_id := str(data["request_id"])
- if request_id != _latest_check_id:
- return
- conflict_result.emit(
- request_id,
- bool(data["has_conflict"]),
- PackedStringArray(data["suggestions"]),
- )
-
-
-func _make_conflict_result(
- peer_id: int,
- display_name: String,
- request_id: String,
-) -> Dictionary:
- var conflict := _name_conflicts(peer_id, display_name)
- return {
- "request_id": request_id,
- "has_conflict": conflict,
- "suggestions": Array(
- _make_suggestions(peer_id, display_name)
- if conflict else PackedStringArray()
- ),
- }
-
-
-func _name_conflicts(peer_id: int, display_name: String) -> bool:
- var normalized := display_name.strip_edges().to_lower()
- for other_id: int in _session.get_authenticated_peer_ids():
- if other_id == peer_id:
- continue
- var record := _session.get_peer_record(other_id)
- if record != null and record.display_name.strip_edges().to_lower() == normalized:
- return true
- return false
-
-
-func _make_suggestions(peer_id: int, base_name: String) -> PackedStringArray:
- var result := PackedStringArray()
- var suffix := 2
- while result.size() < NetworkProfileProtocol.MAX_SUGGESTIONS and suffix < 1000:
- var suffix_text := " %d" % suffix
- var candidate := base_name.left(
- NetworkProtocol.MAX_DISPLAY_NAME_LENGTH - suffix_text.length()
- ) + suffix_text
- if not _name_conflicts(peer_id, candidate) and candidate not in result:
- result.append(candidate)
- suffix += 1
- return result
-
-
-func _apply_to_avatar(peer_id: int, appearance: Dictionary) -> void:
- if _spawn_service == null:
- return
- var avatar := _spawn_service.get_avatar(peer_id)
- if avatar != null:
- avatar.apply_appearance_snapshot(appearance)
-
-
-func _apply_local_voice_to_avatar() -> void:
- if _spawn_service == null:
- return
- var local_peer_id := (
- _session.get_local_peer_id() if _session != null else 1
- )
- var avatar := _spawn_service.get_avatar(local_peer_id)
- if avatar != null:
- avatar.apply_animalese_voice_id(_preferences.voice_id)
- avatar.apply_animalese_sample_set_id(_preferences.sample_set_id)
-
-
-func _new_id() -> String:
- return Crypto.new().generate_random_bytes(16).hex_encode()
diff --git a/network/network_profile_service.gd.uid b/network/network_profile_service.gd.uid
deleted file mode 100644
index 4b3fcf7..0000000
--- a/network/network_profile_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dc80t6nmksvm2
diff --git a/network/network_protocol.gd b/network/network_protocol.gd
deleted file mode 100644
index 49aca95..0000000
--- a/network/network_protocol.gd
+++ /dev/null
@@ -1,393 +0,0 @@
-class_name NetworkProtocol
-extends RefCounted
-
-const WorldLayoutType = preload("res://world/world_layout.gd")
-
-const PROTOCOL_VERSION: int = 12
-const GAME_BUILD: String = "prealpha"
-const MAX_GAME_VERSION_LENGTH: int = 64
-const MAX_DISPLAY_NAME_LENGTH: int = 24
-const MAX_PROFILE_ID_LENGTH: int = 96
-const MAX_NONCE_LENGTH: int = 96
-const MAX_PUBLIC_KEY_LENGTH: int = 8192
-const MAX_SIGNATURE_LENGTH: int = 2048
-# ENet channels: 0 reliable lifecycle, 1 movement input, 2 movement
-# snapshots, 3 fishing input, 4 fishing snapshots, 5 reliable sales,
-# 6 reliable shop transactions, 7 reliable item/equipment,
-# 8 reliable ordered session chat, 9 reliable private session mail,
-# 10 observer-only fishing summaries, 11 reliable movement animation state,
-# 12 reliable fish showcase state, 13 reliable world-spawn events,
-# 14 reliable artwork state, 15 world-spawn snapshots, 16 reliable fishing
-# lifecycle, and 17 reliable portable-home state. Bulk world/art payloads must
-# not head-of-line block held items, fishing, animation, or home transitions.
-const SALE_RELIABLE_CHANNEL: int = 5
-const SHOP_RELIABLE_CHANNEL: int = 6
-const ITEM_RELIABLE_CHANNEL: int = 7
-const CHAT_RELIABLE_CHANNEL: int = 8
-const MAIL_RELIABLE_CHANNEL: int = 9
-const MOVEMENT_ANIMATION_CHANNEL: int = 11
-const SHOWCASE_RELIABLE_CHANNEL: int = 12
-const WORLD_SPAWN_RELIABLE_CHANNEL: int = 13
-const DRAWING_RELIABLE_CHANNEL: int = 14
-const FISHING_RELIABLE_CHANNEL: int = 16
-const HOME_RELIABLE_CHANNEL: int = 17
-const ENET_CHANNEL_COUNT: int = 18
-const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v2"
-const ART_SHOP_CAPABILITY: String = "art_shop_v1"
-const BACKPACK_SHOP_CAPABILITY: String = "backpack_shop_v1"
-const WORLD_TIME_CAPABILITY: String = "world_time_v2"
-const WORLD_WEATHER_CAPABILITY: String = "world_weather_v1"
-const FISH_QUALITY_CAPABILITY: String = "fish_quality_v1"
-const JOBS_CAPABILITY: String = "jobs_v1"
-const WORLD_SPAWN_CAPABILITY: String = "world_spawn_envelope_v1"
-const APPEARANCE_PREVIEW_CAPABILITY: String = "appearance_preview_v1"
-const WORLD_GENERATION_CAPABILITY: String = "world_generation_v1"
-const WORLD_LAYOUT_CAPABILITY: String = "world_layout_v1"
-const FRIENDS_CAPABILITY: String = "friends_v1"
-const MOVEMENT_RECONCILIATION_CAPABILITY: String = "movement_reconciliation_v2"
-const FISHING_REPLICATION_CAPABILITY: String = "fishing_replication_v2"
-const HOME_CAPABILITY: String = "portable_home_v1"
-const DEFAULT_WORLD_SEED: int = 13001
-const MAX_WORLD_SEED: int = 2147483646
-
-enum RejectionCode {
- NONE,
- MALFORMED_HANDSHAKE,
- PROTOCOL_MISMATCH,
- SERVER_FULL,
- DUPLICATE_PROFILE,
- DUPLICATE_IDENTITY,
- INVALID_IDENTITY_PROOF,
- AUTHENTICATION_TIMEOUT,
- SERVER_SHUTTING_DOWN,
- UNSUPPORTED_CLIENT,
- BANNED,
- CLIENT_OUTDATED,
- SERVER_OUTDATED,
- VERSION_MISMATCH,
-}
-
-
-static func game_version() -> String:
- return str(ProjectSettings.get_setting(
- "application/config/version", "unknown"
- )).strip_edges()
-
-
-static func game_version_rejection(
- client_version: String,
- server_version: String,
-) -> RejectionCode:
- if client_version == server_version:
- return RejectionCode.NONE
- var comparison: int = _compare_game_versions(
- client_version, server_version
- )
- if comparison < 0:
- return RejectionCode.CLIENT_OUTDATED
- if comparison > 0:
- return RejectionCode.SERVER_OUTDATED
- return RejectionCode.VERSION_MISMATCH
-
-
-static func _compare_game_versions(first: String, second: String) -> int:
- var first_parts: PackedStringArray = first.split("-", true, 1)
- var second_parts: PackedStringArray = second.split("-", true, 1)
- var first_core: PackedStringArray = first_parts[0].split(".")
- var second_core: PackedStringArray = second_parts[0].split(".")
- for index: int in maxi(first_core.size(), second_core.size()):
- var first_number: int = (
- int(first_core[index])
- if index < first_core.size() and first_core[index].is_valid_int()
- else 0
- )
- var second_number: int = (
- int(second_core[index])
- if index < second_core.size() and second_core[index].is_valid_int()
- else 0
- )
- if first_number != second_number:
- return -1 if first_number < second_number else 1
- var first_rank: int = _prerelease_rank(
- first_parts[1] if first_parts.size() > 1 else ""
- )
- var second_rank: int = _prerelease_rank(
- second_parts[1] if second_parts.size() > 1 else ""
- )
- if first_rank != second_rank:
- return -1 if first_rank < second_rank else 1
- return 0
-
-
-static func _prerelease_rank(label: String) -> int:
- var normalized: String = label.to_lower()
- if normalized.is_empty():
- return 5
- if normalized.begins_with("dev"):
- return 0
- if normalized.begins_with("prealpha"):
- return 1
- if normalized.begins_with("alpha"):
- return 2
- if normalized.begins_with("beta"):
- return 3
- if normalized.begins_with("rc"):
- return 4
- return 2
-
-
-static func make_identity_hello(
- public_key: String,
- fingerprint: String,
- client_nonce: String,
- attempt_id: String,
-) -> Dictionary:
- return {
- "protocol_version": PROTOCOL_VERSION,
- "game_version": game_version(),
- "public_key": public_key,
- "fingerprint": fingerprint,
- "client_nonce": client_nonce,
- "attempt_id": attempt_id,
- "capability_flags": PackedStringArray(["identity_v1"]),
- }
-
-
-static func validate_identity_hello(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- return (
- typeof(value.get("protocol_version")) == TYPE_INT
- and typeof(value.get("game_version")) == TYPE_STRING
- and not str(value["game_version"]).is_empty()
- and str(value["game_version"]).length() <= MAX_GAME_VERSION_LENGTH
- and typeof(value.get("public_key")) == TYPE_STRING
- and str(value["public_key"]).to_utf8_buffer().size() <= MAX_PUBLIC_KEY_LENGTH
- and NetworkIdentityCrypto.valid_fingerprint(value.get("fingerprint"))
- and typeof(value.get("client_nonce")) == TYPE_STRING
- and str(value["client_nonce"]).length() == 64
- and typeof(value.get("attempt_id")) == TYPE_STRING
- and str(value["attempt_id"]).length() == 32
- and typeof(value.get("capability_flags")) in [
- TYPE_ARRAY, TYPE_PACKED_STRING_ARRAY
- ]
- )
-
-
-static func make_client_hello(
- profile_id: String,
- display_name: String,
- client_nonce: String,
- cosmetic_snapshot: Dictionary = {},
- identity_fingerprint: String = "",
- identity_signature: PackedByteArray = PackedByteArray(),
-) -> Dictionary:
- return {
- "protocol_version": PROTOCOL_VERSION,
- "game_version": game_version(),
- "game_build": GAME_BUILD,
- "local_profile_id": profile_id,
- "display_name": display_name,
- "client_nonce": client_nonce,
- "capability_flags": PackedStringArray([
- FISH_QUALITY_CAPABILITY,
- SURFACE_DRAWING_CAPABILITY,
- WORLD_TIME_CAPABILITY,
- WORLD_WEATHER_CAPABILITY,
- JOBS_CAPABILITY,
- WORLD_SPAWN_CAPABILITY,
- APPEARANCE_PREVIEW_CAPABILITY,
- BACKPACK_SHOP_CAPABILITY,
- WORLD_GENERATION_CAPABILITY,
- WORLD_LAYOUT_CAPABILITY,
- FRIENDS_CAPABILITY,
- MOVEMENT_RECONCILIATION_CAPABILITY,
- FISHING_REPLICATION_CAPABILITY,
- HOME_CAPABILITY,
- ]),
- "cosmetic_snapshot": cosmetic_snapshot,
- "identity_fingerprint": identity_fingerprint,
- "identity_signature": identity_signature,
- }
-
-
-static func validate_client_hello(data: Variant) -> String:
- if typeof(data) != TYPE_DICTIONARY:
- return "Handshake payload is not a dictionary."
- var payload: Dictionary = data
- for key: String in [
- "protocol_version",
- "game_version",
- "game_build",
- "local_profile_id",
- "display_name",
- "client_nonce",
- "capability_flags",
- "cosmetic_snapshot",
- "identity_fingerprint",
- "identity_signature",
- ]:
- if not payload.has(key):
- return "Handshake is missing %s." % key
- if typeof(payload["protocol_version"]) != TYPE_INT:
- return "Protocol version is invalid."
- if (
- typeof(payload["game_version"]) != TYPE_STRING
- or str(payload["game_version"]).is_empty()
- or str(payload["game_version"]).length() > MAX_GAME_VERSION_LENGTH
- ):
- return "Game version is invalid."
- if typeof(payload["game_build"]) != TYPE_STRING:
- return "Game build is invalid."
- if typeof(payload["local_profile_id"]) != TYPE_STRING:
- return "Profile ID is invalid."
- if typeof(payload["display_name"]) != TYPE_STRING:
- return "Display name is invalid."
- if typeof(payload["client_nonce"]) != TYPE_STRING:
- return "Client nonce is invalid."
- if typeof(payload["capability_flags"]) not in [
- TYPE_PACKED_STRING_ARRAY,
- TYPE_ARRAY,
- ]:
- return "Capabilities are invalid."
- var capabilities: Variant = payload["capability_flags"]
- if capabilities.size() > 32:
- return "Capabilities are invalid."
- for capability: Variant in capabilities:
- if (
- typeof(capability) not in [TYPE_STRING, TYPE_STRING_NAME]
- or str(capability).is_empty()
- or str(capability).length() > 64
- ):
- return "Capabilities are invalid."
- if typeof(payload["cosmetic_snapshot"]) != TYPE_DICTIONARY:
- return "Cosmetic snapshot is invalid."
- if (
- not NetworkIdentityCrypto.valid_fingerprint(
- payload["identity_fingerprint"]
- )
- or typeof(payload["identity_signature"]) != TYPE_PACKED_BYTE_ARRAY
- ):
- return "Profile identity proof is invalid."
- var profile_id: String = payload["local_profile_id"]
- var display_name: String = payload["display_name"]
- var nonce: String = payload["client_nonce"]
- if (
- profile_id.is_empty()
- or profile_id.length() > MAX_PROFILE_ID_LENGTH
- or display_name.is_empty()
- or display_name.length() > MAX_DISPLAY_NAME_LENGTH
- or nonce.is_empty()
- or nonce.length() > MAX_NONCE_LENGTH
- ):
- return "Handshake strings are outside allowed limits."
- return ""
-
-
-static func client_profile_fields(data: Dictionary) -> Array:
- var appearance: Dictionary = data.get("cosmetic_snapshot", {})
- var result: Array = [
- str(data.get("game_version", "")),
- str(data.get("client_nonce", "")),
- str(data.get("identity_fingerprint", "")),
- str(data.get("local_profile_id", "")),
- str(data.get("display_name", "")),
- ]
- result.append_array(
- CharacterCustomizationCatalog.appearance_signature_values(appearance)
- )
- return result
-
-
-static func make_server_hello(
- accepted: bool,
- rejection_code: RejectionCode,
- session_id: String,
- assigned_peer_id: int,
- player_count: int,
- max_players: int,
- server_display_name: String = "straywild",
- world_seed: int = DEFAULT_WORLD_SEED,
- world_layout: StringName = WorldLayoutType.GENERATED,
-) -> Dictionary:
- return {
- "accepted": accepted,
- "rejection_code": int(rejection_code),
- "protocol_version": PROTOCOL_VERSION,
- "game_version": game_version(),
- "session_id": session_id,
- "assigned_peer_id": assigned_peer_id,
- "server_display_name": server_display_name,
- "player_count": player_count,
- "max_players": max_players,
- "world_seed": world_seed,
- "world_layout": String(world_layout),
- "capability_flags": PackedStringArray([
- "movement_v1",
- "fishing_v1",
- "sale_v1",
- "shop_v1",
- ART_SHOP_CAPABILITY,
- BACKPACK_SHOP_CAPABILITY,
- "item_use_v1",
- "equipment_v1",
- "fish_showcase_v1",
- FISH_QUALITY_CAPABILITY,
- SURFACE_DRAWING_CAPABILITY,
- WORLD_TIME_CAPABILITY,
- WORLD_WEATHER_CAPABILITY,
- JOBS_CAPABILITY,
- WORLD_SPAWN_CAPABILITY,
- APPEARANCE_PREVIEW_CAPABILITY,
- WORLD_GENERATION_CAPABILITY,
- WORLD_LAYOUT_CAPABILITY,
- FRIENDS_CAPABILITY,
- MOVEMENT_RECONCILIATION_CAPABILITY,
- FISHING_REPLICATION_CAPABILITY,
- HOME_CAPABILITY,
- "chat_v1",
- "mail_v1",
- "profile_v1",
- "identity_v1",
- ]),
- }
-
-
-static func rejection_text(code: int) -> String:
- match code:
- RejectionCode.PROTOCOL_MISMATCH:
- return "The server uses a different network protocol."
- RejectionCode.SERVER_FULL:
- return "The server is full."
- RejectionCode.DUPLICATE_PROFILE:
- return "This legacy local profile is already connected."
- RejectionCode.DUPLICATE_IDENTITY:
- return "This player identity is already connected."
- RejectionCode.INVALID_IDENTITY_PROOF:
- return "Player identity authentication failed."
- RejectionCode.AUTHENTICATION_TIMEOUT:
- return "The server did not finish authentication."
- RejectionCode.SERVER_SHUTTING_DOWN:
- return "The server is shutting down."
- RejectionCode.UNSUPPORTED_CLIENT:
- return "This game build is not supported by the server."
- RejectionCode.BANNED:
- return "You are not permitted to join this server."
- RejectionCode.CLIENT_OUTDATED:
- return (
- "Your straywild version is out of date. "
- + "Update the game to join this server."
- )
- RejectionCode.SERVER_OUTDATED:
- return (
- "This server is out of date. "
- + "The host needs to update straywild."
- )
- RejectionCode.VERSION_MISMATCH:
- return (
- "This game and server use different straywild versions. "
- + "Update both to the latest release."
- )
- _:
- return "The server rejected the connection."
diff --git a/network/network_protocol.gd.uid b/network/network_protocol.gd.uid
deleted file mode 100644
index aecfaef..0000000
--- a/network/network_protocol.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dghw7u3pdrpsf
diff --git a/network/network_sale_protocol.gd b/network/network_sale_protocol.gd
deleted file mode 100644
index 190b718..0000000
--- a/network/network_sale_protocol.gd
+++ /dev/null
@@ -1,162 +0,0 @@
-class_name NetworkSaleProtocol
-extends RefCounted
-
-const CAPABILITY: StringName = &"sale_v1"
-const RELIABLE_CHANNEL: int = NetworkProtocol.SALE_RELIABLE_CHANNEL
-const MAX_ID_LENGTH: int = 96
-const MAX_CATCH_ID_LENGTH: int = 160
-const MAX_CATCHES_PER_REQUEST: int = 64
-const MAX_ITEM_STACKS_PER_REQUEST: int = 20
-const MAX_ITEM_QUANTITY: int = 999
-const MAX_MESSAGE_LENGTH: int = 160
-
-enum Rejection {
- NONE,
- MALFORMED,
- STALE_SESSION,
- UNAUTHENTICATED,
- ALREADY_PENDING,
- BUYER_UNAVAILABLE,
- TOO_FAR,
- INVALID_CATCH,
- FAVORITED,
- UNSUPPORTED,
-}
-
-
-static func validate_request(data: Variant) -> String:
- if typeof(data) != TYPE_DICTIONARY:
- return "Sale could not be completed."
- var payload: Dictionary = data
- for key: String in ["request_id", "session_id", "buyer_id", "catches"]:
- if not payload.has(key):
- return "Sale could not be completed."
- if (
- typeof(payload["request_id"]) != TYPE_STRING
- or typeof(payload["session_id"]) != TYPE_STRING
- or typeof(payload["buyer_id"]) not in [TYPE_STRING, TYPE_STRING_NAME]
- or typeof(payload["catches"]) != TYPE_ARRAY
- ):
- return "Sale could not be completed."
- var request_id: String = payload["request_id"]
- var session_id: String = payload["session_id"]
- var buyer_id: String = str(payload["buyer_id"])
- var catches: Array = payload["catches"]
- var items: Array = payload.get("items", [])
- if (
- request_id.is_empty()
- or request_id.length() > MAX_ID_LENGTH
- or session_id.is_empty()
- or session_id.length() > MAX_ID_LENGTH
- or buyer_id.is_empty()
- or buyer_id.length() > MAX_ID_LENGTH
- or typeof(items) != TYPE_ARRAY
- or (catches.is_empty() and items.is_empty())
- or catches.size() > MAX_CATCHES_PER_REQUEST
- or items.size() > MAX_ITEM_STACKS_PER_REQUEST
- ):
- return "Sale could not be completed."
- var seen: Dictionary[String, bool] = {}
- for value: Variant in catches:
- if typeof(value) != TYPE_DICTIONARY:
- return "Sale could not be completed."
- var evidence: Dictionary = value
- for key: String in [
- "catch_id", "fish_id", "weight_lb", "display_scale",
- "quality", "sale_value", "is_favorited",
- ]:
- if not evidence.has(key):
- return "Sale could not be completed."
- var catch_id: String = str(evidence["catch_id"])
- if (
- catch_id.is_empty()
- or catch_id.length() > MAX_CATCH_ID_LENGTH
- or seen.has(catch_id)
- or typeof(evidence["fish_id"]) not in [
- TYPE_STRING, TYPE_STRING_NAME
- ]
- or typeof(evidence["is_favorited"]) != TYPE_BOOL
- ):
- return "Sale could not be completed."
- seen[catch_id] = true
- var seen_items: Dictionary[String, bool] = {}
- for value: Variant in items:
- if typeof(value) != TYPE_DICTIONARY:
- return "Sale could not be completed."
- var item: Dictionary = value
- var item_id := str(item.get("item_id", ""))
- var quantity := int(item.get("quantity", 0))
- if (
- item_id.is_empty()
- or item_id.length() > MAX_ID_LENGTH
- or seen_items.has(item_id)
- or typeof(item.get("quantity")) != TYPE_INT
- or quantity < 1
- or quantity > MAX_ITEM_QUANTITY
- ):
- return "Sale could not be completed."
- seen_items[item_id] = true
- return ""
-
-
-static func validate_result(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var payload: Dictionary = data
- if not (
- typeof(payload.get("result_id")) == TYPE_STRING
- and not str(payload["result_id"]).is_empty()
- and str(payload["result_id"]).length() <= MAX_ID_LENGTH
- and typeof(payload.get("request_id")) == TYPE_STRING
- and not str(payload["request_id"]).is_empty()
- and str(payload["request_id"]).length() <= MAX_ID_LENGTH
- and typeof(payload.get("session_id")) == TYPE_STRING
- and typeof(payload.get("target_peer_id")) == TYPE_INT
- and typeof(payload.get("accepted")) == TYPE_BOOL
- and typeof(payload.get("catch_ids")) == TYPE_ARRAY
- and payload["catch_ids"].size() <= MAX_CATCHES_PER_REQUEST
- and typeof(payload.get("items", [])) == TYPE_ARRAY
- and payload.get("items", []).size() <= MAX_ITEM_STACKS_PER_REQUEST
- and typeof(payload.get("payout")) == TYPE_INT
- and int(payload["payout"]) >= 0
- and typeof(payload.get("base_value")) == TYPE_INT
- and int(payload["base_value"]) >= 0
- and typeof(payload.get("message")) == TYPE_STRING
- and str(payload["message"]).length() <= MAX_MESSAGE_LENGTH
- ):
- return false
- if payload.has("buyer_id") and (
- typeof(payload["buyer_id"]) not in [TYPE_STRING, TYPE_STRING_NAME]
- or str(payload["buyer_id"]).is_empty()
- or str(payload["buyer_id"]).length() > MAX_ID_LENGTH
- ):
- return false
- var seen: Dictionary[String, bool] = {}
- for value: Variant in payload["catch_ids"]:
- if typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME]:
- return false
- var catch_id: String = str(value)
- if (
- catch_id.is_empty()
- or catch_id.length() > MAX_CATCH_ID_LENGTH
- or seen.has(catch_id)
- ):
- return false
- seen[catch_id] = true
- var seen_items: Dictionary[String, bool] = {}
- for value: Variant in payload.get("items", []):
- if typeof(value) != TYPE_DICTIONARY:
- return false
- var item := value as Dictionary
- var item_id := str(item.get("item_id", ""))
- if (
- item_id.is_empty()
- or item_id.length() > MAX_ID_LENGTH
- or seen_items.has(item_id)
- or typeof(item.get("quantity")) != TYPE_INT
- or int(item["quantity"]) < 1
- or int(item["quantity"]) > MAX_ITEM_QUANTITY
- ):
- return false
- seen_items[item_id] = true
- return true
diff --git a/network/network_sale_protocol.gd.uid b/network/network_sale_protocol.gd.uid
deleted file mode 100644
index 58e991a..0000000
--- a/network/network_sale_protocol.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dsj0noi32pqeu
diff --git a/network/network_sale_service.gd b/network/network_sale_service.gd
deleted file mode 100644
index a80f221..0000000
--- a/network/network_sale_service.gd
+++ /dev/null
@@ -1,775 +0,0 @@
-class_name NetworkSaleService
-extends Node
-
-const FishCatchType = preload("res://fish/fish_catch.gd")
-const FishQualityType = preload("res://fish/fish_quality.gd")
-const FishDataType = preload("res://fish/fish_data.gd")
-const FishPoolType = preload("res://fish/fish_pool.gd")
-const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
-const FishSaleServiceType = preload("res://economy/fish_sale_service.gd")
-const FishSaleResultType = preload("res://economy/fish_sale_result.gd")
-const ItemDataType = preload("res://items/item_data.gd")
-const ItemCatalogType = preload("res://items/item_catalog.gd")
-const PlayerBagType = preload("res://inventory/player_bag.gd")
-const ItemResalePolicyType = preload("res://economy/item_resale_policy.gd")
-
-const MAX_LEDGER_ENTRIES_PER_PEER: int = 64
-const LOCAL_REQUEST_RETRY_SECONDS: float = 2.0
-const PELICAN_BUYER_ID: StringName = &"pelicans"
-const MAIN_SHOP_BUYER_ID: StringName = &"main_fishing_shop"
-
-signal local_sale_pending(request_id: String)
-signal local_sale_finished(
- request_id: String,
- accepted: bool,
- message: String,
- catch_ids: Array[StringName],
- payout: int,
-)
-
-var _session: NetworkSession
-var _spawn_service: PlayerSpawnService
-var _network_fishing: NetworkFishingService
-var _shop_interaction: FishingShopInteraction
-var _inventory: FishInventory
-var _bag: PlayerBagType
-var _item_catalog: ItemCatalogType
-var _wallet: PlayerWallet
-var _sale_service: FishSaleServiceType
-var _save_manager: PlayerSaveManager
-var _fish_catalog: FishPoolType
-var _buyers: Dictionary[StringName, FishBuyerProfileType] = {}
-var _request_ledgers: Dictionary[int, Dictionary] = {}
-var _pending_by_peer: Dictionary[int, String] = {}
-var _result_owners: Dictionary[String, int] = {}
-var _acknowledged_results: Dictionary[String, bool] = {}
-var _applied_results: Dictionary[String, bool] = {}
-var _received_results: Dictionary[String, bool] = {}
-var _pending_local_request_id: String = ""
-var _pending_local_request: Dictionary = {}
-var _local_request_retry_elapsed: float = 0.0
-var _pending_local_catch_ids: Array[StringName] = []
-var _pending_local_items: Array[Dictionary] = []
-var _pending_local_buyer_id: StringName
-var _reservations: PlayerAssetReservationService
-var _inventory_layout: PlayerInventoryLayout
-
-
-func _process(delta: float) -> void:
- if (
- _pending_local_request.is_empty()
- or _session == null
- or not _session.is_joined_client()
- ):
- _local_request_retry_elapsed = 0.0
- return
- _local_request_retry_elapsed += delta
- if _local_request_retry_elapsed < LOCAL_REQUEST_RETRY_SECONDS:
- return
- _local_request_retry_elapsed = 0.0
- # The request ID is stable and the host ledger is idempotent, so an
- # application-level retry cannot apply a sale twice. This covers the narrow
- # transition where a generated client has authenticated but is only just
- # resuming regular multiplayer polling.
- submit_sale_request.rpc_id(1, _pending_local_request)
-
-
-func setup(
- session: NetworkSession,
- spawn_service: PlayerSpawnService,
- network_fishing: NetworkFishingService,
- shop_interaction: FishingShopInteraction,
- inventory: FishInventory,
- bag: PlayerBagType,
- item_catalog: ItemCatalogType,
- wallet: PlayerWallet,
- sale_service: FishSaleServiceType,
- save_manager: PlayerSaveManager,
- fish_catalog: FishPoolType,
- buyers: Array[FishBuyerProfileType],
- reservations: PlayerAssetReservationService,
- inventory_layout: PlayerInventoryLayout = null,
-) -> void:
- _session = session
- _spawn_service = spawn_service
- _network_fishing = network_fishing
- _shop_interaction = shop_interaction
- _inventory = inventory
- _bag = bag
- _item_catalog = item_catalog
- _wallet = wallet
- _sale_service = sale_service
- _save_manager = save_manager
- _fish_catalog = fish_catalog
- _buyers.clear()
- for buyer: FishBuyerProfileType in buyers:
- if buyer != null and buyer.is_valid():
- _buyers[buyer.id] = buyer
- _reservations = reservations
- _inventory_layout = inventory_layout
- if not _session.peer_removed.is_connected(_on_peer_removed):
- _session.peer_removed.connect(_on_peer_removed)
- if not _session.state_changed.is_connected(_on_session_state_changed):
- _session.state_changed.connect(_on_session_state_changed)
-
-
-func set_shop_interaction(interaction: FishingShopInteraction) -> void:
- _shop_interaction = interaction
-
-
-func is_local_sale_pending() -> bool:
- return not _pending_local_request_id.is_empty()
-
-
-func can_request_sale(
- buyer_id: StringName = PELICAN_BUYER_ID,
-) -> bool:
- var buyer: FishBuyerProfileType = _buyers.get(buyer_id)
- return (
- buyer != null
- and buyer.is_valid()
- and _session != null
- and _session.is_gameplay_session_active()
- and (
- _session.is_host()
- or _session.supports_server_capability(
- NetworkSaleProtocol.CAPABILITY
- )
- )
- )
-
-
-func request_local_sale(
- catch_ids: Array[StringName],
- buyer_id: StringName = PELICAN_BUYER_ID,
-) -> String:
- var no_items: Dictionary[StringName, int] = {}
- return request_local_mixed_sale(catch_ids, no_items, buyer_id)
-
-
-func request_local_mixed_sale(
- catch_ids: Array[StringName],
- item_quantities: Dictionary[StringName, int],
- buyer_id: StringName = MAIN_SHOP_BUYER_ID,
-) -> String:
- for catch_id: StringName in catch_ids:
- if _reservations != null and _reservations.is_fish_reserved(catch_id):
- local_sale_finished.emit(
- "", false, "Reserved in a letter.", _empty_catch_ids(), 0
- )
- return ""
- if is_local_sale_pending():
- local_sale_finished.emit(
- "", false, "Selling…", _empty_catch_ids(), 0
- )
- return ""
- if not can_request_sale(buyer_id):
- local_sale_finished.emit(
- "",
- false,
- "Selling is not supported by this server.",
- _empty_catch_ids(),
- 0
- )
- return ""
- if (
- (catch_ids.is_empty() and item_quantities.is_empty())
- or _inventory == null
- or _bag == null
- or _item_catalog == null
- ):
- local_sale_finished.emit(
- "", false, "Sale could not be completed.", _empty_catch_ids(), 0
- )
- return ""
- var buyer: FishBuyerProfileType = _buyers.get(buyer_id)
- if buyer == null or not buyer.is_valid():
- local_sale_finished.emit(
- "", false, "The buyer is unavailable.", _empty_catch_ids(), 0
- )
- return ""
- var evidence: Array[Dictionary] = []
- for catch_id: StringName in catch_ids:
- var fish_catch: FishCatch = _inventory.get_catch_by_id(catch_id)
- if fish_catch == null:
- local_sale_finished.emit(
- "", false, "That catch is no longer available.", _empty_catch_ids(), 0
- )
- return ""
- evidence.append(fish_catch.to_network_dict())
- var item_evidence: Array[Dictionary] = []
- var item_ids: Array[StringName] = []
- item_ids.assign(item_quantities.keys())
- item_ids.sort_custom(
- func(left: StringName, right: StringName) -> bool:
- return str(left) < str(right)
- )
- for item_id: StringName in item_ids:
- var quantity := int(item_quantities.get(item_id, 0))
- var item: ItemDataType = _item_catalog.get_item_by_id(item_id)
- if (
- quantity < 1
- or not ItemResalePolicyType.is_sellable(item)
- or quantity > _bag.get_quantity(item_id)
- or (
- _reservations != null
- and quantity > _reservations.get_available_item_quantity(item_id)
- )
- ):
- local_sale_finished.emit(
- "", false, "That item is no longer available.", _empty_catch_ids(), 0
- )
- return ""
- item_evidence.append({"item_id": str(item_id), "quantity": quantity})
- var request_id: String = _new_id("sale")
- var request: Dictionary = {
- "request_id": request_id,
- "session_id": _session.get_session_id(),
- "buyer_id": str(buyer.id),
- "catches": evidence,
- "items": item_evidence,
- }
- _pending_local_request_id = request_id
- _pending_local_request = request.duplicate(true)
- _local_request_retry_elapsed = 0.0
- _pending_local_catch_ids = catch_ids.duplicate()
- _pending_local_items = item_evidence.duplicate(true)
- _pending_local_buyer_id = buyer.id
- local_sale_pending.emit(request_id)
- if _session.is_host():
- _handle_sale_request(_session.get_local_peer_id(), request)
- else:
- submit_sale_request.rpc_id(1, request)
- return request_id
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- NetworkSaleProtocol.RELIABLE_CHANNEL
-)
-func submit_sale_request(data: Dictionary) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if (
- _session == null
- or not _session.is_host()
- or not _session.is_authenticated_peer(sender_id)
- ):
- return
- _handle_sale_request(sender_id, data)
-
-
-func _handle_sale_request(peer_id: int, data: Dictionary) -> void:
- var validation_error: String = NetworkSaleProtocol.validate_request(data)
- var request_id: String = str(data.get("request_id", ""))
- var ledger: Dictionary = _request_ledgers.get(peer_id, {})
- if not request_id.is_empty() and ledger.has(request_id):
- _send_result(peer_id, ledger[request_id])
- return
- if not validation_error.is_empty():
- _record_and_send(peer_id, _rejected_result(
- request_id, validation_error
- ))
- return
- if str(data["session_id"]) != _session.get_session_id():
- _record_and_send(peer_id, _rejected_result(
- request_id, "Sale could not be completed."
- ))
- return
- if (
- _pending_by_peer.has(peer_id)
- and _pending_by_peer[peer_id] != request_id
- ):
- _record_and_send(peer_id, _rejected_result(
- request_id, "A sale is already pending."
- ))
- return
- var buyer_id := StringName(str(data["buyer_id"]))
- var buyer: FishBuyerProfileType = _buyers.get(buyer_id)
- if buyer == null or not buyer.is_valid():
- _record_and_send(peer_id, _rejected_result(
- request_id, "The buyer is unavailable."
- ))
- return
- if (
- buyer_id == MAIN_SHOP_BUYER_ID
- and not _is_shop_available_for_peer(peer_id)
- ):
- _record_and_send(peer_id, _rejected_result(
- request_id, "Move closer to the fishing shop."
- ))
- return
- var result: Dictionary = _build_authoritative_result(
- peer_id,
- request_id,
- data["catches"],
- data.get("items", []),
- buyer,
- )
- _pending_by_peer[peer_id] = request_id
- _record_and_send(peer_id, result)
-
-
-func _build_authoritative_result(
- peer_id: int,
- request_id: String,
- evidence_values: Array,
- item_values: Array,
- buyer: FishBuyerProfileType,
-) -> Dictionary:
- if (
- buyer == null
- or (not evidence_values.is_empty() and _fish_catalog == null)
- or (not item_values.is_empty() and _item_catalog == null)
- ):
- return _rejected_result(
- request_id, "The buyer is unavailable."
- )
- var catch_ids: Array[String] = []
- var base_value: int = 0
- var payout: int = 0
- for value: Variant in evidence_values:
- var evidence: Dictionary = value
- var fish_id := StringName(str(evidence.get("fish_id", "")))
- var fish: FishDataType = _fish_catalog.get_fish_by_id(fish_id)
- var decoded: FishCatch = FishCatchType.from_network_dict(
- evidence, fish
- )
- if decoded == null:
- return _rejected_result(
- request_id, "Sale could not be completed."
- )
- if (
- not str(decoded.catch_id).begins_with("%s:" % fish_id)
- or decoded.weight_lb < fish.get_minimum_weight()
- or decoded.weight_lb > fish.get_maximum_weight()
- or decoded.sale_value != FishQualityType.apply_sale_value(
- fish.get_sale_value_for_weight(decoded.weight_lb),
- decoded.quality,
- )
- ):
- return _rejected_result(
- request_id, "Sale could not be completed."
- )
- if bool(evidence.get("is_favorited", false)):
- return _rejected_result(
- request_id, "Favorite catches cannot be sold."
- )
- var ordinary_value: int = fish.get_sale_value_for_weight(
- decoded.weight_lb
- )
- var offer: int = buyer.get_quality_offer(
- ordinary_value,
- decoded.quality,
- )
- if (
- offer < 0
- or base_value > 9223372036854775807 - decoded.sale_value
- or payout > 9223372036854775807 - offer
- ):
- return _rejected_result(
- request_id, "Sale could not be completed."
- )
- base_value += decoded.sale_value
- payout += offer
- catch_ids.append(str(decoded.catch_id))
- var items: Array[Dictionary] = []
- if not item_values.is_empty() and buyer.id != MAIN_SHOP_BUYER_ID:
- return _rejected_result(request_id, "This buyer only accepts catches.")
- for value: Variant in item_values:
- var evidence := value as Dictionary
- var item_id := StringName(str(evidence.get("item_id", "")))
- var quantity := int(evidence.get("quantity", 0))
- var item: ItemDataType = _item_catalog.get_item_by_id(item_id)
- var unit_value := ItemResalePolicyType.get_unit_value(item)
- if quantity < 1 or unit_value < 0:
- return _rejected_result(request_id, "That item cannot be sold.")
- var item_value := unit_value * quantity
- if (
- item_value < 0
- or base_value > 9223372036854775807 - item_value
- or payout > 9223372036854775807 - item_value
- ):
- return _rejected_result(request_id, "Sale could not be completed.")
- base_value += item_value
- payout += item_value
- items.append({"item_id": str(item_id), "quantity": quantity})
- return {
- "result_id": _new_id("sale_result"),
- "request_id": request_id,
- "session_id": _session.get_session_id(),
- "target_peer_id": peer_id,
- "buyer_id": str(buyer.id),
- "accepted": true,
- "catch_ids": catch_ids,
- "items": items,
- "payout": payout,
- "base_value": base_value,
- "message": "Sale complete.",
- }
-
-
-func _rejected_result(request_id: String, message: String) -> Dictionary:
- var safe_request_id: String = (
- request_id
- if (
- not request_id.is_empty()
- and request_id.length() <= NetworkSaleProtocol.MAX_ID_LENGTH
- )
- else "invalid"
- )
- return {
- "result_id": _new_id("sale_result"),
- "request_id": safe_request_id,
- "session_id": _session.get_session_id() if _session != null else "",
- "target_peer_id": 0,
- "accepted": false,
- "catch_ids": [],
- "items": [],
- "payout": 0,
- "base_value": 0,
- "message": message.left(NetworkSaleProtocol.MAX_MESSAGE_LENGTH),
- }
-
-
-func _record_and_send(peer_id: int, result: Dictionary) -> void:
- result["target_peer_id"] = peer_id
- var request_id: String = str(result["request_id"])
- var ledger: Dictionary = _request_ledgers.get(peer_id, {})
- ledger[request_id] = result.duplicate(true)
- while ledger.size() > MAX_LEDGER_ENTRIES_PER_PEER:
- ledger.erase(ledger.keys().front())
- _request_ledgers[peer_id] = ledger
- _result_owners[str(result["result_id"])] = peer_id
- _bound_dictionary(_result_owners)
- _send_result(peer_id, result)
-
-
-func _send_result(peer_id: int, result: Dictionary) -> void:
- if peer_id == _session.get_local_peer_id():
- _apply_sale_result(result)
- else:
- receive_sale_result.rpc_id(peer_id, result)
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- NetworkSaleProtocol.RELIABLE_CHANNEL
-)
-func receive_sale_result(data: Dictionary) -> void:
- _apply_sale_result(data)
-
-
-func _apply_sale_result(data: Dictionary) -> void:
- if (
- not NetworkSaleProtocol.validate_result(data)
- or _session == null
- or str(data["session_id"]) != _session.get_session_id()
- or int(data["target_peer_id"]) != _session.get_local_peer_id()
- ):
- return
- var result_id: String = data["result_id"]
- if _received_results.has(result_id):
- _acknowledge_result(
- data, _applied_results.has(result_id), ""
- )
- return
- if str(data["request_id"]) != _pending_local_request_id:
- return
- var result_buyer_id := StringName(str(
- data.get("buyer_id", _pending_local_buyer_id)
- ))
- if result_buyer_id != _pending_local_buyer_id:
- _fail_local_apply(data, "Sale could not be completed.")
- return
- if not bool(data["accepted"]):
- _received_results[result_id] = true
- _bound_dictionary(_received_results)
- _finish_local_sale(
- data["request_id"], false, str(data["message"]), [], 0
- )
- _acknowledge_result(data, false, str(data["message"]))
- return
- var catch_ids: Array[StringName] = []
- for value: Variant in data["catch_ids"]:
- if typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME]:
- _fail_local_apply(data, "Sale could not be completed.")
- return
- catch_ids.append(StringName(str(value)))
- if catch_ids != _pending_local_catch_ids:
- _fail_local_apply(data, "Sale could not be completed.")
- return
- var items: Array[Dictionary] = []
- for value: Variant in data.get("items", []):
- items.append((value as Dictionary).duplicate(true))
- if items != _pending_local_items:
- _fail_local_apply(data, "Sale could not be completed.")
- return
- for catch_id: StringName in catch_ids:
- if _reservations != null and _reservations.is_fish_reserved(catch_id):
- _fail_local_apply(data, "Reserved in a letter.")
- return
- var buyer: FishBuyerProfileType = _buyers.get(_pending_local_buyer_id)
- if buyer == null or not buyer.is_valid():
- _fail_local_apply(data, "The buyer is unavailable.")
- return
- var catch_payout: int = 0
- var catch_base_value: int = 0
- var preview: FishSaleResultType
- if not catch_ids.is_empty():
- preview = _sale_service.preview_batch(catch_ids, buyer)
- if preview != null and not preview.is_success():
- var message: String = (
- "Favorite catches cannot be sold."
- if preview.status == FishSaleResultType.Status.FAVORITED
- else "That catch is no longer available."
- )
- _fail_local_apply(data, message)
- return
- if preview != null:
- catch_payout = preview.payout
- catch_base_value = preview.base_value
- var item_payout: int = 0
- for record: Dictionary in items:
- var item_id := StringName(str(record.get("item_id", "")))
- var quantity := int(record.get("quantity", 0))
- var item: ItemDataType = _item_catalog.get_item_by_id(item_id)
- var unit_value := ItemResalePolicyType.get_unit_value(item)
- if (
- quantity < 1
- or unit_value < 0
- or quantity > _bag.get_quantity(item_id)
- or (
- _reservations != null
- and quantity > _reservations.get_available_item_quantity(item_id)
- )
- ):
- _fail_local_apply(data, "That item is no longer available.")
- return
- item_payout += unit_value * quantity
- if (
- catch_payout + item_payout != int(data["payout"])
- or catch_base_value + item_payout != int(data["base_value"])
- ):
- _fail_local_apply(data, "Sale could not be completed.")
- return
- var inventory_snapshot: Array[FishCatch] = _inventory.get_all_catches()
- var sequence_snapshot: int = _inventory.get_next_catch_sequence()
- var bag_snapshot := _bag.get_all_items()
- var layout_snapshot: Dictionary = (
- _inventory_layout.to_save_data()
- if _inventory_layout != null else {}
- )
- var wallet_snapshot: int = _wallet.get_balance()
- var applied: bool = true
- if not catch_ids.is_empty():
- applied = (
- _inventory.remove_catches_by_ids(catch_ids).size()
- == catch_ids.size()
- )
- if applied:
- for record: Dictionary in items:
- if not _bag.remove_item(
- StringName(str(record["item_id"])), int(record["quantity"])
- ):
- applied = false
- break
- if applied:
- applied = _wallet.credit(int(data["payout"]))
- if not applied or not _save_manager.save_if_dirty():
- _inventory.replace_all_catches(
- inventory_snapshot, sequence_snapshot
- )
- _bag.replace_all_items(bag_snapshot)
- if _inventory_layout != null:
- _inventory_layout.restore_from_save_data(layout_snapshot)
- _wallet.restore_balance(wallet_snapshot)
- _save_manager.save_if_dirty()
- _fail_local_apply(data, "Sale could not be completed.")
- return
- _applied_results[result_id] = true
- _bound_dictionary(_applied_results)
- _received_results[result_id] = true
- _bound_dictionary(_received_results)
- _finish_local_sale(
- data["request_id"],
- true,
- "Sale complete.",
- catch_ids,
- int(data["payout"])
- )
- _acknowledge_result(data, true, "")
-
-
-func _fail_local_apply(data: Dictionary, message: String) -> void:
- _finish_local_sale(
- data["request_id"], false, message, _empty_catch_ids(), 0
- )
- _acknowledge_result(data, false, message)
-
-
-func _finish_local_sale(
- request_id: String,
- accepted: bool,
- message: String,
- catch_ids: Array[StringName],
- payout: int,
-) -> void:
- _pending_local_request_id = ""
- _pending_local_request.clear()
- _local_request_retry_elapsed = 0.0
- _pending_local_catch_ids.clear()
- _pending_local_items.clear()
- _pending_local_buyer_id = StringName()
- local_sale_finished.emit(
- request_id, accepted, message, catch_ids, payout
- )
-
-
-func _acknowledge_result(
- data: Dictionary,
- applied: bool,
- message: String,
-) -> void:
- if _session.is_host():
- _handle_acknowledgement(
- _session.get_local_peer_id(),
- str(data["result_id"]),
- str(data["request_id"]),
- applied,
- message
- )
- else:
- acknowledge_sale_result.rpc_id(
- 1,
- str(data["result_id"]),
- str(data["request_id"]),
- applied,
- message.left(NetworkSaleProtocol.MAX_MESSAGE_LENGTH)
- )
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- NetworkSaleProtocol.RELIABLE_CHANNEL
-)
-func acknowledge_sale_result(
- result_id: String,
- request_id: String,
- applied: bool,
- message: String,
-) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if (
- _session == null
- or not _session.is_host()
- or not _session.is_authenticated_peer(sender_id)
- ):
- return
- _handle_acknowledgement(
- sender_id, result_id, request_id, applied, message
- )
-
-
-func _handle_acknowledgement(
- peer_id: int,
- result_id: String,
- request_id: String,
- _applied: bool,
- _message: String,
-) -> void:
- if (
- result_id.is_empty()
- or result_id.length() > NetworkSaleProtocol.MAX_ID_LENGTH
- or request_id.is_empty()
- or request_id.length() > NetworkSaleProtocol.MAX_ID_LENGTH
- or _message.length() > NetworkSaleProtocol.MAX_MESSAGE_LENGTH
- or _result_owners.get(result_id, 0) != peer_id
- ):
- return
- _acknowledged_results[result_id] = true
- _bound_dictionary(_acknowledged_results)
- if _pending_by_peer.get(peer_id, "") == request_id:
- _pending_by_peer.erase(peer_id)
-
-
-func _on_peer_removed(peer_id: int) -> void:
- _request_ledgers.erase(peer_id)
- _pending_by_peer.erase(peer_id)
- for result_id: String in _result_owners.keys():
- if _result_owners[result_id] == peer_id:
- _result_owners.erase(result_id)
- _acknowledged_results.erase(result_id)
-
-
-func _is_shop_available_for_peer(peer_id: int) -> bool:
- if (
- _session == null
- or not _session.is_host()
- or not _session.is_gameplay_session_active()
- or _spawn_service == null
- or _shop_interaction == null
- or not is_instance_valid(_shop_interaction)
- ):
- return false
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- return (
- avatar != null
- and not avatar.is_water_recovery_active()
- and (
- _network_fishing == null
- or not _network_fishing.has_peer_attempt(peer_id)
- )
- and _shop_interaction.is_avatar_in_range(avatar)
- )
-
-
-func _on_session_state_changed(state: NetworkSession.State) -> void:
- if state in [
- NetworkSession.State.INACTIVE,
- NetworkSession.State.DISCONNECTING,
- NetworkSession.State.CONNECTION_FAILED,
- NetworkSession.State.SERVER_LOST,
- ]:
- var had_pending: bool = is_local_sale_pending()
- _clear_session_state()
- if had_pending:
- local_sale_finished.emit(
- "", false, "Connection lost.", _empty_catch_ids(), 0
- )
-
-
-func _empty_catch_ids() -> Array[StringName]:
- return []
-
-
-func _clear_session_state() -> void:
- _request_ledgers.clear()
- _pending_by_peer.clear()
- _result_owners.clear()
- _acknowledged_results.clear()
- _applied_results.clear()
- _received_results.clear()
- _pending_local_request_id = ""
- _pending_local_request.clear()
- _local_request_retry_elapsed = 0.0
- _pending_local_catch_ids.clear()
- _pending_local_items.clear()
- _pending_local_buyer_id = StringName()
-
-
-func _bound_dictionary(values: Dictionary) -> void:
- while values.size() > MAX_LEDGER_ENTRIES_PER_PEER:
- values.erase(values.keys().front())
-
-
-func _new_id(prefix: String) -> String:
- return "%s:%s" % [
- prefix,
- Crypto.new().generate_random_bytes(16).hex_encode(),
- ]
diff --git a/network/network_sale_service.gd.uid b/network/network_sale_service.gd.uid
deleted file mode 100644
index daa398a..0000000
--- a/network/network_sale_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://sqqy3anotq6n
diff --git a/network/network_session.gd b/network/network_session.gd
deleted file mode 100644
index 66d5101..0000000
--- a/network/network_session.gd
+++ /dev/null
@@ -1,3000 +0,0 @@
-class_name NetworkSession
-extends Node
-
-const DEFAULT_PORT: int = 7777
-const DEFAULT_PRIVATE_HOST_PORT_ATTEMPTS: int = 16
-const DEFAULT_SESSION_MAX_PLAYERS: int = 8
-const DEFAULT_TRANSPORT_MAX_CLIENTS: int = 31
-const CONNECTION_TIMEOUT_SECONDS: float = 10.0
-const AUTHENTICATION_TIMEOUT_SECONDS: float = 60.0
-const ENET_TIMEOUT_LIMIT: int = 32
-const ENET_TIMEOUT_MINIMUM_MS: int = 10000
-const ENET_TIMEOUT_MAXIMUM_MS: int = 120000
-const INPUT_INTERVAL: float = 1.0 / 30.0
-const IDLE_INPUT_INTERVAL: float = 1.0 / 5.0
-const SNAPSHOT_INTERVAL: float = 1.0 / 30.0
-const OWNER_SNAPSHOT_DIVISOR: int = 3
-const NEAR_REMOTE_SNAPSHOT_DIVISOR: int = 2
-const FAR_REMOTE_SNAPSHOT_DIVISOR: int = 6
-const DISTANT_REMOTE_SNAPSHOT_DIVISOR: int = 8
-const NEAR_REMOTE_DISTANCE: float = 48.0
-const FAR_REMOTE_DISTANCE: float = 96.0
-const MOVEMENT_SNAPSHOT_BATCH_SIZE: int = 8
-const MOVEMENT_INPUT_FIELD_COUNT: int = 8
-const MOVEMENT_SNAPSHOT_FIELD_COUNT: int = 6
-const MOVEMENT_ANIMATION_FIELD_COUNT: int = 7
-const MOVEMENT_ANIMATION_ACTION_FIELD_COUNT: int = 5
-const MAX_PENDING_MOVEMENT_INPUTS: int = 96
-const ANIMATION_REFRESH_INTERVAL: float = 1.0
-const MAX_MOVEMENT_INPUT_SEQUENCE: int = 2147483647
-const MAX_MOVEMENT_ONE_WAY_TRANSIT_SECONDS: float = 0.25
-const SPACE_TRANSITION_SNAPSHOT_GUARD_DISTANCE: float = 12.0
-
-const MOVEMENT_FLAG_JUMP: int = 1 << 0
-const MOVEMENT_FLAG_SPRINT: int = 1 << 1
-const MOVEMENT_FLAG_SNEAK: int = 1 << 2
-const MOVEMENT_FLAG_SLOW_WALK: int = 1 << 3
-const MOVEMENT_FLAG_SITTING: int = 1 << 4
-const MOVEMENT_FLAG_CASTING: int = 1 << 5
-const MOVEMENT_ALLOWED_FLAGS: int = (
- MOVEMENT_FLAG_JUMP
- | MOVEMENT_FLAG_SPRINT
- | MOVEMENT_FLAG_SNEAK
- | MOVEMENT_FLAG_SLOW_WALK
- | MOVEMENT_FLAG_SITTING
- | MOVEMENT_FLAG_CASTING
-)
-const SNAPSHOT_FLAG_GROUNDED: int = 1 << 0
-const SNAPSHOT_FLAG_SITTING: int = 1 << 1
-const SNAPSHOT_FLAG_CASTING: int = 1 << 2
-const SNAPSHOT_ALLOWED_FLAGS: int = (
- SNAPSHOT_FLAG_GROUNDED
- | SNAPSHOT_FLAG_SITTING
- | SNAPSHOT_FLAG_CASTING
-)
-
-signal state_changed(state: State)
-signal status_message_changed(message: String)
-signal connection_error(message: String)
-signal peer_authenticated(peer_id: int, display_name: String)
-signal peer_removed(peer_id: int)
-signal host_openness_changed(is_open: bool)
-signal session_display_name_changed(display_name: String)
-signal peer_count_changed(player_count: int, max_players: int)
-signal peer_display_name_changed(peer_id: int, display_name: String)
-signal peer_profile_changed(
- peer_id: int,
- display_name: String,
- appearance: Dictionary,
-)
-signal join_authenticated
-signal server_trust_required(
- endpoint: String,
- expected_fingerprint: String,
- received_fingerprint: String,
- is_changed: bool,
-)
-signal peer_identity_observed(peer_id: int, status: String)
-signal operator_status_changed(peer_id: int, is_operator: bool)
-signal server_lost(message: String)
-signal remote_recovery_requested(peer_id: int, entry_position: Vector3)
-signal remote_recovery_presentation_changed(
- peer_id: int,
- active: bool,
- attempt_id: String,
-)
-
-enum State {
- INACTIVE,
- STARTING_PRIVATE_HOST,
- PRIVATE_HOST,
- OPEN_HOST,
- CONNECTING,
- AUTHENTICATING,
- VERIFYING_SERVER_IDENTITY,
- JOINED_CLIENT,
- DISCONNECTING,
- CONNECTION_FAILED,
- SERVER_LOST,
-}
-
-@export_range(2, 128, 1) var session_max_players: int = (
- DEFAULT_SESSION_MAX_PLAYERS
-)
-@export_range(1, 256, 1) var transport_max_clients: int = (
- DEFAULT_TRANSPORT_MAX_CLIENTS
-)
-
-var state: State = State.INACTIVE
-var _transport: DirectEnetTransport
-var _profile: NetworkProfilePreferences
-var _saved_servers: SavedServerStore
-var _spawn_service: PlayerSpawnService
-var _registry := PeerRegistry.new()
-var _pending_authentication: Dictionary[int, float] = {}
-var _session_id: String = ""
-var _operation_generation: int = 0
-var _connection_deadline: float = 0.0
-var _client_nonce: String = ""
-var _current_route: ConnectionRoute
-var _input_sequence: int = 0
-var _input_accumulator: float = 0.0
-var _idle_input_accumulator: float = 0.0
-var _last_input_state_hash: int = 0
-var _pending_movement_inputs: Array[Dictionary] = []
-var _snapshot_accumulator: float = 0.0
-var _movement_snapshot_tick: int = 0
-var _last_local_snapshot_received_msec: int = 0
-var _local_space_transition_guard_active: bool = false
-var _local_space_transition_guard_position: Vector3 = Vector3.ZERO
-var _local_space_transition_minimum_ack: int = 0
-var _animation_refresh_accumulator: float = 0.0
-var _last_animation_state_by_peer: Dictionary[int, Dictionary] = {}
-var _pending_animation_state_by_peer: Dictionary[int, Dictionary] = {}
-var _last_local_animation_action_signature: Array = []
-var _movement_inputs_sent: int = 0
-var _movement_inputs_received: int = 0
-var _movement_snapshot_packets_sent: int = 0
-var _movement_snapshot_states_sent: int = 0
-var _movement_animation_packets_sent: int = 0
-var _movement_animation_states_sent: int = 0
-var _last_server_max_players: int = DEFAULT_SESSION_MAX_PLAYERS
-var _last_server_player_count: int = 0
-var _last_server_display_name: String = ""
-var _last_server_protocol_version: int = 0
-var _last_server_world_seed: int = NetworkProtocol.DEFAULT_WORLD_SEED
-var _last_server_world_layout: StringName = WorldLayout.GENERATED
-var _server_capabilities: PackedStringArray = PackedStringArray()
-var _peer_space_ids: Dictionary[int, StringName] = {}
-var _profile_ready: bool = false
-var _player_identity: PlayerIdentityStore
-var _host_identity: HostIdentityStore
-var _known_players: KnownPlayerStore
-var _server_trust: ServerTrustStore
-var _host_bans: HostBanStore
-var _pending_identity_challenges: Dictionary[int, Dictionary] = {}
-var _authenticated_identity_cache: Dictionary[int, Dictionary] = {}
-var _client_identity_attempt: Dictionary = {}
-var _pending_server_proof: Dictionary = {}
-var _recovery_attempts: Dictionary[int, String] = {}
-var _server_identity_fingerprint: String = ""
-var _server_identity_public_key: String = ""
-var _session_identity_keys: Dictionary[String, String] = {}
-var _local_appearance_snapshot: Dictionary = (
- CharacterCustomizationCatalog.default_snapshot()
-)
-var _moderation_disconnect_message := ""
-var _host_port: int = 0
-var _session_display_name: String = "straywild Room"
-var _dedicated_host: bool = false
-var _configured_operator_fingerprints: Dictionary[String, bool] = {}
-var _session_operator_fingerprints: Dictionary[String, bool] = {}
-var _operator_peer_ids: Dictionary[int, bool] = {}
-var _local_operator: bool = false
-var _host_world_seed: int = NetworkProtocol.DEFAULT_WORLD_SEED
-var _host_world_layout: StringName = WorldLayout.GENERATED
-
-
-func _ready() -> void:
- transport_max_clients = maxi(
- transport_max_clients,
- session_max_players - 1
- )
- _connect_multiplayer_signals()
-
-
-func setup(
- profile: NetworkProfilePreferences,
- saved_servers: SavedServerStore,
- spawn_service: PlayerSpawnService,
- player_identity: PlayerIdentityStore,
- host_identity: HostIdentityStore,
- known_players: KnownPlayerStore,
- server_trust: ServerTrustStore,
- host_bans: HostBanStore,
- dedicated: bool = false,
-) -> void:
- _profile = profile
- _saved_servers = saved_servers
- _spawn_service = spawn_service
- _player_identity = player_identity
- _host_identity = host_identity
- _known_players = known_players
- _server_trust = server_trust
- _host_bans = host_bans
- if not dedicated and _profile != null:
- _profile_ready = _profile.load_or_create()
- if not dedicated and _player_identity != null:
- _profile_ready = _profile_ready and _player_identity.load_or_create()
-
-
-func configure_dedicated_operators(
- fingerprints: PackedStringArray,
-) -> bool:
- if state != State.INACTIVE:
- return false
- var configured: Dictionary[String, bool] = {}
- for fingerprint: String in fingerprints:
- if not NetworkIdentityCrypto.valid_fingerprint(fingerprint):
- return false
- configured[fingerprint] = true
- _configured_operator_fingerprints = configured
- return true
-
-
-func start_private_host(
- port: int = DEFAULT_PORT,
- port_attempts: int = 1,
-) -> bool:
- if (
- state != State.INACTIVE
- or not _profile_ready
- or _profile == null
- or _spawn_service == null
- or _host_identity == null
- ):
- return false
- return _start_host(port, port_attempts, false)
-
-
-func start_dedicated_host(
- port: int = DEFAULT_PORT,
- max_players: int = DEFAULT_SESSION_MAX_PLAYERS,
- bind_address: String = "*",
-) -> bool:
- if (
- state != State.INACTIVE
- or _spawn_service == null
- or _host_identity == null
- or max_players < 1
- or max_players > 128
- or bind_address.is_empty()
- ):
- return false
- session_max_players = max_players
- transport_max_clients = maxi(transport_max_clients, max_players)
- return _start_host(port, 1, true, bind_address)
-
-
-func _start_host(
- port: int,
- port_attempts: int,
- dedicated: bool,
- bind_address: String = "*",
-) -> bool:
- if not _host_identity.load_or_create():
- _fail(_host_identity.error_message)
- return false
- if port < 1 or port > 65535 or port_attempts < 1:
- _fail("The hosting port must be from 1 to 65535.")
- return false
- _operation_generation += 1
- _set_state(
- State.STARTING_PRIVATE_HOST,
- "Starting dedicated server..." if dedicated else "Starting private game...",
- )
- var selected_port: int = 0
- var final_port: int = mini(port + port_attempts - 1, 65535)
- for candidate_port: int in range(port, final_port + 1):
- if not _can_bind_udp_port(candidate_port, bind_address):
- continue
- _replace_transport()
- var error: Error = _transport.start_host(
- candidate_port,
- transport_max_clients,
- bind_address,
- )
- if error == OK:
- selected_port = candidate_port
- break
- if selected_port == 0:
- _fail(
- "Could not start a private game on UDP ports %d–%d."
- % [port, final_port]
- )
- return false
- _host_port = selected_port
- _dedicated_host = dedicated
- var peer: MultiplayerPeer = _transport.get_multiplayer_peer()
- peer.refuse_new_connections = true
- multiplayer.multiplayer_peer = peer
- _session_id = Crypto.new().generate_random_bytes(16).hex_encode()
- _registry.clear()
- _session_operator_fingerprints.clear()
- _operator_peer_ids.clear()
- _local_operator = false
- _spawn_service.clear_remote_players()
- if not dedicated:
- _register_player_host()
- _set_state(
- State.PRIVATE_HOST,
- (
- "Dedicated server • UDP %d"
- if dedicated else "Private game • UDP %d"
- ) % selected_port,
- )
- host_openness_changed.emit(false)
- _emit_peer_count()
- return true
-
-
-func _register_player_host() -> void:
- _registry.add_peer(
- 1,
- _profile.profile_id,
- _profile.display_name,
- NetworkProtocol.PROTOCOL_VERSION,
- _player_identity.fingerprint,
- _player_identity.public_pem,
- PackedStringArray([
- NetworkProtocol.FISH_QUALITY_CAPABILITY,
- NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
- NetworkProtocol.WORLD_TIME_CAPABILITY,
- NetworkProtocol.WORLD_WEATHER_CAPABILITY,
- NetworkProtocol.JOBS_CAPABILITY,
- NetworkProtocol.WORLD_SPAWN_CAPABILITY,
- NetworkProtocol.BACKPACK_SHOP_CAPABILITY,
- NetworkProtocol.WORLD_GENERATION_CAPABILITY,
- NetworkProtocol.WORLD_LAYOUT_CAPABILITY,
- NetworkProtocol.FRIENDS_CAPABILITY,
- NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
- NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
- NetworkProtocol.HOME_CAPABILITY,
- ]),
- )
- _registry.update_appearance(1, _local_appearance_snapshot)
- var host_profile_hello := NetworkProtocol.make_client_hello(
- _profile.profile_id,
- _profile.display_name,
- NetworkIdentityCrypto.secure_id(32),
- _local_appearance_snapshot,
- _player_identity.fingerprint,
- )
- host_profile_hello["identity_signature"] = _player_identity.sign(
- "handshake_client_profile",
- NetworkProtocol.client_profile_fields(host_profile_hello),
- )
- var host_record := _registry.get_peer(1)
- if host_record != null:
- host_record.profile_authorization = {
- "domain": "handshake_client_profile",
- "signature": host_profile_hello["identity_signature"],
- "client_nonce": host_profile_hello["client_nonce"],
- }
- _archive_authenticated_identity(host_record)
- _spawn_service.register_local_player(1)
- var host_avatar := _spawn_service.get_avatar(1)
- if host_avatar != null:
- host_avatar.apply_appearance_snapshot(_local_appearance_snapshot)
-
-
-func get_host_port() -> int:
- return _host_port if is_host() else 0
-
-
-func send_traversal_packet(
- destination_address: String,
- destination_port: int,
- packet: PackedByteArray,
-) -> bool:
- return (
- _transport != null
- and state in [
- State.PRIVATE_HOST,
- State.OPEN_HOST,
- State.CONNECTING,
- State.AUTHENTICATING,
- ]
- and _transport.send_raw_packet(
- destination_address,
- destination_port,
- packet,
- )
- )
-
-
-func is_dedicated_host() -> bool:
- return is_host() and _dedicated_host
-
-
-func is_local_operator() -> bool:
- return state == State.JOINED_CLIENT and _local_operator
-
-
-func can_local_moderate() -> bool:
- return is_host() or is_local_operator()
-
-
-func can_manage_operators() -> bool:
- return is_host() and not _dedicated_host
-
-
-func is_peer_operator(peer_id: int) -> bool:
- return bool(_operator_peer_ids.get(peer_id, false))
-
-
-func set_peer_operator(
- peer_id: int,
- fingerprint: String,
- enabled: bool,
-) -> bool:
- if not can_manage_operators() or peer_id <= 1:
- return false
- var record: PeerRegistry.PeerRecord = _registry.get_peer(peer_id)
- if (
- record == null
- or not record.identity_authenticated
- or record.identity_fingerprint != fingerprint
- ):
- return false
- if enabled:
- _session_operator_fingerprints[fingerprint] = true
- else:
- _session_operator_fingerprints.erase(fingerprint)
- _set_operator_status(peer_id, enabled)
- return true
-
-
-func set_session_display_name(value: String) -> void:
- var cleaned: String = value.strip_edges().left(48)
- if cleaned.is_empty():
- cleaned = "straywild Room"
- if cleaned == _session_display_name:
- return
- _session_display_name = cleaned
- session_display_name_changed.emit(_session_display_name)
-
-
-func get_session_display_name() -> String:
- return _session_display_name
-
-
-func get_local_display_name() -> String:
- return _profile.display_name if _profile != null else ""
-
-
-static func _can_bind_udp_port(port: int, bind_address: String = "*") -> bool:
- var probe := PacketPeerUDP.new()
- var error: Error = probe.bind(port, bind_address)
- probe.close()
- return error == OK
-
-
-func join_direct(endpoint_text: String) -> bool:
- if (
- state != State.INACTIVE
- or not _profile_ready
- or _profile == null
- or _spawn_service == null
- ):
- return false
- var endpoint: ConnectionEndpoint = EndpointParser.parse(endpoint_text)
- if not endpoint.is_valid():
- _fail(endpoint.error_message)
- return false
- _operation_generation += 1
- var generation: int = _operation_generation
- _current_route = ConnectionRoute.direct(endpoint)
- _set_state(
- State.CONNECTING,
- "Connecting to %s..." % endpoint.normalized_display
- )
- _replace_transport()
- var error: Error = _transport.connect_to_route(_current_route)
- if error != OK:
- _fail("Could not begin the direct connection.")
- return false
- multiplayer.multiplayer_peer = _transport.get_multiplayer_peer()
- _connection_deadline = (
- Time.get_ticks_msec() / 1000.0 + CONNECTION_TIMEOUT_SECONDS
- )
- # The generation is checked by the process timeout and all state-gated
- # multiplayer callbacks.
- if generation != _operation_generation:
- return false
- return true
-
-
-func cancel_connection() -> void:
- if state not in [
- State.CONNECTING,
- State.AUTHENTICATING,
- State.VERIFYING_SERVER_IDENTITY,
- ]:
- return
- _operation_generation += 1
- _teardown_peer()
- _set_state(State.INACTIVE, "Connection cancelled.")
-
-
-func reset_failure() -> void:
- if state not in [State.CONNECTION_FAILED, State.SERVER_LOST]:
- return
- _teardown_peer()
- _set_state(State.INACTIVE, "Ready for a direct connection.")
-
-
-func disconnect_session(message: String = "Disconnected.") -> void:
- if state == State.INACTIVE:
- return
- if state in [State.CONNECTION_FAILED, State.SERVER_LOST]:
- _teardown_peer()
- _set_state(State.INACTIVE, message)
- return
- _operation_generation += 1
- _set_state(State.DISCONNECTING, "Disconnecting...")
- _teardown_peer()
- _set_state(State.INACTIVE, message)
-
-
-func set_host_open(is_open: bool) -> bool:
- if state not in [State.PRIVATE_HOST, State.OPEN_HOST]:
- return false
- var peer: MultiplayerPeer = multiplayer.multiplayer_peer
- if peer == null:
- return false
- peer.refuse_new_connections = not is_open
- _set_state(
- State.OPEN_HOST if is_open else State.PRIVATE_HOST,
- (
- "Dedicated server • UDP %d • %d / %d players"
- if _dedicated_host
- else "Open game • UDP %d • %d / %d players"
- if is_open
- else "Private game • UDP %d • %d / %d players"
- ) % [_host_port, _registry.size(), session_max_players]
- )
- host_openness_changed.emit(is_open)
- return true
-
-
-func is_host() -> bool:
- return state in [State.PRIVATE_HOST, State.OPEN_HOST]
-
-
-func is_open_host() -> bool:
- return state == State.OPEN_HOST
-
-
-func is_joined_client() -> bool:
- return state == State.JOINED_CLIENT
-
-
-func is_session_active() -> bool:
- return is_host() or is_joined_client()
-
-
-func get_player_count() -> int:
- return _registry.size()
-
-
-func get_session_max_players() -> int:
- return _last_server_max_players if is_joined_client() else session_max_players
-
-
-func get_current_route_display() -> String:
- return (
- _current_route.display_description
- if _current_route != null
- else _transport.get_route_description() if _transport != null else ""
- )
-
-
-func get_current_endpoint() -> ConnectionEndpoint:
- return (
- _current_route.direct_endpoint
- if _current_route != null
- and _current_route.kind == ConnectionRoute.Kind.DIRECT
- else null
- )
-
-
-func get_last_server_metadata() -> Dictionary:
- return {
- "server_display_name": _last_server_display_name,
- "protocol_version": _last_server_protocol_version,
- "player_count": _last_server_player_count,
- "max_players": _last_server_max_players,
- "world_seed": _last_server_world_seed,
- "world_layout": String(_last_server_world_layout),
- }
-
-
-func set_host_world_seed(seed: int) -> bool:
- return set_host_world(WorldLayout.GENERATED, seed)
-
-
-func set_host_world(world_layout: StringName, seed: int) -> bool:
- if (
- state != State.INACTIVE
- or not WorldLayout.is_valid(world_layout)
- or seed <= 0
- or seed > NetworkProtocol.MAX_WORLD_SEED
- ):
- return false
- _host_world_layout = world_layout
- _host_world_seed = seed
- return true
-
-
-func get_authoritative_world_layout() -> StringName:
- return (
- _last_server_world_layout
- if is_joined_client()
- else _host_world_layout
- )
-
-
-func get_authoritative_world_seed() -> int:
- return _last_server_world_seed if is_joined_client() else _host_world_seed
-
-
-func can_use_host_gameplay() -> bool:
- return is_host()
-
-
-func is_gameplay_session_active() -> bool:
- return state in [State.PRIVATE_HOST, State.OPEN_HOST, State.JOINED_CLIENT]
-
-
-func is_authenticated_peer(peer_id: int) -> bool:
- return _registry.has_peer(peer_id)
-
-
-func get_session_id() -> String:
- return _session_id
-
-
-func get_operation_generation() -> int:
- return _operation_generation
-
-
-func supports_server_capability(capability: StringName) -> bool:
- if is_host():
- return str(capability) in PackedStringArray([
- "movement_v1", "fishing_v1", "sale_v1", "shop_v1",
- NetworkProtocol.ART_SHOP_CAPABILITY,
- NetworkProtocol.BACKPACK_SHOP_CAPABILITY,
- "item_use_v1", "equipment_v1", "fish_showcase_v1",
- NetworkProtocol.FISH_QUALITY_CAPABILITY,
- NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
- NetworkProtocol.WORLD_TIME_CAPABILITY,
- NetworkProtocol.WORLD_WEATHER_CAPABILITY,
- NetworkProtocol.WORLD_SPAWN_CAPABILITY,
- NetworkProtocol.APPEARANCE_PREVIEW_CAPABILITY,
- NetworkProtocol.WORLD_GENERATION_CAPABILITY,
- NetworkProtocol.WORLD_LAYOUT_CAPABILITY,
- NetworkProtocol.FRIENDS_CAPABILITY,
- NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
- NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
- NetworkProtocol.HOME_CAPABILITY,
- "chat_v1",
- "mail_v1",
- "profile_v1",
- "identity_v1",
- ])
- return str(capability) in _server_capabilities
-
-
-func get_peer_record(peer_id: int) -> PeerRegistry.PeerRecord:
- return _registry.get_peer(peer_id)
-
-
-func peer_supports_capability(
- peer_id: int,
- capability: StringName,
-) -> bool:
- var record: PeerRegistry.PeerRecord = _registry.get_peer(peer_id)
- return record != null and str(capability) in record.capability_flags
-
-
-func get_authenticated_peer_ids() -> Array[int]:
- return _registry.get_peer_ids()
-
-
-func set_peer_space(peer_id: int, space_id: StringName) -> bool:
- if not _registry.has_peer(peer_id) or space_id.is_empty():
- return false
- _peer_space_ids[peer_id] = space_id
- return true
-
-
-func get_peer_space(peer_id: int) -> StringName:
- return _peer_space_ids.get(peer_id, &"world")
-
-
-func peers_share_space(first_peer_id: int, second_peer_id: int) -> bool:
- return get_peer_space(first_peer_id) == get_peer_space(second_peer_id)
-
-
-func apply_authoritative_space_transition(
- peer_id: int,
- target_transform: Transform3D,
- acknowledged_input: int,
-) -> bool:
- if not target_transform.origin.is_finite():
- return false
- var local_peer_id: int = get_local_peer_id()
- var local_transition: bool = peer_id == local_peer_id
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- # Remote home transitions can arrive before their player-spawn message. Keep
- # the new space assignment so the later spawn inherits correct visibility;
- # only a missing local avatar is a transition failure.
- if avatar == null:
- return not local_transition
- avatar.apply_network_space_transition(target_transform, local_transition)
- if (
- local_transition
- and state == State.JOINED_CLIENT
- ):
- # Movement snapshots use another ENet channel and a pre-transition audit
- # can therefore arrive after the reliable home transition. Clear local
- # prediction and reject those old-world snapshots until the host publishes
- # its first audit from the new space.
- _pending_movement_inputs.clear()
- _last_local_snapshot_received_msec = 0
- _last_input_state_hash = 0
- _idle_input_accumulator = IDLE_INPUT_INTERVAL
- _local_space_transition_guard_active = true
- _local_space_transition_guard_position = target_transform.origin
- _local_space_transition_minimum_ack = maxi(acknowledged_input, 0)
- return true
-
-
-func get_peer_rtt_ms(peer_id: int) -> int:
- if peer_id == 1:
- return 0
- var enet := multiplayer.multiplayer_peer as ENetMultiplayerPeer
- if (
- enet == null
- or enet.get_connection_status()
- != MultiplayerPeer.CONNECTION_CONNECTED
- ):
- return -1
- var measurable := is_host() or (
- is_joined_client() and peer_id == get_local_peer_id()
- )
- if not measurable:
- return -1
- if is_joined_client() and not _registry.has_peer(1):
- return -1
- var packet_peer: ENetPacketPeer = enet.get_peer(peer_id if is_host() else 1)
- if packet_peer == null:
- return -1
- return int(packet_peer.get_statistic(ENetPacketPeer.PEER_ROUND_TRIP_TIME))
-
-
-func get_movement_metrics() -> Dictionary:
- return {
- "inputs_sent": _movement_inputs_sent,
- "inputs_received": _movement_inputs_received,
- "snapshot_packets_sent": _movement_snapshot_packets_sent,
- "snapshot_states_sent": _movement_snapshot_states_sent,
- "animation_packets_sent": _movement_animation_packets_sent,
- "animation_states_sent": _movement_animation_states_sent,
- "pending_local_inputs": _pending_movement_inputs.size(),
- }
-
-
-func kick_authenticated_peer(
- peer_id: int,
- fingerprint: String,
- banned: bool = false,
-) -> bool:
- if not is_host() or peer_id <= 1:
- return false
- var record := _registry.get_peer(peer_id)
- if record == null or record.identity_fingerprint != fingerprint:
- return false
- receive_moderation_disconnect.rpc_id(
- peer_id,
- (
- "You are not permitted to join this server."
- if banned else "You were removed by the host."
- ),
- )
- call_deferred("_disconnect_rejected_peer", peer_id)
- return true
-
-
-func get_local_identity_fingerprint() -> String:
- return _player_identity.fingerprint if _player_identity != null else ""
-
-
-func sign_local_action(domain: String, fields: Array) -> PackedByteArray:
- return (
- _player_identity.sign(domain, fields)
- if _player_identity != null else PackedByteArray()
- )
-
-
-func verify_peer_action(
- peer_id: int,
- domain: String,
- fields: Array,
- signature: PackedByteArray,
-) -> bool:
- var record := _registry.get_peer(peer_id)
- if record == null or not record.identity_authenticated:
- return false
- return NetworkIdentityCrypto.verify_fields(
- NetworkIdentityCrypto.load_public_key(record.identity_public_key),
- domain,
- fields,
- signature,
- )
-
-
-func matches_authenticated_session_identity(
- fingerprint: String, public_pem: String
-) -> bool:
- var normalized := NetworkIdentityCrypto.normalize_public_pem(public_pem)
- return (
- NetworkIdentityCrypto.valid_fingerprint(fingerprint)
- and NetworkIdentityCrypto.fingerprint_public_pem(normalized)
- == fingerprint
- and _session_identity_keys.get(fingerprint, "") == normalized
- )
-
-
-func sign_host_action(domain: String, fields: Array) -> PackedByteArray:
- return (
- _host_identity.sign(domain, fields)
- if is_host() and _host_identity != null else PackedByteArray()
- )
-
-
-func get_host_identity_fingerprint() -> String:
- return (
- _host_identity.fingerprint
- if is_host() and _host_identity != null
- else _server_identity_fingerprint
- )
-
-
-func verify_host_action(
- domain: String, fields: Array, signature: PackedByteArray
-) -> bool:
- var public_pem: String = (
- _host_identity.public_pem
- if is_host() and _host_identity != null
- else _server_identity_public_key
- )
- return NetworkIdentityCrypto.verify_fields(
- NetworkIdentityCrypto.load_public_key(public_pem),
- domain,
- fields,
- signature,
- )
-
-
-func update_local_display_name(value: String) -> bool:
- if _profile == null or not _profile.set_display_name(value):
- return false
- var peer_id := get_local_peer_id()
- if is_host():
- _apply_display_name(peer_id, _profile.display_name)
- receive_display_name.rpc(peer_id, _profile.display_name)
- elif is_joined_client() and supports_server_capability(&"chat_v1"):
- submit_display_name.rpc_id(1, _profile.display_name)
- return true
-
-
-func set_local_appearance_snapshot(snapshot: Dictionary) -> void:
- if CharacterCustomizationCatalog.validate_snapshot(snapshot):
- _local_appearance_snapshot = snapshot.duplicate(true)
- var local_id := get_local_peer_id()
- if local_id > 0:
- _registry.update_appearance(local_id, _local_appearance_snapshot)
-
-
-func apply_canonical_profile(
- peer_id: int,
- display_name: String,
- appearance: Dictionary,
-) -> bool:
- if (
- not NetworkProfilePreferences.is_valid_display_name(display_name)
- or not CharacterCustomizationCatalog.validate_snapshot(appearance)
- ):
- return false
- var name_changed := _registry.update_display_name(peer_id, display_name)
- var appearance_changed := _registry.update_appearance(peer_id, appearance)
- if name_changed:
- peer_display_name_changed.emit(peer_id, display_name)
- if appearance_changed:
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- if avatar != null:
- avatar.apply_appearance_snapshot(appearance)
- if name_changed or appearance_changed:
- peer_profile_changed.emit(peer_id, display_name, appearance.duplicate(true))
- return name_changed or appearance_changed
-
-
-@rpc("any_peer", "call_remote", "reliable", 0)
-func submit_display_name(value: String) -> void:
- var sender_id := multiplayer.get_remote_sender_id()
- if (
- is_host()
- and is_authenticated_peer(sender_id)
- and NetworkProfilePreferences.is_valid_display_name(value)
- ):
- _apply_display_name(sender_id, value.strip_edges())
- receive_display_name.rpc(sender_id, value.strip_edges())
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_display_name(peer_id: int, value: String) -> void:
- if NetworkProfilePreferences.is_valid_display_name(value):
- _apply_display_name(peer_id, value.strip_edges())
-
-
-func _apply_display_name(peer_id: int, value: String) -> void:
- if _registry.update_display_name(peer_id, value):
- peer_display_name_changed.emit(peer_id, value)
-
-
-func get_local_peer_id() -> int:
- if is_dedicated_host():
- return 0
- return multiplayer.get_unique_id() if is_gameplay_session_active() else 0
-
-
-func get_latest_movement_input_sequence() -> int:
- return _input_sequence if state == State.JOINED_CLIENT else 0
-
-
-func _process(delta: float) -> void:
- var now: float = Time.get_ticks_msec() / 1000.0
- if (
- state in [
- State.CONNECTING,
- State.AUTHENTICATING,
- State.VERIFYING_SERVER_IDENTITY,
- ]
- and _connection_deadline > 0.0
- and now >= _connection_deadline
- ):
- _fail("Connection timed out.")
- _teardown_peer()
- return
- if is_host():
- _expire_pending_authentication(now)
- if state not in [State.OPEN_HOST, State.PRIVATE_HOST, State.JOINED_CLIENT]:
- return
- _input_accumulator += delta
- _snapshot_accumulator += delta
- _animation_refresh_accumulator += delta
- if state == State.JOINED_CLIENT and _input_accumulator >= INPUT_INTERVAL:
- var elapsed_input_time: float = _input_accumulator
- _input_accumulator = fmod(_input_accumulator, INPUT_INTERVAL)
- _idle_input_accumulator += elapsed_input_time
- _maybe_send_local_animation_action()
- _maybe_send_local_input()
- if is_host() and _snapshot_accumulator >= SNAPSHOT_INTERVAL:
- _snapshot_accumulator = fmod(_snapshot_accumulator, SNAPSHOT_INTERVAL)
- _broadcast_movement_snapshots()
-
-
-func _connect_multiplayer_signals() -> void:
- if not multiplayer.peer_connected.is_connected(_on_peer_connected):
- multiplayer.peer_connected.connect(_on_peer_connected)
- if not multiplayer.peer_disconnected.is_connected(_on_peer_disconnected):
- multiplayer.peer_disconnected.connect(_on_peer_disconnected)
- if not multiplayer.connected_to_server.is_connected(
- _on_connected_to_server
- ):
- multiplayer.connected_to_server.connect(_on_connected_to_server)
- if not multiplayer.connection_failed.is_connected(_on_connection_failed):
- multiplayer.connection_failed.connect(_on_connection_failed)
- if not multiplayer.server_disconnected.is_connected(
- _on_server_disconnected
- ):
- multiplayer.server_disconnected.connect(_on_server_disconnected)
-
-
-func _replace_transport() -> void:
- if _transport != null:
- _transport.disconnect_transport()
- _transport.queue_free()
- _transport = DirectEnetTransport.new()
- add_child(_transport)
- _transport.transport_error.connect(connection_error.emit)
-
-
-func _on_peer_connected(peer_id: int) -> void:
- if not is_host():
- return
- if state != State.OPEN_HOST:
- multiplayer.multiplayer_peer.disconnect_peer(peer_id)
- return
- _configure_enet_peer_timeout(peer_id)
- _pending_authentication[peer_id] = (
- Time.get_ticks_msec() / 1000.0 + AUTHENTICATION_TIMEOUT_SECONDS
- )
-
-
-func _on_connected_to_server() -> void:
- if state != State.CONNECTING:
- return
- _configure_enet_peer_timeout(1)
- _set_state(State.AUTHENTICATING, "Authenticating...")
- _client_nonce = NetworkIdentityCrypto.secure_id(32)
- _client_identity_attempt = NetworkProtocol.make_identity_hello(
- _player_identity.public_pem,
- _player_identity.fingerprint,
- _client_nonce,
- NetworkIdentityCrypto.secure_id(16),
- )
- submit_identity_hello.rpc_id(1, _client_identity_attempt)
-
-
-func _configure_enet_peer_timeout(peer_id: int) -> void:
- var enet := multiplayer.multiplayer_peer as ENetMultiplayerPeer
- if enet == null:
- return
- var packet_peer: ENetPacketPeer = enet.get_peer(peer_id)
- if packet_peer == null:
- return
- # A deterministic generated world can temporarily occupy the client's main
- # thread during the authenticated join transition. Keep that bounded load
- # from looking like a dead connection while retaining normal ENet liveness
- # checks once the client resumes polling.
- packet_peer.set_timeout(
- ENET_TIMEOUT_LIMIT,
- ENET_TIMEOUT_MINIMUM_MS,
- ENET_TIMEOUT_MAXIMUM_MS,
- )
-
-
-func _on_connection_failed() -> void:
- if state not in [
- State.CONNECTING,
- State.AUTHENTICATING,
- State.VERIFYING_SERVER_IDENTITY,
- ]:
- return
- _teardown_peer()
- _fail("Could not connect. The server may be private, unavailable, or unreachable.")
-
-
-func _on_server_disconnected() -> void:
- if state in [State.INACTIVE, State.DISCONNECTING]:
- return
- _operation_generation += 1
- _teardown_peer()
- if state in [
- State.CONNECTING,
- State.AUTHENTICATING,
- State.VERIFYING_SERVER_IDENTITY,
- ]:
- _fail("The server rejected or ended authentication.")
- return
- var message := (
- _moderation_disconnect_message
- if not _moderation_disconnect_message.is_empty()
- else "The server connection was lost."
- )
- _moderation_disconnect_message = ""
- _set_state(State.SERVER_LOST, message)
- server_lost.emit(message)
- connection_error.emit(message)
-
-
-func _on_peer_disconnected(peer_id: int) -> void:
- _pending_authentication.erase(peer_id)
- _pending_identity_challenges.erase(peer_id)
- _authenticated_identity_cache.erase(peer_id)
- _operator_peer_ids.erase(peer_id)
- _peer_space_ids.erase(peer_id)
- _last_animation_state_by_peer.erase(peer_id)
- _pending_animation_state_by_peer.erase(peer_id)
- var recovery_attempt: String = _recovery_attempts.get(peer_id, "")
- if not recovery_attempt.is_empty():
- _recovery_attempts.erase(peer_id)
- remote_recovery_presentation_changed.emit(
- peer_id, false, recovery_attempt
- )
- if _registry.has_peer(peer_id):
- _registry.remove_peer(peer_id)
- _spawn_service.remove_peer(peer_id)
- if is_host():
- receive_peer_despawn.rpc(peer_id)
- peer_removed.emit(peer_id)
- _emit_peer_count()
-
-
-@rpc("any_peer", "call_remote", "reliable", 0)
-func submit_identity_hello(data: Dictionary) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if not is_host() or sender_id <= 1 or not _pending_authentication.has(sender_id):
- return
- var client_game_version: String = str(data.get("game_version", ""))
- var version_rejection: NetworkProtocol.RejectionCode = (
- NetworkProtocol.game_version_rejection(
- client_game_version,
- NetworkProtocol.game_version(),
- )
- )
- if version_rejection != NetworkProtocol.RejectionCode.NONE:
- _reject_peer(sender_id, version_rejection)
- return
- if (
- not NetworkProtocol.validate_identity_hello(data)
- or int(data["protocol_version"]) != NetworkProtocol.PROTOCOL_VERSION
- ):
- _reject_peer(
- sender_id,
- NetworkProtocol.RejectionCode.PROTOCOL_MISMATCH
- )
- return
- var public_pem := NetworkIdentityCrypto.normalize_public_pem(data["public_key"])
- var fingerprint := NetworkIdentityCrypto.fingerprint_public_pem(public_pem)
- if (
- fingerprint != str(data["fingerprint"])
- or NetworkIdentityCrypto.load_public_key(public_pem) == null
- ):
- _reject_peer(
- sender_id,
- NetworkProtocol.RejectionCode.INVALID_IDENTITY_PROOF
- )
- return
- if _registry.has_fingerprint(fingerprint):
- _reject_peer(sender_id, NetworkProtocol.RejectionCode.DUPLICATE_IDENTITY)
- return
- var server_nonce := NetworkIdentityCrypto.secure_id(32)
- var challenge := {
- "peer_id": sender_id,
- "attempt_id": str(data["attempt_id"]),
- "client_nonce": str(data["client_nonce"]),
- "client_fingerprint": fingerprint,
- "client_public_key": public_pem,
- "client_game_version": client_game_version,
- "server_nonce": server_nonce,
- "server_fingerprint": _host_identity.fingerprint,
- "server_game_version": NetworkProtocol.game_version(),
- "session_id": _session_id,
- "generation": _operation_generation,
- "expires_at_msec": Time.get_ticks_msec() + 60000,
- }
- var fields := _identity_proof_fields(challenge)
- var proof := challenge.duplicate(true)
- proof["server_public_key"] = _host_identity.public_pem
- proof["server_signature"] = _host_identity.sign(
- "handshake_server_proof", fields
- )
- _pending_identity_challenges[sender_id] = challenge
- receive_server_identity_proof.rpc_id(sender_id, proof)
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_server_identity_proof(data: Dictionary) -> void:
- if state != State.AUTHENTICATING:
- return
- var server_game_version: String = str(data.get("server_game_version", ""))
- if server_game_version.is_empty():
- _fail_identity(NetworkProtocol.rejection_text(
- NetworkProtocol.RejectionCode.SERVER_OUTDATED
- ))
- return
- var version_rejection: NetworkProtocol.RejectionCode = (
- NetworkProtocol.game_version_rejection(
- NetworkProtocol.game_version(),
- server_game_version,
- )
- )
- if version_rejection != NetworkProtocol.RejectionCode.NONE:
- _fail_identity(NetworkProtocol.rejection_text(version_rejection))
- return
- if not _valid_server_proof_shape(data):
- _fail_identity("The server sent an invalid identity proof.")
- return
- if (
- str(data["attempt_id"]) != str(_client_identity_attempt.get("attempt_id", ""))
- or str(data["client_nonce"]) != _client_nonce
- ):
- _fail_identity("The server identity proof did not match this connection.")
- return
- var public_pem := NetworkIdentityCrypto.normalize_public_pem(data["server_public_key"])
- var fingerprint := NetworkIdentityCrypto.fingerprint_public_pem(public_pem)
- var public_key := NetworkIdentityCrypto.load_public_key(public_pem)
- if (
- fingerprint != str(data["server_fingerprint"])
- or not NetworkIdentityCrypto.verify_fields(
- public_key,
- "handshake_server_proof",
- _identity_proof_fields(data),
- data["server_signature"],
- )
- ):
- _fail_identity("The server identity proof was invalid.")
- return
- _pending_server_proof = data.duplicate(true)
- _server_identity_fingerprint = fingerprint
- _server_identity_public_key = public_pem
- var verification := _server_trust.verify(
- get_current_endpoint(), fingerprint
- )
- if verification == ServerTrustStore.Verification.MATCH:
- _server_trust.touch(get_current_endpoint())
- _send_client_identity_proof()
- return
- _set_state(
- State.VERIFYING_SERVER_IDENTITY,
- "Confirm this server identity before continuing.",
- )
- _connection_deadline = 0.0
- var expected := str(
- _server_trust.get_record(get_current_endpoint()).get("fingerprint", "")
- )
- server_trust_required.emit(
- get_current_route_display(),
- expected,
- fingerprint,
- verification == ServerTrustStore.Verification.CHANGED,
- )
-
-
-func resolve_server_trust(accepted: bool) -> void:
- if state != State.VERIFYING_SERVER_IDENTITY:
- return
- if not accepted:
- cancel_connection()
- return
- if not _server_trust.trust(
- get_current_endpoint(),
- str(_pending_server_proof["server_fingerprint"]),
- "straywild",
- ):
- _fail_identity("The server identity could not be pinned.")
- return
- _set_state(State.AUTHENTICATING, "Authenticating identity...")
- _connection_deadline = (
- Time.get_ticks_msec() / 1000.0 + CONNECTION_TIMEOUT_SECONDS
- )
- _send_client_identity_proof()
-
-
-func _send_client_identity_proof() -> void:
- var proof := {
- "attempt_id": _pending_server_proof["attempt_id"],
- "session_id": _pending_server_proof["session_id"],
- "client_fingerprint": _player_identity.fingerprint,
- "client_signature": _player_identity.sign(
- "handshake_client_proof",
- _identity_proof_fields(_pending_server_proof),
- ),
- }
- submit_client_identity_proof.rpc_id(1, proof)
-
-
-@rpc("any_peer", "call_remote", "reliable", 0)
-func submit_client_identity_proof(data: Dictionary) -> void:
- var sender_id := multiplayer.get_remote_sender_id()
- var challenge: Dictionary = _pending_identity_challenges.get(sender_id, {})
- if (
- not is_host()
- or challenge.is_empty()
- or Time.get_ticks_msec() > int(challenge["expires_at_msec"])
- or data.get("attempt_id") != challenge["attempt_id"]
- or data.get("session_id") != _session_id
- or data.get("client_fingerprint") != challenge["client_fingerprint"]
- or typeof(data.get("client_signature")) != TYPE_PACKED_BYTE_ARRAY
- ):
- _reject_peer(sender_id, NetworkProtocol.RejectionCode.INVALID_IDENTITY_PROOF)
- return
- var verified := NetworkIdentityCrypto.verify_fields(
- NetworkIdentityCrypto.load_public_key(challenge["client_public_key"]),
- "handshake_client_proof",
- _identity_proof_fields(challenge),
- data["client_signature"],
- )
- _pending_identity_challenges.erase(sender_id)
- if not verified:
- _reject_peer(sender_id, NetworkProtocol.RejectionCode.INVALID_IDENTITY_PROOF)
- return
- _authenticated_identity_cache[sender_id] = {
- "fingerprint": challenge["client_fingerprint"],
- "public_key": challenge["client_public_key"],
- "game_version": challenge["client_game_version"],
- }
- if (
- _host_bans != null
- and _host_bans.is_banned(
- _host_identity.fingerprint,
- str(challenge["client_fingerprint"]),
- )
- ):
- _reject_peer(sender_id, NetworkProtocol.RejectionCode.BANNED)
- return
- request_client_profile.rpc_id(sender_id)
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_moderation_disconnect(message: String) -> void:
- _moderation_disconnect_message = message.left(120)
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func request_client_profile() -> void:
- if state != State.AUTHENTICATING:
- return
- var hello := NetworkProtocol.make_client_hello(
- _profile.profile_id,
- _profile.display_name,
- _client_nonce,
- _local_appearance_snapshot,
- _player_identity.fingerprint,
- )
- hello["identity_signature"] = _player_identity.sign(
- "handshake_client_profile",
- NetworkProtocol.client_profile_fields(hello),
- )
- submit_client_hello.rpc_id(1, hello)
-
-
-@rpc("any_peer", "call_remote", "reliable", 0)
-func submit_client_hello(data: Dictionary) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if not is_host() or sender_id <= 1 or not _pending_authentication.has(sender_id):
- return
- var validation_error: String = NetworkProtocol.validate_client_hello(data)
- if not validation_error.is_empty():
- _reject_peer(sender_id, NetworkProtocol.RejectionCode.MALFORMED_HANDSHAKE)
- return
- var client_game_version: String = str(data.get("game_version", ""))
- var version_rejection: NetworkProtocol.RejectionCode = (
- NetworkProtocol.game_version_rejection(
- client_game_version,
- NetworkProtocol.game_version(),
- )
- )
- if version_rejection != NetworkProtocol.RejectionCode.NONE:
- _reject_peer(sender_id, version_rejection)
- return
- var client_capabilities: PackedStringArray = _sanitized_capabilities(
- data.get("capability_flags", [])
- )
- if NetworkProtocol.FISH_QUALITY_CAPABILITY not in client_capabilities:
- _reject_peer(
- sender_id,
- NetworkProtocol.RejectionCode.UNSUPPORTED_CLIENT,
- )
- return
- if NetworkProtocol.WORLD_GENERATION_CAPABILITY not in client_capabilities:
- _reject_peer(
- sender_id,
- NetworkProtocol.RejectionCode.UNSUPPORTED_CLIENT,
- )
- return
- for required_capability: String in [
- NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
- NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
- NetworkProtocol.HOME_CAPABILITY,
- ]:
- if required_capability not in client_capabilities:
- _reject_peer(
- sender_id,
- NetworkProtocol.RejectionCode.UNSUPPORTED_CLIENT,
- )
- return
- if (
- _host_world_layout == WorldLayout.STARTER_ISLAND
- and NetworkProtocol.WORLD_LAYOUT_CAPABILITY not in client_capabilities
- ):
- _reject_peer(
- sender_id,
- NetworkProtocol.RejectionCode.UNSUPPORTED_CLIENT,
- )
- return
- var identity: Dictionary = _authenticated_identity_cache.get(sender_id, {})
- if identity.is_empty():
- _reject_peer(sender_id, NetworkProtocol.RejectionCode.INVALID_IDENTITY_PROOF)
- return
- if (
- data["identity_fingerprint"] != identity["fingerprint"]
- or client_game_version != str(identity.get("game_version", ""))
- or not NetworkIdentityCrypto.verify_fields(
- NetworkIdentityCrypto.load_public_key(identity["public_key"]),
- "handshake_client_profile",
- NetworkProtocol.client_profile_fields(data),
- data["identity_signature"],
- )
- ):
- _reject_peer(sender_id, NetworkProtocol.RejectionCode.INVALID_IDENTITY_PROOF)
- return
- if _registry.size() >= session_max_players:
- _reject_peer(sender_id, NetworkProtocol.RejectionCode.SERVER_FULL)
- return
- var profile_id: String = data["local_profile_id"]
- var display_name: String = data["display_name"]
- if not NetworkProfilePreferences.is_valid_display_name(display_name):
- _reject_peer(
- sender_id,
- NetworkProtocol.RejectionCode.MALFORMED_HANDSHAKE
- )
- return
- if not _registry.add_peer(
- sender_id,
- profile_id,
- display_name,
- NetworkProtocol.PROTOCOL_VERSION,
- identity["fingerprint"],
- identity["public_key"],
- client_capabilities,
- ):
- _reject_peer(
- sender_id,
- NetworkProtocol.RejectionCode.MALFORMED_HANDSHAKE
- )
- return
- var operator_enabled: bool = _operator_for_fingerprint(
- str(identity["fingerprint"])
- )
- if operator_enabled:
- _operator_peer_ids[sender_id] = true
- var submitted_appearance := CharacterCustomizationCatalog.sanitized_snapshot(
- data["cosmetic_snapshot"]
- )
- _registry.update_appearance(sender_id, submitted_appearance)
- var peer_record := _registry.get_peer(sender_id)
- if peer_record != null:
- peer_record.profile_authorization = {
- "domain": "handshake_client_profile",
- "signature": data["identity_signature"],
- "client_nonce": data["client_nonce"],
- }
- _archive_authenticated_identity(peer_record)
- var identity_status := _known_players.observe(
- str(identity["fingerprint"]), display_name
- )
- peer_identity_observed.emit(sender_id, identity_status)
- _pending_authentication.erase(sender_id)
- _authenticated_identity_cache.erase(sender_id)
- var spawn_index: int = _registry.get_peer_ids().find(sender_id)
- var spawn_transform: Transform3D = (
- _spawn_service.get_spawn_transform_for_index(spawn_index)
- )
- _spawn_service.spawn_remote_player(sender_id, spawn_transform, true)
- var spawned_avatar: Player = _spawn_service.get_avatar(sender_id)
- if spawned_avatar != null:
- spawned_avatar.apply_appearance_snapshot(submitted_appearance)
- peer_profile_changed.emit(
- sender_id,
- display_name,
- submitted_appearance.duplicate(true),
- )
- receive_server_hello.rpc_id(
- sender_id,
- NetworkProtocol.make_server_hello(
- true,
- NetworkProtocol.RejectionCode.NONE,
- _session_id,
- sender_id,
- _registry.size(),
- session_max_players,
- _session_display_name,
- _host_world_seed,
- _host_world_layout,
- )
- )
- receive_spawn_list.rpc_id(sender_id, _build_spawn_list())
- receive_operator_snapshot.rpc_id(sender_id, _operator_peer_id_snapshot())
- receive_peer_spawn.rpc(
- _make_spawn_entry(sender_id, display_name, spawn_transform)
- )
- if operator_enabled:
- receive_operator_status.rpc(sender_id, true)
- peer_authenticated.emit(sender_id, display_name)
- _emit_peer_count()
-
-
-func _reject_peer(
- peer_id: int,
- code: NetworkProtocol.RejectionCode,
-) -> void:
- receive_server_hello.rpc_id(
- peer_id,
- NetworkProtocol.make_server_hello(
- false,
- code,
- _session_id,
- peer_id,
- _registry.size(),
- session_max_players
- )
- )
- _pending_authentication.erase(peer_id)
- call_deferred("_disconnect_rejected_peer", peer_id)
-
-
-func _disconnect_rejected_peer(peer_id: int) -> void:
- await get_tree().create_timer(0.15).timeout
- if (
- is_host()
- and multiplayer.multiplayer_peer != null
- and multiplayer.multiplayer_peer.get_connection_status()
- == MultiplayerPeer.CONNECTION_CONNECTED
- and peer_id in multiplayer.get_peers()
- ):
- multiplayer.multiplayer_peer.disconnect_peer(peer_id)
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_server_hello(data: Dictionary) -> void:
- if state != State.AUTHENTICATING:
- return
- if (
- typeof(data.get("accepted")) != TYPE_BOOL
- or typeof(data.get("protocol_version")) != TYPE_INT
- or typeof(data.get("game_version")) != TYPE_STRING
- or typeof(data.get("rejection_code")) != TYPE_INT
- or typeof(data.get("world_seed")) != TYPE_INT
- or typeof(
- data.get("world_layout", String(WorldLayout.GENERATED))
- ) not in [TYPE_STRING, TYPE_STRING_NAME]
- ):
- _teardown_peer()
- _fail("The server sent an invalid handshake response.")
- return
- if not bool(data["accepted"]):
- var message: String = NetworkProtocol.rejection_text(
- int(data["rejection_code"])
- )
- _teardown_peer()
- _fail(message)
- return
- var version_rejection: NetworkProtocol.RejectionCode = (
- NetworkProtocol.game_version_rejection(
- NetworkProtocol.game_version(),
- str(data["game_version"]),
- )
- )
- if version_rejection != NetworkProtocol.RejectionCode.NONE:
- _teardown_peer()
- _fail(NetworkProtocol.rejection_text(version_rejection))
- return
- if int(data["protocol_version"]) != NetworkProtocol.PROTOCOL_VERSION:
- _teardown_peer()
- _fail("The server uses a different network protocol.")
- return
- var received_world_seed := int(data["world_seed"])
- if received_world_seed <= 0 or received_world_seed > NetworkProtocol.MAX_WORLD_SEED:
- _teardown_peer()
- _fail("The server sent an invalid world seed.")
- return
- var received_world_layout := WorldLayout.normalized(
- data.get("world_layout", String(WorldLayout.GENERATED)),
- &"",
- )
- if received_world_layout.is_empty():
- _teardown_peer()
- _fail("The server sent an invalid world layout.")
- return
- _session_id = str(data.get("session_id", ""))
- _last_server_max_players = int(data.get(
- "max_players",
- DEFAULT_SESSION_MAX_PLAYERS
- ))
- _last_server_player_count = int(data.get("player_count", 1))
- _last_server_display_name = str(data.get("server_display_name", ""))
- _last_server_protocol_version = int(data.get(
- "protocol_version", NetworkProtocol.PROTOCOL_VERSION
- ))
- _last_server_world_seed = received_world_seed
- _last_server_world_layout = received_world_layout
- _server_capabilities = PackedStringArray()
- var advertised_capabilities: Variant = data.get(
- "capability_flags", PackedStringArray()
- )
- if typeof(advertised_capabilities) in [
- TYPE_ARRAY, TYPE_PACKED_STRING_ARRAY
- ]:
- for value: Variant in advertised_capabilities:
- if typeof(value) in [TYPE_STRING, TYPE_STRING_NAME]:
- _server_capabilities.append(str(value))
- if NetworkProtocol.FISH_QUALITY_CAPABILITY not in _server_capabilities:
- _teardown_peer()
- _fail("This server does not support fish quality data.")
- return
- if NetworkProtocol.WORLD_GENERATION_CAPABILITY not in _server_capabilities:
- _teardown_peer()
- _fail("This server does not support generated worlds.")
- return
- for required_capability: String in [
- NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
- NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
- NetworkProtocol.HOME_CAPABILITY,
- ]:
- if required_capability not in _server_capabilities:
- _teardown_peer()
- _fail("This server is missing required network capabilities.")
- return
- if (
- received_world_layout == WorldLayout.STARTER_ISLAND
- and NetworkProtocol.WORLD_LAYOUT_CAPABILITY not in _server_capabilities
- ):
- _teardown_peer()
- _fail("This server does not support selectable world layouts.")
- return
- var local_peer_id: int = multiplayer.get_unique_id()
- _registry.clear()
- _registry.add_peer(
- local_peer_id,
- _profile.profile_id,
- _profile.display_name,
- NetworkProtocol.PROTOCOL_VERSION,
- _player_identity.fingerprint,
- _player_identity.public_pem,
- PackedStringArray([
- NetworkProtocol.FISH_QUALITY_CAPABILITY,
- NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
- NetworkProtocol.WORLD_TIME_CAPABILITY,
- NetworkProtocol.WORLD_WEATHER_CAPABILITY,
- NetworkProtocol.BACKPACK_SHOP_CAPABILITY,
- NetworkProtocol.WORLD_GENERATION_CAPABILITY,
- NetworkProtocol.WORLD_LAYOUT_CAPABILITY,
- NetworkProtocol.FRIENDS_CAPABILITY,
- NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
- NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
- NetworkProtocol.HOME_CAPABILITY,
- ]),
- )
- _registry.update_appearance(local_peer_id, _local_appearance_snapshot)
- var local_record := _registry.get_peer(local_peer_id)
- if local_record != null:
- _archive_authenticated_identity(local_record)
- _spawn_service.clear_remote_players()
- _spawn_service.register_local_player(local_peer_id)
- var local_avatar := _spawn_service.get_avatar(local_peer_id)
- if local_avatar != null:
- local_avatar.apply_appearance_snapshot(_local_appearance_snapshot)
- _connection_deadline = 0.0
- _set_state(
- State.JOINED_CLIENT,
- "Connected • %d / %d players" % [
- int(data.get("player_count", 1)),
- _last_server_max_players,
- ]
- )
- join_authenticated.emit()
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_spawn_list(entries: Array) -> void:
- if state != State.JOINED_CLIENT:
- return
- for value: Variant in entries:
- if typeof(value) == TYPE_DICTIONARY:
- _apply_spawn_entry(value)
- _emit_peer_count()
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_peer_spawn(entry: Dictionary) -> void:
- if state != State.JOINED_CLIENT:
- return
- _apply_spawn_entry(entry)
- _emit_peer_count()
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_peer_despawn(peer_id: int) -> void:
- if state != State.JOINED_CLIENT:
- return
- _operator_peer_ids.erase(peer_id)
- _peer_space_ids.erase(peer_id)
- _last_animation_state_by_peer.erase(peer_id)
- _pending_animation_state_by_peer.erase(peer_id)
- _registry.remove_peer(peer_id)
- _spawn_service.remove_peer(peer_id)
- peer_removed.emit(peer_id)
- _emit_peer_count()
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_operator_snapshot(peer_ids: PackedInt32Array) -> void:
- if (
- state != State.JOINED_CLIENT
- or peer_ids.size() > get_session_max_players()
- ):
- return
- _operator_peer_ids.clear()
- for peer_id: int in peer_ids:
- if peer_id > 1:
- _operator_peer_ids[peer_id] = true
- _update_local_operator()
- for peer_id: int in _operator_peer_ids:
- operator_status_changed.emit(peer_id, true)
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_operator_status(peer_id: int, enabled: bool) -> void:
- if state != State.JOINED_CLIENT or peer_id <= 1:
- return
- if enabled:
- _operator_peer_ids[peer_id] = true
- else:
- _operator_peer_ids.erase(peer_id)
- _update_local_operator()
- operator_status_changed.emit(peer_id, enabled)
-
-
-func _apply_spawn_entry(entry: Dictionary) -> void:
- if (
- typeof(entry.get("peer_id")) != TYPE_INT
- or typeof(entry.get("profile_id")) != TYPE_STRING
- or typeof(entry.get("display_name")) != TYPE_STRING
- or typeof(entry.get("position")) != TYPE_ARRAY
- or typeof(entry.get("yaw")) not in [TYPE_FLOAT, TYPE_INT]
- or typeof(entry.get("sitting")) != TYPE_BOOL
- or not NetworkPlayerAnimationProtocol.validate_state(
- entry.get("animation_state")
- )
- ):
- return
- if not _verify_spawn_identity(entry):
- return
- var peer_id: int = entry["peer_id"]
- if peer_id == multiplayer.get_unique_id():
- var own_position: Array = entry["position"]
- if own_position.size() == 3:
- var own_avatar: Player = _spawn_service.get_avatar(peer_id)
- if own_avatar != null:
- var own_snapshot: Dictionary = own_avatar.make_network_snapshot(
- peer_id
- )
- own_snapshot["position"] = own_position
- own_snapshot["visual_yaw"] = float(entry["yaw"])
- own_snapshot["sitting"] = bool(entry["sitting"])
- own_avatar.apply_network_teleport(own_snapshot)
- return
- var peer_was_added: bool = false
- if not _registry.has_peer(peer_id):
- if not _registry.add_peer(
- peer_id,
- entry["profile_id"],
- entry["display_name"],
- NetworkProtocol.PROTOCOL_VERSION,
- str(entry.get("identity_fingerprint", "")),
- str(entry.get("identity_public_key", "")),
- _sanitized_capabilities(entry.get("capability_flags", [])),
- ):
- return
- peer_was_added = true
- var added_record := _registry.get_peer(peer_id)
- if added_record != null:
- added_record.profile_authorization = Dictionary(
- entry.get("profile_authorization", {})
- ).duplicate(true)
- if added_record != null and added_record.identity_authenticated:
- _archive_authenticated_identity(added_record)
- var status := _known_players.observe(
- added_record.identity_fingerprint,
- added_record.display_name,
- )
- peer_identity_observed.emit(peer_id, status)
- if typeof(entry.get("appearance")) == TYPE_DICTIONARY:
- var appearance := CharacterCustomizationCatalog.sanitized_snapshot(
- entry["appearance"]
- )
- _registry.update_appearance(peer_id, appearance)
- var transform: Transform3D = _spawn_service.get_spawn_transform_for_index(0)
- var spawn_position: Array = entry["position"]
- if spawn_position.size() != 3:
- return
- transform.origin = Vector3(
- float(spawn_position[0]),
- float(spawn_position[1]),
- float(spawn_position[2])
- )
- transform.basis = Basis(Vector3.UP, float(entry["yaw"]))
- var avatar := _spawn_service.spawn_remote_player(peer_id, transform, false)
- var record := _registry.get_peer(peer_id)
- if avatar != null and record != null:
- avatar.apply_appearance_snapshot(record.appearance_snapshot)
- # Lifecycle and animation updates use separate reliable ENet channels.
- # Include the current authoritative state in the spawn record so a peer
- # joining during an action presents it immediately, regardless of which
- # channel arrives first.
- avatar.apply_network_animation_state(entry["animation_state"])
- avatar.apply_network_sitting_state(bool(entry["sitting"]))
- var pending_animation: Dictionary = (
- _pending_animation_state_by_peer.get(peer_id, {})
- )
- if not pending_animation.is_empty():
- avatar.apply_network_animation_state(pending_animation)
- _pending_animation_state_by_peer.erase(peer_id)
- if peer_was_added and record != null:
- peer_authenticated.emit(peer_id, record.display_name)
-
-
-func _build_spawn_list() -> Array[Dictionary]:
- var entries: Array[Dictionary] = []
- for peer_id: int in _registry.get_peer_ids():
- var record: PeerRegistry.PeerRecord = _registry.get_peer(peer_id)
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- if record != null and avatar != null:
- entries.append(_make_spawn_entry(
- peer_id,
- record.display_name,
- avatar.global_transform
- ))
- return entries
-
-
-func _make_spawn_entry(
- peer_id: int,
- display_name: String,
- transform: Transform3D,
-) -> Dictionary:
- var record: PeerRegistry.PeerRecord = _registry.get_peer(peer_id)
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- var animation_state: Dictionary = (
- avatar.make_network_animation_state()
- if avatar != null
- else NetworkPlayerAnimationProtocol.make_state(
- NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE,
- true,
- )
- )
- return {
- "peer_id": peer_id,
- "profile_id": record.profile_id if record != null else "",
- "display_name": display_name,
- "position": [
- transform.origin.x,
- transform.origin.y,
- transform.origin.z,
- ],
- "yaw": transform.basis.get_euler().y,
- "sitting": (
- avatar.get_network_sitting_state()
- if avatar != null
- else false
- ),
- "animation_state": animation_state,
- "appearance": (
- record.appearance_snapshot.duplicate(true)
- if record != null
- else CharacterCustomizationCatalog.default_snapshot()
- ),
- "identity_fingerprint": (
- record.identity_fingerprint if record != null else ""
- ),
- "identity_public_key": (
- record.identity_public_key if record != null else ""
- ),
- "profile_authorization": (
- record.profile_authorization.duplicate(true)
- if record != null else {}
- ),
- "capability_flags": (
- record.capability_flags.duplicate()
- if record != null else PackedStringArray()
- ),
- }
-
-
-func _sanitized_capabilities(value: Variant) -> PackedStringArray:
- var result := PackedStringArray()
- if typeof(value) not in [TYPE_ARRAY, TYPE_PACKED_STRING_ARRAY]:
- return result
- for capability: Variant in value:
- if (
- typeof(capability) in [TYPE_STRING, TYPE_STRING_NAME]
- and not str(capability).is_empty()
- and str(capability).length() <= 64
- and str(capability) not in result
- ):
- result.append(str(capability))
- return result
-
-
-func _operator_for_fingerprint(fingerprint: String) -> bool:
- return bool((
- _configured_operator_fingerprints
- if _dedicated_host
- else _session_operator_fingerprints
- ).get(fingerprint, false))
-
-
-func _operator_peer_id_snapshot() -> PackedInt32Array:
- var result: PackedInt32Array = PackedInt32Array()
- for peer_id: int in _operator_peer_ids:
- result.append(peer_id)
- result.sort()
- return result
-
-
-func _set_operator_status(peer_id: int, enabled: bool) -> void:
- if enabled:
- _operator_peer_ids[peer_id] = true
- else:
- _operator_peer_ids.erase(peer_id)
- operator_status_changed.emit(peer_id, enabled)
- receive_operator_status.rpc(peer_id, enabled)
-
-
-func _update_local_operator() -> void:
- var local_peer_id: int = multiplayer.get_unique_id()
- _local_operator = bool(_operator_peer_ids.get(local_peer_id, false))
-
-
-func verify_profile_authorization(
- peer_id: int,
- display_name: String,
- appearance: Dictionary,
- authorization: Dictionary,
-) -> bool:
- var record := _registry.get_peer(peer_id)
- if record == null:
- return false
- return _verify_profile_authorization_fields(
- peer_id,
- record.profile_id,
- display_name,
- appearance,
- record.identity_fingerprint,
- record.identity_public_key,
- authorization,
- )
-
-
-func _verify_spawn_identity(entry: Dictionary) -> bool:
- if typeof(entry.get("profile_authorization")) != TYPE_DICTIONARY:
- return false
- return _verify_profile_authorization_fields(
- int(entry.get("peer_id", 0)),
- str(entry.get("profile_id", "")),
- str(entry.get("display_name", "")),
- Dictionary(entry.get("appearance", {})),
- str(entry.get("identity_fingerprint", "")),
- str(entry.get("identity_public_key", "")),
- Dictionary(entry.get("profile_authorization", {})),
- )
-
-
-func _verify_profile_authorization_fields(
- peer_id: int,
- profile_id: String,
- display_name: String,
- appearance: Dictionary,
- fingerprint: String,
- identity_public_key: String,
- authorization: Dictionary,
-) -> bool:
- var public_pem := NetworkIdentityCrypto.normalize_public_pem(
- identity_public_key
- )
- if NetworkIdentityCrypto.fingerprint_public_pem(public_pem) != fingerprint:
- return false
- if authorization.is_empty():
- return peer_id == 1
- if authorization.get("domain") == "handshake_client_profile":
- var hello := NetworkProtocol.make_client_hello(
- profile_id,
- display_name,
- str(authorization.get("client_nonce", "")),
- appearance,
- fingerprint,
- authorization.get("signature", PackedByteArray()),
- )
- return NetworkIdentityCrypto.verify_fields(
- NetworkIdentityCrypto.load_public_key(public_pem),
- "handshake_client_profile",
- NetworkProtocol.client_profile_fields(hello),
- hello["identity_signature"],
- )
- if authorization.get("domain") == "appearance_preview":
- if str(authorization.get("sender_fingerprint", "")) != fingerprint:
- return false
- var preview := authorization.duplicate(true)
- preview["appearance"] = appearance
- return NetworkIdentityCrypto.verify_fields(
- NetworkIdentityCrypto.load_public_key(public_pem),
- "appearance_preview",
- NetworkProfileProtocol.appearance_preview_signature_fields(preview),
- preview.get("sender_signature", PackedByteArray()),
- )
- if authorization.has("sender_signature"):
- if str(authorization.get("sender_fingerprint", "")) != fingerprint:
- return false
- var signed := authorization.duplicate(true)
- signed["display_name"] = display_name
- signed["appearance"] = appearance
- return NetworkIdentityCrypto.verify_fields(
- NetworkIdentityCrypto.load_public_key(public_pem),
- "profile_update",
- NetworkProfileProtocol.signature_fields(signed),
- signed["sender_signature"],
- )
- return false
-
-
-func _identity_proof_fields(data: Dictionary) -> Array:
- return [
- NetworkProtocol.PROTOCOL_VERSION,
- str(data.get("client_game_version", "")),
- str(data.get("server_game_version", "")),
- str(data.get("attempt_id", "")),
- str(data.get("client_fingerprint", "")),
- str(data.get("client_nonce", "")),
- str(data.get("server_fingerprint", "")),
- str(data.get("server_nonce", "")),
- str(data.get("session_id", "")),
- int(data.get("peer_id", 0)),
- int(data.get("generation", 0)),
- int(data.get("expires_at_msec", 0)),
- ]
-
-
-func _archive_authenticated_identity(record: PeerRegistry.PeerRecord) -> void:
- if (
- record != null
- and record.identity_authenticated
- and NetworkIdentityCrypto.fingerprint_public_pem(
- record.identity_public_key
- ) == record.identity_fingerprint
- ):
- _session_identity_keys[record.identity_fingerprint] = (
- NetworkIdentityCrypto.normalize_public_pem(
- record.identity_public_key
- )
- )
-
-
-func _valid_server_proof_shape(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- return (
- typeof(value.get("attempt_id")) == TYPE_STRING
- and str(value["attempt_id"]).length() == 32
- and typeof(value.get("client_nonce")) == TYPE_STRING
- and str(value["client_nonce"]).length() == 64
- and typeof(value.get("server_nonce")) == TYPE_STRING
- and str(value["server_nonce"]).length() == 64
- and NetworkIdentityCrypto.valid_fingerprint(value.get("client_fingerprint"))
- and NetworkIdentityCrypto.valid_fingerprint(value.get("server_fingerprint"))
- and typeof(value.get("client_game_version")) == TYPE_STRING
- and str(value["client_game_version"]) == NetworkProtocol.game_version()
- and typeof(value.get("server_game_version")) == TYPE_STRING
- and str(value["server_game_version"]) == NetworkProtocol.game_version()
- and typeof(value.get("server_public_key")) == TYPE_STRING
- and str(value["server_public_key"]).to_utf8_buffer().size()
- <= NetworkProtocol.MAX_PUBLIC_KEY_LENGTH
- and typeof(value.get("server_signature")) == TYPE_PACKED_BYTE_ARRAY
- and PackedByteArray(value["server_signature"]).size()
- <= NetworkIdentityCrypto.MAX_SIGNATURE_BYTES
- and typeof(value.get("session_id")) == TYPE_STRING
- and typeof(value.get("peer_id")) == TYPE_INT
- and typeof(value.get("generation")) == TYPE_INT
- and typeof(value.get("expires_at_msec")) == TYPE_INT
- )
-
-
-func _fail_identity(message: String) -> void:
- _teardown_peer()
- _fail(message)
-
-
-func _maybe_send_local_input() -> void:
- var peer_id: int = multiplayer.get_unique_id()
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- if avatar == null:
- return
- var state_hash: int = avatar.get_network_input_state_hash()
- var state_changed: bool = state_hash != _last_input_state_hash
- if (
- avatar.has_active_network_input()
- or state_changed
- or _idle_input_accumulator >= IDLE_INPUT_INTERVAL
- ):
- _send_local_input()
- _last_input_state_hash = state_hash
- _idle_input_accumulator = 0.0
-
-
-func _send_local_input() -> void:
- var peer_id: int = multiplayer.get_unique_id()
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- if avatar == null:
- return
- _input_sequence = (
- 1
- if _input_sequence >= MAX_MOVEMENT_INPUT_SEQUENCE
- else _input_sequence + 1
- )
- var input: Dictionary = avatar.capture_network_input(_input_sequence)
- var encoded: Array = _encode_movement_input(input)
- if encoded.is_empty():
- return
- _pending_movement_inputs.append(input.duplicate(true))
- while _pending_movement_inputs.size() > MAX_PENDING_MOVEMENT_INPUTS:
- _pending_movement_inputs.pop_front()
- _movement_inputs_sent += 1
- submit_movement_input.rpc_id(1, encoded)
-
-
-func _maybe_send_local_animation_action() -> void:
- var peer_id: int = multiplayer.get_unique_id()
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- if avatar == null:
- return
- var state: Dictionary = avatar.make_network_animation_state()
- var action: Dictionary = state.get("action", {})
- if not NetworkPlayerAnimationProtocol.validate_action_state(action):
- return
- var signature: Array = [
- str(action["id"]),
- int(action["sequence"]),
- bool(action.get("paused", false)),
- avatar.get_network_sitting_intent(),
- ]
- if signature == _last_local_animation_action_signature:
- return
- var encoded: Array = _encode_movement_animation_action(
- action,
- avatar.get_network_sitting_intent(),
- )
- if encoded.is_empty():
- return
- _last_local_animation_action_signature = signature
- submit_movement_animation_action.rpc_id(1, encoded)
-
-
-func submit_neutral_local_movement() -> void:
- if state != State.JOINED_CLIENT:
- return
- _send_local_input()
-
-
-@rpc("any_peer", "call_remote", "unreliable_ordered", 1)
-func submit_movement_input(encoded: Array) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if not is_host() or not _registry.has_peer(sender_id):
- return
- var avatar: Player = _spawn_service.get_avatar(sender_id)
- var data: Dictionary = _decode_movement_input(encoded)
- if avatar == null or data.is_empty():
- return
- _movement_inputs_received += 1
- avatar.apply_authoritative_network_input(
- data,
- Player.resolve_network_input_stale_timeout_seconds(
- get_peer_rtt_ms(sender_id)
- ),
- )
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- NetworkProtocol.MOVEMENT_ANIMATION_CHANNEL,
-)
-func submit_movement_animation_action(encoded: Array) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if not is_host() or not _registry.has_peer(sender_id):
- return
- var avatar: Player = _spawn_service.get_avatar(sender_id)
- var state: Dictionary = _decode_movement_animation_action(encoded)
- if avatar == null or state.is_empty():
- return
- avatar.apply_authoritative_network_animation_action(state["action"])
- avatar.apply_authoritative_network_sitting_state(bool(state["sitting"]))
-
-
-static func _encode_movement_input(data: Dictionary) -> Array:
- if not _is_valid_movement_input(data):
- return []
- var axis: Array = data["axis"]
- var flags: int = 0
- if bool(data["jump"]):
- flags |= MOVEMENT_FLAG_JUMP
- if bool(data["sprint"]):
- flags |= MOVEMENT_FLAG_SPRINT
- if bool(data["sneak"]):
- flags |= MOVEMENT_FLAG_SNEAK
- if bool(data["slow_walk"]):
- flags |= MOVEMENT_FLAG_SLOW_WALK
- if bool(data["sitting"]):
- flags |= MOVEMENT_FLAG_SITTING
- if bool(data["casting"]):
- flags |= MOVEMENT_FLAG_CASTING
- var action: Dictionary = data["animation_action"]
- return [
- int(data["sequence"]),
- Vector2(float(axis[0]), float(axis[1])),
- float(data["camera_yaw"]),
- flags,
- str(action["id"]),
- int(action["sequence"]),
- float(action["elapsed"]),
- bool(action.get("paused", false)),
- ]
-
-
-static func _decode_movement_input(value: Variant) -> Dictionary:
- if typeof(value) != TYPE_ARRAY:
- return {}
- var fields: Array = value
- if (
- fields.size() != MOVEMENT_INPUT_FIELD_COUNT
- or typeof(fields[0]) != TYPE_INT
- or typeof(fields[1]) != TYPE_VECTOR2
- or typeof(fields[2]) not in [TYPE_FLOAT, TYPE_INT]
- or typeof(fields[3]) != TYPE_INT
- or typeof(fields[4]) not in [TYPE_STRING, TYPE_STRING_NAME]
- or typeof(fields[5]) != TYPE_INT
- or typeof(fields[6]) not in [TYPE_FLOAT, TYPE_INT]
- or typeof(fields[7]) != TYPE_BOOL
- ):
- return {}
- var axis: Vector2 = fields[1]
- var flags: int = int(fields[3])
- if flags < 0 or flags & ~MOVEMENT_ALLOWED_FLAGS != 0:
- return {}
- var decoded: Dictionary = {
- "sequence": int(fields[0]),
- "axis": [axis.x, axis.y],
- "camera_yaw": float(fields[2]),
- "jump": bool(flags & MOVEMENT_FLAG_JUMP),
- "sprint": bool(flags & MOVEMENT_FLAG_SPRINT),
- "sneak": bool(flags & MOVEMENT_FLAG_SNEAK),
- "slow_walk": bool(flags & MOVEMENT_FLAG_SLOW_WALK),
- "sitting": bool(flags & MOVEMENT_FLAG_SITTING),
- "casting": bool(flags & MOVEMENT_FLAG_CASTING),
- "animation_action": NetworkPlayerAnimationProtocol.make_action_state(
- StringName(str(fields[4])),
- int(fields[5]),
- float(fields[6]),
- bool(fields[7]),
- ),
- }
- return decoded if _is_valid_movement_input(decoded) else {}
-
-
-static func _is_valid_movement_input(data: Dictionary) -> bool:
- if (
- typeof(data.get("sequence")) != TYPE_INT
- or typeof(data.get("axis")) != TYPE_ARRAY
- or typeof(data.get("jump")) != TYPE_BOOL
- or typeof(data.get("sprint")) != TYPE_BOOL
- or typeof(data.get("sneak")) != TYPE_BOOL
- or typeof(data.get("slow_walk")) != TYPE_BOOL
- or typeof(data.get("sitting")) != TYPE_BOOL
- or typeof(data.get("casting")) != TYPE_BOOL
- or not NetworkPlayerAnimationProtocol.validate_action_state(
- data.get("animation_action")
- )
- or typeof(data.get("camera_yaw")) not in [TYPE_FLOAT, TYPE_INT]
- ):
- return false
- var axis: Array = data["axis"]
- if axis.size() != 2:
- return false
- if (
- typeof(axis[0]) not in [TYPE_FLOAT, TYPE_INT]
- or typeof(axis[1]) not in [TYPE_FLOAT, TYPE_INT]
- ):
- return false
- var x: float = float(axis[0])
- var y: float = float(axis[1])
- var camera_yaw: float = float(data["camera_yaw"])
- return (
- int(data["sequence"]) > 0
- and int(data["sequence"]) <= MAX_MOVEMENT_INPUT_SEQUENCE
- and is_finite(x)
- and is_finite(y)
- and is_finite(camera_yaw)
- and absf(x) <= 1.01
- and absf(y) <= 1.01
- and absf(camera_yaw) <= TAU * 100.0
- )
-
-
-func _broadcast_movement_snapshots() -> void:
- _movement_snapshot_tick += 1
- var peer_ids: Array[int] = _registry.get_peer_ids()
- for recipient_id: int in peer_ids:
- # Peer 1 is the local listen-server player. Dedicated servers do not
- # register a peer 1, so every remaining record is a remote recipient.
- if recipient_id == 1:
- continue
- var recipient_avatar: Player = _spawn_service.get_avatar(recipient_id)
- if recipient_avatar == null:
- continue
- var snapshots: Array = []
- for subject_id: int in peer_ids:
- if not peers_share_space(recipient_id, subject_id):
- continue
- var subject_avatar: Player = _spawn_service.get_avatar(subject_id)
- if subject_avatar == null:
- continue
- if subject_id == recipient_id:
- # The owner already simulates locally. Its authoritative state is
- # an audit and acknowledgement, not a presentation stream, so it
- # does not need the full 30 Hz observer snapshot rate.
- if _movement_snapshot_tick % OWNER_SNAPSHOT_DIVISOR != 0:
- continue
- elif not _should_send_remote_snapshot(
- recipient_avatar.global_position,
- subject_avatar.global_position,
- ):
- continue
- var encoded: Array = _encode_movement_snapshot(
- subject_avatar.make_network_snapshot(subject_id)
- )
- if not encoded.is_empty():
- snapshots.append(encoded)
- _send_movement_snapshot_batches(recipient_id, snapshots)
- _broadcast_movement_animation_updates(peer_ids)
-
-
-func _should_send_remote_snapshot(
- recipient_position: Vector3,
- subject_position: Vector3,
-) -> bool:
- var distance_squared: float = recipient_position.distance_squared_to(
- subject_position
- )
- var divisor: int = DISTANT_REMOTE_SNAPSHOT_DIVISOR
- if distance_squared <= NEAR_REMOTE_DISTANCE * NEAR_REMOTE_DISTANCE:
- divisor = NEAR_REMOTE_SNAPSHOT_DIVISOR
- elif distance_squared <= FAR_REMOTE_DISTANCE * FAR_REMOTE_DISTANCE:
- divisor = FAR_REMOTE_SNAPSHOT_DIVISOR
- return _movement_snapshot_tick % divisor == 0
-
-
-func _send_movement_snapshot_batches(
- recipient_id: int,
- snapshots: Array,
-) -> void:
- for start_index: int in range(
- 0,
- snapshots.size(),
- MOVEMENT_SNAPSHOT_BATCH_SIZE,
- ):
- var batch: Array = snapshots.slice(
- start_index,
- mini(
- start_index + MOVEMENT_SNAPSHOT_BATCH_SIZE,
- snapshots.size(),
- ),
- )
- receive_movement_snapshots.rpc_id(recipient_id, batch)
- _movement_snapshot_packets_sent += 1
- _movement_snapshot_states_sent += batch.size()
-
-
-func _broadcast_movement_animation_updates(peer_ids: Array[int]) -> void:
- var refresh_all: bool = (
- _animation_refresh_accumulator >= ANIMATION_REFRESH_INTERVAL
- )
- if refresh_all:
- _animation_refresh_accumulator = fmod(
- _animation_refresh_accumulator,
- ANIMATION_REFRESH_INTERVAL,
- )
- var updates_by_subject: Dictionary[int, Array] = {}
- for subject_id: int in peer_ids:
- var avatar: Player = _spawn_service.get_avatar(subject_id)
- if avatar == null:
- continue
- var state: Dictionary = avatar.make_network_animation_state()
- var previous: Dictionary = _last_animation_state_by_peer.get(
- subject_id, {}
- )
- if (
- not refresh_all
- and _movement_animation_signature(state)
- == _movement_animation_signature(previous)
- ):
- continue
- _last_animation_state_by_peer[subject_id] = state.duplicate(true)
- var encoded: Array = _encode_movement_animation(subject_id, state)
- if not encoded.is_empty():
- updates_by_subject[subject_id] = encoded
- if updates_by_subject.is_empty():
- return
- for recipient_id: int in peer_ids:
- if recipient_id == 1:
- continue
- var updates: Array = []
- for subject_id: int in updates_by_subject:
- if peers_share_space(recipient_id, subject_id):
- updates.append(updates_by_subject[subject_id])
- if updates.is_empty():
- continue
- receive_movement_animations.rpc_id(recipient_id, updates)
- _movement_animation_packets_sent += 1
- _movement_animation_states_sent += updates.size()
-
-
-static func _movement_animation_signature(state: Dictionary) -> Array:
- if not NetworkPlayerAnimationProtocol.validate_state(state):
- return []
- var action: Dictionary = state["action"]
- return [
- str(state["locomotion_id"]),
- bool(state["grounded"]),
- str(action["id"]),
- int(action["sequence"]),
- bool(action.get("paused", false)),
- ]
-
-
-static func _encode_movement_snapshot(snapshot: Dictionary) -> Array:
- var position: Variant = snapshot.get("position")
- var snapshot_velocity: Variant = snapshot.get("velocity")
- if (
- typeof(position) != TYPE_ARRAY
- or position.size() != 3
- or typeof(snapshot_velocity) != TYPE_ARRAY
- or snapshot_velocity.size() != 3
- ):
- return []
- var flags: int = 0
- if bool(snapshot.get("grounded", false)):
- flags |= SNAPSHOT_FLAG_GROUNDED
- if bool(snapshot.get("sitting", false)):
- flags |= SNAPSHOT_FLAG_SITTING
- if bool(snapshot.get("casting", false)):
- flags |= SNAPSHOT_FLAG_CASTING
- return [
- int(snapshot.get("peer_id", 0)),
- int(snapshot.get("acknowledged_input", 0)),
- Vector3(float(position[0]), float(position[1]), float(position[2])),
- Vector3(
- float(snapshot_velocity[0]),
- float(snapshot_velocity[1]),
- float(snapshot_velocity[2]),
- ),
- float(snapshot.get("visual_yaw", 0.0)),
- flags,
- ]
-
-
-static func _decode_movement_snapshot(value: Variant) -> Dictionary:
- if typeof(value) != TYPE_ARRAY:
- return {}
- var fields: Array = value
- if (
- fields.size() != MOVEMENT_SNAPSHOT_FIELD_COUNT
- or typeof(fields[0]) != TYPE_INT
- or typeof(fields[1]) != TYPE_INT
- or typeof(fields[2]) != TYPE_VECTOR3
- or typeof(fields[3]) != TYPE_VECTOR3
- or typeof(fields[4]) not in [TYPE_FLOAT, TYPE_INT]
- or typeof(fields[5]) != TYPE_INT
- ):
- return {}
- var position: Vector3 = fields[2]
- var snapshot_velocity: Vector3 = fields[3]
- var flags: int = int(fields[5])
- if (
- int(fields[0]) <= 0
- or int(fields[1]) < 0
- or not position.is_finite()
- or not snapshot_velocity.is_finite()
- or not is_finite(float(fields[4]))
- or flags < 0
- or flags & ~SNAPSHOT_ALLOWED_FLAGS != 0
- ):
- return {}
- return {
- "peer_id": int(fields[0]),
- "acknowledged_input": int(fields[1]),
- "position": [position.x, position.y, position.z],
- "velocity": [
- snapshot_velocity.x,
- snapshot_velocity.y,
- snapshot_velocity.z,
- ],
- "visual_yaw": float(fields[4]),
- "grounded": bool(flags & SNAPSHOT_FLAG_GROUNDED),
- "sitting": bool(flags & SNAPSHOT_FLAG_SITTING),
- "casting": bool(flags & SNAPSHOT_FLAG_CASTING),
- }
-
-
-static func _encode_movement_animation(
- peer_id: int,
- state: Dictionary,
-) -> Array:
- if not NetworkPlayerAnimationProtocol.validate_state(state):
- return []
- var action: Dictionary = state["action"]
- return [
- peer_id,
- str(state["locomotion_id"]),
- bool(state["grounded"]),
- str(action["id"]),
- int(action["sequence"]),
- float(action["elapsed"]),
- bool(action.get("paused", false)),
- ]
-
-
-static func _decode_movement_animation(value: Variant) -> Dictionary:
- if typeof(value) != TYPE_ARRAY:
- return {}
- var fields: Array = value
- if (
- fields.size() != MOVEMENT_ANIMATION_FIELD_COUNT
- or typeof(fields[0]) != TYPE_INT
- or typeof(fields[1]) not in [TYPE_STRING, TYPE_STRING_NAME]
- or typeof(fields[2]) != TYPE_BOOL
- or typeof(fields[3]) not in [TYPE_STRING, TYPE_STRING_NAME]
- or typeof(fields[4]) != TYPE_INT
- or typeof(fields[5]) not in [TYPE_FLOAT, TYPE_INT]
- or typeof(fields[6]) != TYPE_BOOL
- ):
- return {}
- var state: Dictionary = NetworkPlayerAnimationProtocol.make_state(
- StringName(str(fields[1])),
- bool(fields[2]),
- StringName(str(fields[3])),
- int(fields[4]),
- float(fields[5]),
- bool(fields[6]),
- )
- if (
- int(fields[0]) <= 0
- or not NetworkPlayerAnimationProtocol.validate_state(state)
- ):
- return {}
- return {"peer_id": int(fields[0]), "state": state}
-
-
-static func _encode_movement_animation_action(
- action: Dictionary,
- sitting: bool,
-) -> Array:
- if not NetworkPlayerAnimationProtocol.validate_action_state(action):
- return []
- return [
- str(action["id"]),
- int(action["sequence"]),
- float(action["elapsed"]),
- bool(action.get("paused", false)),
- sitting,
- ]
-
-
-static func _decode_movement_animation_action(value: Variant) -> Dictionary:
- if typeof(value) != TYPE_ARRAY:
- return {}
- var fields: Array = value
- if (
- fields.size() != MOVEMENT_ANIMATION_ACTION_FIELD_COUNT
- or typeof(fields[0]) not in [TYPE_STRING, TYPE_STRING_NAME]
- or typeof(fields[1]) != TYPE_INT
- or typeof(fields[2]) not in [TYPE_FLOAT, TYPE_INT]
- or typeof(fields[3]) != TYPE_BOOL
- or typeof(fields[4]) != TYPE_BOOL
- ):
- return {}
- var action: Dictionary = NetworkPlayerAnimationProtocol.make_action_state(
- StringName(str(fields[0])),
- int(fields[1]),
- float(fields[2]),
- bool(fields[3]),
- )
- if not NetworkPlayerAnimationProtocol.validate_action_state(action):
- return {}
- return {"action": action, "sitting": bool(fields[4])}
-
-
-@rpc("authority", "call_remote", "unreliable_ordered", 2)
-func receive_movement_snapshots(encoded_snapshots: Array) -> void:
- if state != State.JOINED_CLIENT:
- return
- var local_peer_id: int = multiplayer.get_unique_id()
- var estimated_transit_seconds: float = (
- _estimated_movement_transit_seconds()
- )
- for value: Variant in encoded_snapshots:
- var snapshot: Dictionary = _decode_movement_snapshot(value)
- if snapshot.is_empty():
- continue
- if typeof(snapshot.get("peer_id")) != TYPE_INT:
- continue
- var peer_id: int = snapshot["peer_id"]
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- if avatar == null:
- continue
- if peer_id == local_peer_id:
- if _reject_stale_space_transition_snapshot(snapshot):
- continue
- _discard_acknowledged_movement_inputs(
- int(snapshot.get("acknowledged_input", 0))
- )
- avatar.apply_local_prediction_correction(
- snapshot,
- _input_sequence,
- INPUT_INTERVAL,
- estimated_transit_seconds,
- _local_snapshot_delta_seconds(),
- )
- else:
- avatar.push_network_snapshot(
- snapshot,
- estimated_transit_seconds,
- )
-
-
-func _reject_stale_space_transition_snapshot(snapshot: Dictionary) -> bool:
- if not _local_space_transition_guard_active:
- return false
- var position_values: Array = snapshot.get("position", [])
- if position_values.size() != 3:
- return true
- var position := Vector3(
- float(position_values[0]),
- float(position_values[1]),
- float(position_values[2]),
- )
- var acknowledged_input: int = int(
- snapshot.get("acknowledged_input", 0)
- )
- if acknowledged_input < _local_space_transition_minimum_ack:
- return true
- if acknowledged_input > _local_space_transition_minimum_ack:
- _local_space_transition_guard_active = false
- return false
- if position.distance_to(_local_space_transition_guard_position) > (
- SPACE_TRANSITION_SNAPSHOT_GUARD_DISTANCE
- ):
- return true
- _local_space_transition_guard_active = false
- return false
-
-
-func _discard_acknowledged_movement_inputs(acknowledged_sequence: int) -> void:
- while (
- not _pending_movement_inputs.is_empty()
- and int(_pending_movement_inputs[0].get("sequence", 0))
- <= acknowledged_sequence
- ):
- _pending_movement_inputs.pop_front()
-
-
-func _local_snapshot_delta_seconds() -> float:
- var now_msec: int = Time.get_ticks_msec()
- if _last_local_snapshot_received_msec <= 0:
- _last_local_snapshot_received_msec = now_msec
- return 0.0
- var elapsed_seconds: float = clampf(
- float(now_msec - _last_local_snapshot_received_msec) / 1000.0,
- 0.0,
- Player.LOCAL_PREDICTION_MAX_AUDIT_DELTA_SECONDS,
- )
- _last_local_snapshot_received_msec = now_msec
- return elapsed_seconds
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- NetworkProtocol.MOVEMENT_ANIMATION_CHANNEL,
-)
-func receive_movement_animations(encoded_states: Array) -> void:
- if state != State.JOINED_CLIENT:
- return
- for value: Variant in encoded_states:
- var decoded: Dictionary = _decode_movement_animation(value)
- if decoded.is_empty():
- continue
- var avatar: Player = _spawn_service.get_avatar(
- int(decoded["peer_id"])
- )
- if avatar == null:
- _pending_animation_state_by_peer[int(decoded["peer_id"])] = (
- Dictionary(decoded["state"]).duplicate(true)
- )
- continue
- avatar.apply_network_animation_state(decoded["state"])
-
-
-func _estimated_movement_transit_seconds() -> float:
- if state != State.JOINED_CLIENT:
- return -1.0
- var round_trip_msec: int = get_peer_rtt_ms(get_local_peer_id())
- if round_trip_msec < 0:
- return -1.0
- return minf(
- float(round_trip_msec) / 2000.0,
- MAX_MOVEMENT_ONE_WAY_TRANSIT_SECONDS,
- )
-
-
-func publish_authoritative_teleport(peer_id: int) -> void:
- if not is_host():
- return
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- if avatar == null:
- return
- receive_authoritative_teleport.rpc(
- avatar.make_network_snapshot(peer_id)
- )
-
-
-func request_recovery_presentation(
- active: bool,
- attempt_id: String,
-) -> void:
- if (
- attempt_id.is_empty()
- or attempt_id.length() > 96
- or not is_gameplay_session_active()
- ):
- return
- if is_host():
- _set_authoritative_recovery_presentation(
- get_local_peer_id(), active, attempt_id
- )
- elif state == State.JOINED_CLIENT:
- submit_recovery_presentation.rpc_id(
- 1, _session_id, active, attempt_id
- )
-
-
-@rpc("any_peer", "call_remote", "reliable", 0)
-func submit_recovery_presentation(
- session_id: String,
- active: bool,
- attempt_id: String,
-) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if (
- not is_host()
- or not _registry.has_peer(sender_id)
- or session_id != _session_id
- or attempt_id.is_empty()
- or attempt_id.length() > 96
- ):
- return
- _set_authoritative_recovery_presentation(sender_id, active, attempt_id)
-
-
-func _set_authoritative_recovery_presentation(
- peer_id: int,
- active: bool,
- attempt_id: String,
-) -> void:
- var current_attempt: String = _recovery_attempts.get(peer_id, "")
- if active:
- if not current_attempt.is_empty() and current_attempt != attempt_id:
- return
- elif current_attempt != attempt_id:
- return
- var data := {
- "session_id": _session_id,
- "generation": _operation_generation,
- "peer_id": peer_id,
- "active": active,
- "attempt_id": attempt_id,
- }
- _apply_recovery_presentation(data)
- receive_recovery_presentation.rpc(data)
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_recovery_presentation(data: Dictionary) -> void:
- _apply_recovery_presentation(data)
-
-
-func _apply_recovery_presentation(data: Dictionary) -> void:
- if (
- typeof(data.get("session_id")) != TYPE_STRING
- or str(data["session_id"]) != _session_id
- or typeof(data.get("generation")) != TYPE_INT
- or int(data["generation"]) < 1
- or typeof(data.get("peer_id")) != TYPE_INT
- or typeof(data.get("active")) != TYPE_BOOL
- or typeof(data.get("attempt_id")) != TYPE_STRING
- or str(data["attempt_id"]).is_empty()
- or str(data["attempt_id"]).length() > 96
- ):
- return
- var peer_id: int = data["peer_id"]
- var attempt_id: String = data["attempt_id"]
- var active: bool = data["active"]
- var current_attempt: String = _recovery_attempts.get(peer_id, "")
- if active:
- if not current_attempt.is_empty() and current_attempt != attempt_id:
- return
- _recovery_attempts[peer_id] = attempt_id
- elif current_attempt != attempt_id:
- return
- else:
- _recovery_attempts.erase(peer_id)
- remote_recovery_presentation_changed.emit(peer_id, active, attempt_id)
-
-
-func request_safe_respawn(entry_position: Vector3) -> void:
- if not entry_position.is_finite():
- return
- if is_host():
- remote_recovery_requested.emit(1, entry_position)
- elif state == State.JOINED_CLIENT:
- submit_safe_respawn_request.rpc_id(
- 1,
- [entry_position.x, entry_position.y, entry_position.z]
- )
-
-
-@rpc("any_peer", "call_remote", "reliable", 0)
-func submit_safe_respawn_request(position_data: Array) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if (
- not is_host()
- or not _registry.has_peer(sender_id)
- or position_data.size() != 3
- ):
- return
- var entry_position := Vector3(
- float(position_data[0]),
- float(position_data[1]),
- float(position_data[2])
- )
- if not entry_position.is_finite():
- return
- remote_recovery_requested.emit(sender_id, entry_position)
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_authoritative_teleport(snapshot: Dictionary) -> void:
- if state != State.JOINED_CLIENT:
- return
- var peer_id: int = int(snapshot.get("peer_id", 0))
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- if avatar != null:
- avatar.apply_network_teleport(snapshot)
-
-
-func _expire_pending_authentication(now: float) -> void:
- for peer_id: int in _pending_authentication.keys():
- if now >= float(_pending_authentication[peer_id]):
- _reject_peer(
- peer_id,
- NetworkProtocol.RejectionCode.AUTHENTICATION_TIMEOUT
- )
-
-
-func _emit_peer_count() -> void:
- peer_count_changed.emit(_registry.size(), session_max_players)
-
-
-func _set_state(new_state: State, message: String) -> void:
- if not _is_transition_allowed(state, new_state):
- push_warning(
- "Rejected invalid network state transition %s -> %s."
- % [State.keys()[state], State.keys()[new_state]]
- )
- return
- state = new_state
- state_changed.emit(state)
- status_message_changed.emit(message)
-
-
-func _is_transition_allowed(from_state: State, to_state: State) -> bool:
- if from_state == to_state:
- return true
- var allowed: Dictionary[State, Array] = {
- State.INACTIVE: [
- State.STARTING_PRIVATE_HOST,
- State.CONNECTING,
- State.CONNECTION_FAILED,
- ],
- State.STARTING_PRIVATE_HOST: [
- State.PRIVATE_HOST,
- State.CONNECTION_FAILED,
- State.DISCONNECTING,
- ],
- State.PRIVATE_HOST: [
- State.OPEN_HOST,
- State.DISCONNECTING,
- ],
- State.OPEN_HOST: [
- State.PRIVATE_HOST,
- State.DISCONNECTING,
- ],
- State.CONNECTING: [
- State.AUTHENTICATING,
- State.CONNECTION_FAILED,
- State.DISCONNECTING,
- State.INACTIVE,
- ],
- State.AUTHENTICATING: [
- State.VERIFYING_SERVER_IDENTITY,
- State.JOINED_CLIENT,
- State.CONNECTION_FAILED,
- State.DISCONNECTING,
- State.INACTIVE,
- ],
- State.VERIFYING_SERVER_IDENTITY: [
- State.AUTHENTICATING,
- State.CONNECTION_FAILED,
- State.DISCONNECTING,
- State.INACTIVE,
- ],
- State.JOINED_CLIENT: [
- State.DISCONNECTING,
- State.SERVER_LOST,
- ],
- State.DISCONNECTING: [
- State.INACTIVE,
- State.CONNECTING,
- ],
- State.CONNECTION_FAILED: [
- State.INACTIVE,
- State.CONNECTING,
- State.DISCONNECTING,
- ],
- State.SERVER_LOST: [
- State.INACTIVE,
- State.CONNECTING,
- State.DISCONNECTING,
- ],
- }
- return to_state in allowed.get(from_state, [])
-
-
-func _fail(message: String) -> void:
- _set_state(State.CONNECTION_FAILED, message)
- connection_error.emit(message)
-
-
-func _teardown_peer() -> void:
- var local_avatar: Player
- if _spawn_service != null:
- local_avatar = _spawn_service.get_local_player()
- if local_avatar != null:
- local_avatar.reset_network_movement_state()
- for peer_id: int in _recovery_attempts.keys():
- remote_recovery_presentation_changed.emit(
- peer_id, false, _recovery_attempts[peer_id]
- )
- _recovery_attempts.clear()
- _pending_authentication.clear()
- _pending_identity_challenges.clear()
- _authenticated_identity_cache.clear()
- _client_identity_attempt.clear()
- _pending_server_proof.clear()
- _registry.clear()
- if _spawn_service != null:
- _spawn_service.clear_remote_players()
- if _transport != null:
- _transport.disconnect_transport()
- multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
- _connection_deadline = 0.0
- _input_sequence = 0
- _input_accumulator = 0.0
- _idle_input_accumulator = 0.0
- _last_input_state_hash = 0
- _pending_movement_inputs.clear()
- _snapshot_accumulator = 0.0
- _movement_snapshot_tick = 0
- _last_local_snapshot_received_msec = 0
- _local_space_transition_guard_active = false
- _local_space_transition_guard_position = Vector3.ZERO
- _local_space_transition_minimum_ack = 0
- _animation_refresh_accumulator = 0.0
- _last_animation_state_by_peer.clear()
- _pending_animation_state_by_peer.clear()
- _last_local_animation_action_signature.clear()
- _server_capabilities = PackedStringArray()
- _peer_space_ids.clear()
- _server_identity_fingerprint = ""
- _server_identity_public_key = ""
- _session_identity_keys.clear()
- _session_operator_fingerprints.clear()
- _operator_peer_ids.clear()
- _local_operator = false
- _host_port = 0
- _dedicated_host = false
diff --git a/network/network_session.gd.uid b/network/network_session.gd.uid
deleted file mode 100644
index 1a423e8..0000000
--- a/network/network_session.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://u62a32ktf0y5
diff --git a/network/network_shop_protocol.gd b/network/network_shop_protocol.gd
deleted file mode 100644
index da9419a..0000000
--- a/network/network_shop_protocol.gd
+++ /dev/null
@@ -1,127 +0,0 @@
-class_name NetworkShopProtocol
-extends RefCounted
-
-const CAPABILITY: StringName = &"shop_v1"
-const ART_CAPABILITY: StringName = &"art_shop_v1"
-const BACKPACK_CAPABILITY: StringName = &"backpack_shop_v1"
-const DECOR_CAPABILITY: StringName = &"portable_home_v1"
-const RELIABLE_CHANNEL: int = NetworkProtocol.SHOP_RELIABLE_CHANNEL
-const MAX_ID_LENGTH: int = 96
-const MAX_MESSAGE_LENGTH: int = 160
-const MAX_QUANTITY: int = 99
-
-enum ProductCategory {
- SUPPLY,
- ROD,
- REEL_SPEED_UPGRADE,
- BARRIER_POWER_UPGRADE,
- COOLER_CAPACITY_UPGRADE,
- ART_KIT,
- ART_UPGRADE,
- BACKPACK_CAPACITY_UPGRADE,
- DECOR,
-}
-
-
-static func validate_request(data: Variant) -> String:
- if typeof(data) != TYPE_DICTIONARY:
- return "Purchase could not be completed."
- var payload: Dictionary = data
- for key: String in [
- "request_id",
- "session_id",
- "shop_id",
- "product_id",
- "category",
- "quantity",
- "wallet_balance",
- "current_state",
- ]:
- if not payload.has(key):
- return "Purchase could not be completed."
- if (
- typeof(payload["request_id"]) != TYPE_STRING
- or typeof(payload["session_id"]) != TYPE_STRING
- or typeof(payload["shop_id"]) not in [TYPE_STRING, TYPE_STRING_NAME]
- or typeof(payload["product_id"]) not in [
- TYPE_STRING, TYPE_STRING_NAME
- ]
- or typeof(payload["category"]) != TYPE_INT
- or typeof(payload["quantity"]) != TYPE_INT
- or typeof(payload["wallet_balance"]) != TYPE_INT
- or typeof(payload["current_state"]) != TYPE_INT
- ):
- return "Purchase could not be completed."
- var request_id: String = payload["request_id"]
- var session_id: String = payload["session_id"]
- var shop_id: String = str(payload["shop_id"])
- var product_id: String = str(payload["product_id"])
- var category: int = payload["category"]
- var quantity: int = payload["quantity"]
- var wallet_balance: int = payload["wallet_balance"]
- var current_state: int = payload["current_state"]
- if (
- payload.has("bait_unlocked")
- and typeof(payload["bait_unlocked"]) != TYPE_BOOL
- ):
- return "Purchase could not be completed."
- if (
- request_id.is_empty()
- or request_id.length() > MAX_ID_LENGTH
- or session_id.is_empty()
- or session_id.length() > MAX_ID_LENGTH
- or shop_id.is_empty()
- or shop_id.length() > MAX_ID_LENGTH
- or product_id.is_empty()
- or product_id.length() > MAX_ID_LENGTH
- or category < 0
- or category >= ProductCategory.size()
- or quantity < 1
- or quantity > MAX_QUANTITY
- or wallet_balance < 0
- or current_state < 0
- ):
- return "Purchase could not be completed."
- return ""
-
-
-static func validate_result(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var payload: Dictionary = data
- return (
- _valid_id(payload.get("result_id"))
- and _valid_id(payload.get("request_id"))
- and typeof(payload.get("session_id")) == TYPE_STRING
- and typeof(payload.get("target_peer_id")) == TYPE_INT
- and typeof(payload.get("accepted")) == TYPE_BOOL
- and typeof(payload.get("product_id")) in [
- TYPE_STRING, TYPE_STRING_NAME
- ]
- and not str(payload["product_id"]).is_empty()
- and str(payload["product_id"]).length() <= MAX_ID_LENGTH
- and typeof(payload.get("category")) == TYPE_INT
- and int(payload["category"]) >= 0
- and int(payload["category"]) < ProductCategory.size()
- and typeof(payload.get("quantity")) == TYPE_INT
- and int(payload["quantity"]) >= 0
- and int(payload["quantity"]) <= MAX_QUANTITY
- and typeof(payload.get("total_cost")) == TYPE_INT
- and int(payload["total_cost"]) >= 0
- and typeof(payload.get("expected_wallet")) == TYPE_INT
- and int(payload["expected_wallet"]) >= 0
- and typeof(payload.get("expected_state")) == TYPE_INT
- and int(payload["expected_state"]) >= 0
- and typeof(payload.get("resulting_state")) == TYPE_INT
- and int(payload["resulting_state"]) >= 0
- and typeof(payload.get("message")) == TYPE_STRING
- and str(payload["message"]).length() <= MAX_MESSAGE_LENGTH
- )
-
-
-static func _valid_id(value: Variant) -> bool:
- return (
- typeof(value) == TYPE_STRING
- and not str(value).is_empty()
- and str(value).length() <= MAX_ID_LENGTH
- )
diff --git a/network/network_shop_protocol.gd.uid b/network/network_shop_protocol.gd.uid
deleted file mode 100644
index ce71a8c..0000000
--- a/network/network_shop_protocol.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cmb5ud0b1rrmg
diff --git a/network/network_shop_service.gd b/network/network_shop_service.gd
deleted file mode 100644
index 8294b53..0000000
--- a/network/network_shop_service.gd
+++ /dev/null
@@ -1,1149 +0,0 @@
-class_name NetworkShopService
-extends Node
-
-const FishingShopStockType = preload(
- "res://economy/fishing_shop_stock.gd"
-)
-const ItemDataType = preload("res://items/item_data.gd")
-const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
-const OwnedItemType = preload("res://items/owned_item.gd")
-const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
-const DecorCatalogType = preload("res://homes/decor_catalog.gd")
-
-const FISHING_SHOP_ID: StringName = &"main_fishing_shop"
-const DECOR_SHOP_ID: StringName = &"rv_decor_shop"
-const REEL_PRODUCT_ID: StringName = &"reel_speed_upgrade"
-const BARRIER_PRODUCT_ID: StringName = &"barrier_power_upgrade"
-const COOLER_PRODUCT_ID: StringName = &"cooler_capacity_upgrade"
-const BACKPACK_PRODUCT_ID: StringName = &"backpack_capacity_upgrade"
-const MAX_LEDGER_ENTRIES_PER_PEER: int = 64
-
-signal local_purchase_pending(request_id: String)
-signal local_purchase_finished(
- request_id: String,
- accepted: bool,
- message: String,
- product_id: StringName,
- category: int,
- quantity: int,
- total_cost: int,
-)
-
-var _session: NetworkSession
-var _spawn_service: PlayerSpawnService
-var _network_fishing: NetworkFishingService
-var _interaction: FishingShopInteraction
-var _decor_interaction: DecorShopInteraction
-var _wallet: PlayerWallet
-var _bag: PlayerBag
-var _item_catalog: ItemCatalog
-var _upgrades: PlayerFishingUpgrades
-var _cooler_capacity: PlayerCoolerCapacity
-var _inventory_layout: PlayerInventoryLayout
-var _art_unlocks: PlayerArtUnlocks
-var _save_manager: PlayerSaveManager
-var _request_ledgers: Dictionary[int, Dictionary] = {}
-var _pending_by_peer: Dictionary[int, String] = {}
-var _result_owners: Dictionary[String, int] = {}
-var _acknowledged_results: Dictionary[String, bool] = {}
-var _received_results: Dictionary[String, bool] = {}
-var _applied_results: Dictionary[String, bool] = {}
-var _pending_local_request: Dictionary = {}
-var _reservations: PlayerAssetReservationService
-var _home_state: PlayerHomeState
-var _network_home: NetworkHomeService
-
-
-func setup(
- session: NetworkSession,
- spawn_service: PlayerSpawnService,
- network_fishing: NetworkFishingService,
- interaction: FishingShopInteraction,
- wallet: PlayerWallet,
- bag: PlayerBag,
- item_catalog: ItemCatalog,
- upgrades: PlayerFishingUpgrades,
- cooler_capacity: PlayerCoolerCapacity,
- art_unlocks: PlayerArtUnlocks,
- save_manager: PlayerSaveManager,
- reservations: PlayerAssetReservationService,
- inventory_layout: PlayerInventoryLayout = null,
- home_state: PlayerHomeState = null,
- network_home: NetworkHomeService = null,
- decor_interaction: DecorShopInteraction = null,
-) -> void:
- _session = session
- _spawn_service = spawn_service
- _network_fishing = network_fishing
- _interaction = interaction
- _wallet = wallet
- _bag = bag
- _item_catalog = item_catalog
- _upgrades = upgrades
- _cooler_capacity = cooler_capacity
- _art_unlocks = art_unlocks
- _save_manager = save_manager
- _reservations = reservations
- _inventory_layout = inventory_layout
- _home_state = home_state
- _network_home = network_home
- _decor_interaction = decor_interaction
- if not _session.peer_removed.is_connected(_on_peer_removed):
- _session.peer_removed.connect(_on_peer_removed)
- if not _session.state_changed.is_connected(_on_session_state_changed):
- _session.state_changed.connect(_on_session_state_changed)
-
-
-func set_shop_interaction(interaction: FishingShopInteraction) -> void:
- _interaction = interaction
-
-
-func set_decor_shop_interaction(
- interaction: DecorShopInteraction,
-) -> void:
- _decor_interaction = interaction
-
-
-func can_request_purchase() -> bool:
- return (
- _session != null
- and _session.is_gameplay_session_active()
- and (
- _session.is_host()
- or _session.supports_server_capability(
- NetworkShopProtocol.CAPABILITY
- )
- )
- )
-
-
-func can_request_art_purchase() -> bool:
- return (
- can_request_purchase()
- and (
- _session.is_host()
- or _session.supports_server_capability(
- NetworkShopProtocol.ART_CAPABILITY
- )
- )
- )
-
-
-func can_request_backpack_purchase() -> bool:
- return (
- can_request_purchase()
- and (
- _session.is_host()
- or _session.supports_server_capability(
- NetworkShopProtocol.BACKPACK_CAPABILITY
- )
- )
- )
-
-
-func can_request_decor_purchase() -> bool:
- return (
- can_request_purchase()
- and _home_state != null
- and (
- _session.is_host()
- or _session.supports_server_capability(
- NetworkShopProtocol.DECOR_CAPABILITY
- )
- )
- )
-
-
-func is_local_purchase_pending() -> bool:
- return not _pending_local_request.is_empty()
-
-
-func request_supply(item_id: StringName) -> String:
- var owned: int = _bag.get_quantity(item_id) if _bag != null else 0
- var item: ItemDataType = (
- _item_catalog.get_item_by_id(item_id)
- if _item_catalog != null else null
- )
- var quantity: int = FishingShopStockType.get_purchase_quantity(
- item_id,
- owned,
- item.max_stack if item != null else FishingShopStockType.WORM_MAX_STACK,
- )
- return _request_purchase(
- item_id,
- NetworkShopProtocol.ProductCategory.SUPPLY,
- quantity,
- owned
- )
-
-
-func request_rod(item_id: StringName) -> String:
- return _request_purchase(
- item_id,
- NetworkShopProtocol.ProductCategory.ROD,
- 1,
- _bag.get_quantity(item_id) if _bag != null else 0,
- )
-
-
-func request_reel_speed_upgrade() -> String:
- return _request_purchase(
- REEL_PRODUCT_ID,
- NetworkShopProtocol.ProductCategory.REEL_SPEED_UPGRADE,
- 1,
- _upgrades.get_reel_speed_level() if _upgrades != null else 0
- )
-
-
-func request_barrier_power_upgrade() -> String:
- return _request_purchase(
- BARRIER_PRODUCT_ID,
- NetworkShopProtocol.ProductCategory.BARRIER_POWER_UPGRADE,
- 1,
- _upgrades.get_barrier_power_level() if _upgrades != null else 0
- )
-
-
-func request_cooler_capacity_upgrade() -> String:
- return _request_purchase(
- COOLER_PRODUCT_ID,
- NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE,
- 1,
- _cooler_capacity.get_level() if _cooler_capacity != null else 0
- )
-
-
-func request_backpack_capacity_upgrade() -> String:
- return _request_purchase(
- BACKPACK_PRODUCT_ID,
- NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE,
- 1,
- _inventory_layout.get_backpack_level()
- if _inventory_layout != null else 0,
- )
-
-
-func request_decor(product_id: StringName) -> String:
- return _request_purchase(
- product_id,
- NetworkShopProtocol.ProductCategory.DECOR,
- 1,
- _home_state.get_owned_count(product_id) if _home_state != null else 0,
- )
-
-
-func request_art_kit() -> String:
- return _request_purchase(
- ArtShopStockType.ART_KIT_ITEM_ID,
- NetworkShopProtocol.ProductCategory.ART_KIT,
- 1,
- _bag.get_quantity(ArtShopStockType.ART_KIT_ITEM_ID)
- if _bag != null else 0,
- )
-
-
-func request_art_upgrade(product_id: StringName) -> String:
- if (
- _bag == null
- or not _bag.owns_item(ArtShopStockType.ART_KIT_ITEM_ID)
- ):
- local_purchase_finished.emit(
- "",
- false,
- "Own an Art Kit before buying upgrades.",
- product_id,
- NetworkShopProtocol.ProductCategory.ART_UPGRADE,
- 0,
- 0,
- )
- return ""
- return _request_purchase(
- product_id,
- NetworkShopProtocol.ProductCategory.ART_UPGRADE,
- 1,
- _art_unlocks.get_unlock_mask() if _art_unlocks != null else 0,
- )
-
-
-func _request_purchase(
- product_id: StringName,
- category: int,
- quantity: int,
- current_state: int,
-) -> String:
- if is_local_purchase_pending():
- local_purchase_finished.emit(
- "", false, "Purchasing…", product_id, category, 0, 0
- )
- return ""
- if not can_request_purchase():
- local_purchase_finished.emit(
- "",
- false,
- "Purchases are not supported by this server.",
- product_id,
- category,
- 0,
- 0
- )
- return ""
- if (
- category in [
- NetworkShopProtocol.ProductCategory.ART_KIT,
- NetworkShopProtocol.ProductCategory.ART_UPGRADE,
- ]
- and not can_request_art_purchase()
- ):
- local_purchase_finished.emit(
- "", false, "Art supplies require a newer server.",
- product_id, category, 0, 0,
- )
- return ""
- if (
- category == NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE
- and not can_request_backpack_purchase()
- ):
- local_purchase_finished.emit(
- "", false, "Backpack upgrades require a newer server.",
- product_id, category, 0, 0,
- )
- return ""
- if (
- category == NetworkShopProtocol.ProductCategory.DECOR
- and not can_request_decor_purchase()
- ):
- local_purchase_finished.emit(
- "", false, "Decor require a newer server.",
- product_id, category, 0, 0,
- )
- return ""
- var request_id: String = _new_id("shop")
- var shop_id: StringName = (
- DECOR_SHOP_ID
- if category == NetworkShopProtocol.ProductCategory.DECOR
- else FISHING_SHOP_ID
- )
- var request: Dictionary = {
- "request_id": request_id,
- "session_id": _session.get_session_id(),
- "shop_id": str(shop_id),
- "product_id": str(product_id),
- "category": category,
- "quantity": quantity,
- "wallet_balance": _wallet.get_balance() if _wallet != null else 0,
- "current_state": current_state,
- }
- if (
- category == NetworkShopProtocol.ProductCategory.SUPPLY
- and FishingShopStockType.is_bait_topoff(product_id)
- ):
- request["bait_unlocked"] = (
- _bag != null and _bag.is_bait_unlocked(product_id)
- )
- _pending_local_request = request.duplicate(true)
- local_purchase_pending.emit(request_id)
- if _session.is_host():
- _handle_purchase_request(_session.get_local_peer_id(), request)
- else:
- submit_purchase_request.rpc_id(1, request)
- return request_id
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- NetworkShopProtocol.RELIABLE_CHANNEL
-)
-func submit_purchase_request(data: Dictionary) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if (
- _session == null
- or not _session.is_host()
- or not _session.is_authenticated_peer(sender_id)
- ):
- return
- _handle_purchase_request(sender_id, data)
-
-
-func _handle_purchase_request(peer_id: int, data: Dictionary) -> void:
- var validation_error: String = NetworkShopProtocol.validate_request(data)
- var request_id: String = str(data.get("request_id", ""))
- var ledger: Dictionary = _request_ledgers.get(peer_id, {})
- if (
- not request_id.is_empty()
- and request_id.length() <= NetworkShopProtocol.MAX_ID_LENGTH
- and ledger.has(request_id)
- ):
- _send_result(peer_id, ledger[request_id])
- return
- if not validation_error.is_empty():
- _record_and_send(
- peer_id,
- _rejected_result(data, validation_error)
- )
- return
- if str(data["session_id"]) != _session.get_session_id():
- _record_and_send(
- peer_id,
- _rejected_result(data, "Purchase could not be completed.")
- )
- return
- if (
- _pending_by_peer.has(peer_id)
- and _pending_by_peer[peer_id] != request_id
- ):
- _record_and_send(
- peer_id,
- _rejected_result(data, "A purchase is already pending.")
- )
- return
- var requested_shop_id := StringName(str(data["shop_id"]))
- if not _is_shop_available_for_peer(peer_id, requested_shop_id):
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- var requested_interaction: FishingShopInteraction = (
- _decor_interaction
- if requested_shop_id == DECOR_SHOP_ID
- else _interaction
- )
- var message: String = (
- "The shop is unavailable."
- if avatar == null or requested_interaction == null
- else "Move closer to the shop."
- )
- _record_and_send(peer_id, _rejected_result(data, message))
- return
- var result: Dictionary = _build_authoritative_result(peer_id, data)
- if (
- bool(result.get("accepted", false))
- and int(result.get("category", -1))
- == NetworkShopProtocol.ProductCategory.DECOR
- and _network_home != null
- ):
- _network_home.authorize_decor_purchase(
- peer_id,
- StringName(str(result["product_id"])),
- int(result["resulting_state"]),
- str(result["result_id"]),
- )
- _pending_by_peer[peer_id] = request_id
- _record_and_send(peer_id, result)
-
-
-func _build_authoritative_result(
- peer_id: int,
- request: Dictionary,
-) -> Dictionary:
- var product_id := StringName(str(request["product_id"]))
- var category: int = request["category"]
- var quantity: int = request["quantity"]
- var wallet_balance: int = request["wallet_balance"]
- var current_state: int = request["current_state"]
- if (
- category == NetworkShopProtocol.ProductCategory.DECOR
- and _network_home != null
- ):
- var authoritative_count: int = (
- _network_home.get_authoritative_owned_count(peer_id, product_id)
- )
- if authoritative_count >= 0:
- current_state = authoritative_count
- var cost: int = -1
- var resulting_state: int = current_state
- var rejection: String = ""
- var expected_shop_id: StringName = (
- DECOR_SHOP_ID
- if category == NetworkShopProtocol.ProductCategory.DECOR
- else FISHING_SHOP_ID
- )
- if StringName(str(request["shop_id"])) != expected_shop_id:
- rejection = "The shop is unavailable."
- elif quantity < 1:
- rejection = "Purchase could not be completed."
- else:
- match category:
- NetworkShopProtocol.ProductCategory.SUPPLY:
- var item: ItemDataType = _item_catalog.get_item_by_id(
- product_id
- ) if _item_catalog != null else null
- var bait_topoff: bool = (
- FishingShopStockType.is_bait_topoff(product_id)
- )
- var permanent_unlock: bool = (
- FishingShopStockType.is_permanent_unlock(product_id, item)
- )
- if (
- item == null
- or not item.is_available()
- or product_id not in (
- FishingShopStockType.get_stock_item_ids()
- )
- or (
- item.category != ItemDataType.Category.CONSUMABLE
- and not bait_topoff
- and not permanent_unlock
- )
- or (not item.stackable and not permanent_unlock)
- or (
- not item.usable
- and not bait_topoff
- and not permanent_unlock
- and not FishingShopStockType.is_passive_supply(
- product_id
- )
- )
- or current_state >= item.max_stack
- or current_state + quantity > item.max_stack
- ):
- rejection = (
- "Your inventory is full."
- if item != null and current_state >= item.max_stack
- else "Purchase could not be completed."
- )
- else:
- var expected_quantity: int = (
- FishingShopStockType.get_purchase_quantity(
- product_id, current_state, item.max_stack
- )
- )
- if quantity != expected_quantity:
- rejection = "Purchase could not be completed."
- else:
- cost = FishingShopStockType.get_purchase_cost(
- product_id,
- quantity,
- bool(request.get("bait_unlocked", true)),
- )
- if cost < 0:
- rejection = "Purchase could not be completed."
- resulting_state = current_state + quantity
- NetworkShopProtocol.ProductCategory.ROD:
- var rod := (
- _item_catalog.get_item_by_id(product_id)
- as FishingRodDataType
- if _item_catalog != null else null
- )
- var available_rods: Array[FishingRodDataType] = (
- FishingShopStockType.get_rod_stock(_item_catalog)
- )
- if (
- rod == null
- or not rod.is_shop_available()
- or rod not in available_rods
- or quantity != 1
- or current_state != 0
- ):
- rejection = (
- "Rod already owned."
- if current_state > 0
- else "Purchase could not be completed."
- )
- else:
- cost = rod.shop_price
- resulting_state = 1
- NetworkShopProtocol.ProductCategory.REEL_SPEED_UPGRADE:
- if (
- product_id != REEL_PRODUCT_ID
- or current_state
- >= PlayerFishingUpgrades.MAX_REEL_SPEED_LEVEL
- ):
- rejection = "Upgrade is already at maximum."
- else:
- cost = (
- PlayerFishingUpgrades.REEL_SPEED_COSTS[
- current_state
- ]
- )
- resulting_state = current_state + 1
- NetworkShopProtocol.ProductCategory.BARRIER_POWER_UPGRADE:
- if (
- product_id != BARRIER_PRODUCT_ID
- or current_state
- >= PlayerFishingUpgrades.MAX_BARRIER_POWER_LEVEL
- ):
- rejection = "Upgrade is already at maximum."
- else:
- cost = (
- PlayerFishingUpgrades.BARRIER_POWER_COSTS[
- current_state
- ]
- )
- resulting_state = current_state + 1
- NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE:
- if (
- product_id != COOLER_PRODUCT_ID
- or current_state >= PlayerCoolerCapacity.MAX_LEVEL
- ):
- rejection = "Upgrade is already at maximum."
- else:
- cost = (
- PlayerCoolerCapacity.EXPANSION_COSTS[
- current_state
- ]
- )
- resulting_state = current_state + 1
- NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE:
- if (
- product_id != BACKPACK_PRODUCT_ID
- or current_state >= PlayerInventoryLayout.MAX_BACKPACK_LEVEL
- ):
- rejection = "Upgrade is already at maximum."
- else:
- cost = PlayerInventoryLayout.BACKPACK_EXPANSION_COSTS[
- current_state
- ]
- resulting_state = current_state + 1
- NetworkShopProtocol.ProductCategory.ART_KIT:
- var art_item: ItemDataType = _item_catalog.get_item_by_id(
- product_id
- ) if _item_catalog != null else null
- cost = ArtShopStockType.get_price(product_id)
- if (
- product_id != ArtShopStockType.ART_KIT_ITEM_ID
- or art_item == null
- or not art_item.is_available()
- or art_item.category != ItemDataType.Category.TOOL
- or art_item.stackable
- or not art_item.equippable
- or not art_item.hotbar_allowed
- or current_state != 0
- ):
- rejection = (
- "Art Kit already owned."
- if current_state > 0
- else "Purchase could not be completed."
- )
- else:
- resulting_state = 1
- NetworkShopProtocol.ProductCategory.ART_UPGRADE:
- cost = ArtShopStockType.get_price(product_id)
- if (
- not PlayerArtUnlocks.is_product_id(product_id)
- or current_state < 0
- or (current_state & ~PlayerArtUnlocks.ALL_UNLOCK_MASK) != 0
- or PlayerArtUnlocks.resulting_mask(
- current_state, product_id
- ) == current_state
- ):
- rejection = "Upgrade is already unlocked."
- else:
- resulting_state = PlayerArtUnlocks.resulting_mask(
- current_state, product_id
- )
- NetworkShopProtocol.ProductCategory.DECOR:
- cost = DecorCatalogType.get_price(product_id)
- if (
- not DecorCatalogType.has_product(product_id)
- or quantity != 1
- or current_state < 0
- or current_state
- >= DecorCatalogType.MAX_OWNED_PER_PRODUCT
- or cost < 0
- ):
- rejection = "Decor is unavailable."
- else:
- resulting_state = current_state + 1
- if rejection.is_empty() and (cost < 0 or wallet_balance < cost):
- rejection = "Insufficient funds."
- if not rejection.is_empty():
- return _rejected_result(request, rejection)
- return {
- "result_id": _new_id("shop_result"),
- "request_id": str(request["request_id"]),
- "session_id": _session.get_session_id(),
- "target_peer_id": peer_id,
- "accepted": true,
- "product_id": str(product_id),
- "category": category,
- "quantity": quantity,
- "total_cost": cost,
- "expected_wallet": wallet_balance,
- "expected_state": current_state,
- "resulting_state": resulting_state,
- "message": "Purchase complete.",
- }
-
-
-func _rejected_result(request: Dictionary, message: String) -> Dictionary:
- var request_id: String = str(request.get("request_id", ""))
- if (
- request_id.is_empty()
- or request_id.length() > NetworkShopProtocol.MAX_ID_LENGTH
- ):
- request_id = "invalid"
- var category: int = clampi(
- int(request.get("category", 0)),
- 0,
- NetworkShopProtocol.ProductCategory.size() - 1
- )
- return {
- "result_id": _new_id("shop_result"),
- "request_id": request_id,
- "session_id": _session.get_session_id() if _session != null else "",
- "target_peer_id": 0,
- "accepted": false,
- "product_id": str(request.get("product_id", "invalid")).left(
- NetworkShopProtocol.MAX_ID_LENGTH
- ),
- "category": category,
- "quantity": 0,
- "total_cost": 0,
- "expected_wallet": maxi(int(request.get("wallet_balance", 0)), 0),
- "expected_state": maxi(int(request.get("current_state", 0)), 0),
- "resulting_state": maxi(int(request.get("current_state", 0)), 0),
- "message": message.left(NetworkShopProtocol.MAX_MESSAGE_LENGTH),
- }
-
-
-func _record_and_send(peer_id: int, result: Dictionary) -> void:
- result["target_peer_id"] = peer_id
- var request_id: String = result["request_id"]
- var ledger: Dictionary = _request_ledgers.get(peer_id, {})
- ledger[request_id] = result.duplicate(true)
- _bound_dictionary(ledger)
- _request_ledgers[peer_id] = ledger
- _result_owners[str(result["result_id"])] = peer_id
- _bound_dictionary(_result_owners)
- _send_result(peer_id, result)
-
-
-func _send_result(peer_id: int, result: Dictionary) -> void:
- if peer_id == _session.get_local_peer_id():
- _apply_purchase_result(result)
- else:
- receive_purchase_result.rpc_id(peer_id, result)
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- NetworkShopProtocol.RELIABLE_CHANNEL
-)
-func receive_purchase_result(data: Dictionary) -> void:
- _apply_purchase_result(data)
-
-
-func _apply_purchase_result(data: Dictionary) -> void:
- if (
- not NetworkShopProtocol.validate_result(data)
- or _session == null
- or str(data["session_id"]) != _session.get_session_id()
- or int(data["target_peer_id"]) != _session.get_local_peer_id()
- ):
- return
- var result_id: String = data["result_id"]
- if _received_results.has(result_id):
- _acknowledge_result(data, _applied_results.has(result_id), "")
- return
- if (
- _pending_local_request.is_empty()
- or str(data["request_id"])
- != str(_pending_local_request.get("request_id", ""))
- ):
- return
- if not bool(data["accepted"]):
- _received_results[result_id] = true
- _bound_dictionary(_received_results)
- _finish_local_purchase(data, false, str(data["message"]))
- _acknowledge_result(data, false, str(data["message"]))
- return
- var validation_message: String = _validate_local_result(data)
- if not validation_message.is_empty():
- _fail_local_apply(data, validation_message)
- return
- var wallet_snapshot: int = _wallet.get_balance()
- var bag_snapshot: Array[OwnedItemType] = _bag.get_all_items()
- var bait_unlock_snapshot: Array[StringName] = (
- _bag.get_unlocked_bait_ids()
- )
- var reel_snapshot: int = _upgrades.get_reel_speed_level()
- var barrier_snapshot: int = _upgrades.get_barrier_power_level()
- var cooler_snapshot: int = _cooler_capacity.get_level()
- var backpack_snapshot: int = (
- _inventory_layout.get_backpack_level()
- if _inventory_layout != null else 0
- )
- var art_snapshot: int = _art_unlocks.get_unlock_mask()
- var home_snapshot: Dictionary = (
- _home_state.to_save_data() if _home_state != null else {}
- )
- var applied: bool = _apply_local_product(data)
- if not applied or not _save_manager.save_if_dirty():
- _bag.replace_all_items(bag_snapshot)
- _bag.replace_unlocked_bait_ids(bait_unlock_snapshot)
- _upgrades.restore_levels(reel_snapshot, barrier_snapshot)
- _cooler_capacity.restore_level(cooler_snapshot)
- if _inventory_layout != null:
- _inventory_layout.restore_backpack_level(backpack_snapshot)
- _art_unlocks.restore_mask(art_snapshot)
- if _home_state != null and not home_snapshot.is_empty():
- _home_state.restore_from_save_data(home_snapshot)
- _wallet.restore_balance(wallet_snapshot)
- _save_manager.save_if_dirty()
- _fail_local_apply(data, "Purchase could not be completed.")
- return
- _applied_results[result_id] = true
- _bound_dictionary(_applied_results)
- _received_results[result_id] = true
- _bound_dictionary(_received_results)
- _finish_local_purchase(data, true, "Purchase complete.")
- _acknowledge_result(data, true, "")
-
-
-func _validate_local_result(data: Dictionary) -> String:
- if (
- _wallet == null
- or _bag == null
- or _upgrades == null
- or _cooler_capacity == null
- or _art_unlocks == null
- or _save_manager == null
- or _home_state == null
- or _wallet.get_balance() != int(data["expected_wallet"])
- or str(data["product_id"])
- != str(_pending_local_request.get("product_id", ""))
- or int(data["category"])
- != int(_pending_local_request.get("category", -1))
- or int(data["quantity"])
- != int(_pending_local_request.get("quantity", -1))
- ):
- return "Purchase could not be completed."
- var category: int = data["category"]
- var product_id := StringName(str(data["product_id"]))
- var expected_state: int = data["expected_state"]
- var cost: int = data["total_cost"]
- if (
- _reservations != null
- and _reservations.get_available_fish_coin() < cost
- ):
- return "Reserved in a letter."
- var resulting_state: int = int(data["resulting_state"])
- if (
- category != NetworkShopProtocol.ProductCategory.ART_UPGRADE
- and resulting_state != expected_state + int(data["quantity"])
- ):
- return "Purchase could not be completed."
- if not _wallet.can_afford(cost):
- return "Insufficient funds."
- match category:
- NetworkShopProtocol.ProductCategory.SUPPLY:
- var item: ItemDataType = (
- _item_catalog.get_item_by_id(product_id)
- if _item_catalog != null else null
- )
- var bait_unlocked: bool = _bag.is_bait_unlocked(product_id)
- if (
- item == null
- or not item.is_available()
- or _bag.get_quantity(product_id) != expected_state
- or int(data["quantity"]) != FishingShopStockType.get_purchase_quantity(
- product_id, expected_state, item.max_stack
- )
- ):
- return "Purchase could not be completed."
- if cost != FishingShopStockType.get_purchase_cost(
- product_id, int(data["quantity"]), bait_unlocked
- ):
- return "Purchase could not be completed."
- if not _bag.can_add_item(product_id, data["quantity"]):
- return "Your inventory is full."
- NetworkShopProtocol.ProductCategory.ROD:
- var rod := (
- _item_catalog.get_item_by_id(product_id)
- as FishingRodDataType
- if _item_catalog != null else null
- )
- if (
- rod == null
- or not rod.is_shop_available()
- or _bag.get_quantity(product_id) != expected_state
- or expected_state != 0
- or int(data["quantity"]) != 1
- or cost != rod.shop_price
- or not _bag.can_add_item(product_id, 1)
- ):
- return "Purchase could not be completed."
- NetworkShopProtocol.ProductCategory.REEL_SPEED_UPGRADE:
- if _upgrades.get_reel_speed_level() != expected_state:
- return "Purchase could not be completed."
- if _upgrades.get_next_reel_speed_cost() != cost:
- return "Purchase could not be completed."
- NetworkShopProtocol.ProductCategory.BARRIER_POWER_UPGRADE:
- if _upgrades.get_barrier_power_level() != expected_state:
- return "Purchase could not be completed."
- if _upgrades.get_next_barrier_power_cost() != cost:
- return "Purchase could not be completed."
- NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE:
- if _cooler_capacity.get_level() != expected_state:
- return "Purchase could not be completed."
- if _cooler_capacity.get_next_cost() != cost:
- return "Purchase could not be completed."
- NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE:
- if (
- _inventory_layout == null
- or _inventory_layout.get_backpack_level() != expected_state
- or _inventory_layout.get_next_backpack_cost() != cost
- ):
- return "Purchase could not be completed."
- NetworkShopProtocol.ProductCategory.ART_KIT:
- var art_item: ItemDataType = (
- _item_catalog.get_item_by_id(product_id)
- if _item_catalog != null else null
- )
- if (
- product_id != ArtShopStockType.ART_KIT_ITEM_ID
- or art_item == null
- or not art_item.is_available()
- or _bag.get_quantity(product_id) != expected_state
- or not _bag.can_add_item(product_id, 1)
- or cost != ArtShopStockType.ART_KIT_PRICE
- ):
- return "Purchase could not be completed."
- NetworkShopProtocol.ProductCategory.ART_UPGRADE:
- if (
- _art_unlocks.get_unlock_mask() != expected_state
- or not PlayerArtUnlocks.is_product_id(product_id)
- or resulting_state != PlayerArtUnlocks.resulting_mask(
- expected_state, product_id
- )
- or cost != ArtShopStockType.UPGRADE_PRICE
- ):
- return "Purchase could not be completed."
- NetworkShopProtocol.ProductCategory.DECOR:
- if (
- not DecorCatalogType.has_product(product_id)
- or _home_state.get_owned_count(product_id) != expected_state
- or resulting_state != expected_state + 1
- or cost != DecorCatalogType.get_price(product_id)
- or expected_state >= DecorCatalogType.MAX_OWNED_PER_PRODUCT
- or not _bag.can_add_item(product_id, 1)
- ):
- return (
- "Your inventory is full."
- if not _bag.can_add_item(product_id, 1)
- else "Purchase could not be completed."
- )
- _:
- return "Purchase could not be completed."
- return ""
-
-
-func _apply_local_product(data: Dictionary) -> bool:
- var category: int = data["category"]
- var product_id := StringName(str(data["product_id"]))
- match category:
- NetworkShopProtocol.ProductCategory.SUPPLY:
- return (
- _wallet.debit(data["total_cost"])
- and _bag.add_item(product_id, data["quantity"])
- )
- NetworkShopProtocol.ProductCategory.ROD:
- return (
- _wallet.debit(int(data["total_cost"]))
- and _bag.add_item(product_id, 1)
- )
- NetworkShopProtocol.ProductCategory.REEL_SPEED_UPGRADE:
- return _upgrades.purchase_reel_speed(_wallet)
- NetworkShopProtocol.ProductCategory.BARRIER_POWER_UPGRADE:
- return _upgrades.purchase_barrier_power(_wallet)
- NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE:
- return _cooler_capacity.purchase(_wallet)
- NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE:
- return (
- _inventory_layout != null
- and _inventory_layout.purchase_backpack(_wallet)
- )
- NetworkShopProtocol.ProductCategory.ART_KIT:
- return (
- _wallet.debit(int(data["total_cost"]))
- and _bag.add_item(product_id, 1)
- )
- NetworkShopProtocol.ProductCategory.ART_UPGRADE:
- return _art_unlocks.purchase_product(
- product_id, _wallet, int(data["total_cost"])
- )
- NetworkShopProtocol.ProductCategory.DECOR:
- return (
- _wallet.debit(int(data["total_cost"]))
- and _bag.add_item(product_id, 1)
- and _home_state.add_owned_decor(product_id, 1)
- )
- return false
-
-
-func _fail_local_apply(data: Dictionary, message: String) -> void:
- _finish_local_purchase(data, false, message)
- _acknowledge_result(data, false, message)
-
-
-func _finish_local_purchase(
- data: Dictionary,
- accepted: bool,
- message: String,
-) -> void:
- _pending_local_request.clear()
- local_purchase_finished.emit(
- str(data["request_id"]),
- accepted,
- message,
- StringName(str(data["product_id"])),
- int(data["category"]),
- int(data["quantity"]),
- int(data["total_cost"])
- )
-
-
-func _acknowledge_result(
- data: Dictionary,
- applied: bool,
- message: String,
-) -> void:
- if _session.is_host():
- _handle_acknowledgement(
- _session.get_local_peer_id(),
- str(data["result_id"]),
- str(data["request_id"]),
- applied,
- message
- )
- else:
- acknowledge_purchase_result.rpc_id(
- 1,
- str(data["result_id"]),
- str(data["request_id"]),
- applied,
- message.left(NetworkShopProtocol.MAX_MESSAGE_LENGTH)
- )
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- NetworkShopProtocol.RELIABLE_CHANNEL
-)
-func acknowledge_purchase_result(
- result_id: String,
- request_id: String,
- applied: bool,
- message: String,
-) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if (
- _session == null
- or not _session.is_host()
- or not _session.is_authenticated_peer(sender_id)
- ):
- return
- _handle_acknowledgement(
- sender_id, result_id, request_id, applied, message
- )
-
-
-func _handle_acknowledgement(
- peer_id: int,
- result_id: String,
- request_id: String,
- applied: bool,
- message: String,
-) -> void:
- if (
- result_id.is_empty()
- or result_id.length() > NetworkShopProtocol.MAX_ID_LENGTH
- or request_id.is_empty()
- or request_id.length() > NetworkShopProtocol.MAX_ID_LENGTH
- or message.length() > NetworkShopProtocol.MAX_MESSAGE_LENGTH
- or _result_owners.get(result_id, 0) != peer_id
- ):
- return
- if _network_home != null:
- _network_home.finalize_decor_purchase(
- peer_id, result_id, applied
- )
- _acknowledged_results[result_id] = true
- _bound_dictionary(_acknowledged_results)
- if _pending_by_peer.get(peer_id, "") == request_id:
- _pending_by_peer.erase(peer_id)
-
-
-func _is_shop_available_for_peer(
- peer_id: int,
- shop_id: StringName,
-) -> bool:
- var interaction: FishingShopInteraction = (
- _decor_interaction
- if shop_id == DECOR_SHOP_ID
- else _interaction
- )
- if (
- _session == null
- or not _session.is_host()
- or not _session.is_gameplay_session_active()
- or _spawn_service == null
- or interaction == null
- or not is_instance_valid(interaction)
- ):
- return false
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- return (
- avatar != null
- and not avatar.is_water_recovery_active()
- and (
- _network_fishing == null
- or not _network_fishing.has_peer_attempt(peer_id)
- )
- and interaction.is_avatar_in_range(avatar)
- )
-
-
-func _on_peer_removed(peer_id: int) -> void:
- _request_ledgers.erase(peer_id)
- _pending_by_peer.erase(peer_id)
- for result_id: String in _result_owners.keys():
- if _result_owners[result_id] == peer_id:
- _result_owners.erase(result_id)
- _acknowledged_results.erase(result_id)
-
-
-func _on_session_state_changed(state: NetworkSession.State) -> void:
- if state in [
- NetworkSession.State.INACTIVE,
- NetworkSession.State.DISCONNECTING,
- NetworkSession.State.CONNECTION_FAILED,
- NetworkSession.State.SERVER_LOST,
- ]:
- var had_pending: bool = is_local_purchase_pending()
- _clear_session_state()
- if had_pending:
- local_purchase_finished.emit(
- "",
- false,
- "Connection lost.",
- StringName(),
- 0,
- 0,
- 0
- )
-
-
-func _clear_session_state() -> void:
- _request_ledgers.clear()
- _pending_by_peer.clear()
- _result_owners.clear()
- _acknowledged_results.clear()
- _received_results.clear()
- _applied_results.clear()
- _pending_local_request.clear()
-
-
-func _bound_dictionary(values: Dictionary) -> void:
- while values.size() > MAX_LEDGER_ENTRIES_PER_PEER:
- values.erase(values.keys().front())
-
-
-func _new_id(prefix: String) -> String:
- return "%s:%s" % [
- prefix,
- Crypto.new().generate_random_bytes(16).hex_encode(),
- ]
diff --git a/network/network_shop_service.gd.uid b/network/network_shop_service.gd.uid
deleted file mode 100644
index 0a102ea..0000000
--- a/network/network_shop_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://4ci4dbhyfc11
diff --git a/network/network_surface_drawing_service.gd b/network/network_surface_drawing_service.gd
deleted file mode 100644
index 485dfaa..0000000
--- a/network/network_surface_drawing_service.gd
+++ /dev/null
@@ -1,2950 +0,0 @@
-class_name NetworkSurfaceDrawingService
-extends Node
-
-const BrushHighlightShader: Shader = preload(
- "res://drawing/surface_drawing_highlight.gdshader"
-)
-const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
-
-signal hud_state_changed(
- is_active: bool,
- mode_name: String,
- color_name: String,
- color_value: Color,
- brush_size: int,
- grid_size: int,
- status: String,
-)
-signal session_artwork_changed(canvas_count: int, painted_cell_count: int)
-
-const SOLID_SURFACE_MASK: int = 1
-const MAX_DRAW_DISTANCE: float = 16.0
-const SURFACE_VALIDATION_DEPTH: float = 0.5
-const MIN_SURFACE_NORMAL_DOT: float = 0.35
-const EDIT_INTERVAL_MSEC: int = 45
-const REQUEST_WINDOW_MSEC: int = 1000
-const MAX_REQUESTS_PER_WINDOW: int = 32
-const MAX_RECENT_REQUEST_IDS: int = 64
-const GRID_PREVIEW_SURFACE_OFFSET: float = 0.03
-const POINTER_EDGE_MARGIN: float = 2.0
-const ARTWORK_DIRECTORY_NAME: String = "artwork"
-const STAMP_ALPHA_THRESHOLD: float = 0.5
-const STAMP_COLOR_TOLERANCE: float = 0.012
-const INVALID_POINTER_SCREEN_POSITION := Vector2(-1.0, -1.0)
-
-enum GuideAction {
- NONE,
- HIDE,
- RESTORE,
- FINALIZE,
-}
-
-var _session: NetworkSession
-var _spawn_service: PlayerSpawnService
-var _relationships: PlayerRelationshipStore
-var _local_player: Player
-var _bag: PlayerBag
-var _hotbar: PlayerHotbar
-var _art_unlocks: PlayerArtUnlocks
-var _data_root: PlayerDataRoot
-var _drawing_root: Node3D
-var _canvas_states: Dictionary[String, Dictionary] = {}
-var _canvas_nodes: Dictionary[String, SurfaceDrawingCanvas] = {}
-var _selected_canvas_id: String = ""
-var _hovered_canvas_id: String = ""
-var _last_hovered_canvas_id: String = ""
-var _brush_preview: MultiMeshInstance3D
-var _brush_preview_multimesh: MultiMesh
-var _brush_preview_material: ShaderMaterial
-var _placement_preview: MeshInstance3D
-var _placement_preview_material: StandardMaterial3D
-var _stamp_preview: MeshInstance3D
-var _stamp_preview_material: StandardMaterial3D
-var _active: bool = false
-var _placing_grid: bool = false
-var _stamp_mode: bool = false
-var _stamp_path: String = ""
-var _stamp_pixels := PackedByteArray()
-var _stamp_image: Image
-var _brush_size: int = 1
-var _grid_size: int = SurfaceDrawingProtocol.DEFAULT_GRID_SIZE
-var _color_ids: Array[StringName] = []
-var _color_index: int = 0
-var _painting: bool = false
-var _erasing: bool = false
-var _eraser_mode: bool = false
-var _armed_guide_action: int = GuideAction.NONE
-var _armed_return_placement_mode: bool = false
-var _armed_return_eraser_mode: bool = false
-var _camera_look_active: bool = false
-var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
-var _last_edited_cell := Vector3i(-1, -1, -1)
-var _active_stroke_id: String = ""
-var _last_local_stroke_id: String = ""
-var _last_edit_msec: int = 0
-var _aim_hit: Dictionary = {}
-var _pointer_screen_position: Vector2 = Vector2.ZERO
-var _request_sequence: int = 0
-var _canvas_sequence: int = 0
-var _peer_request_times: Dictionary[int, PackedInt64Array] = {}
-var _peer_request_ids: Dictionary[int, PackedStringArray] = {}
-var _stroke_history_by_peer: Dictionary[int, Dictionary] = {}
-var _cell_last_stroke: Dictionary[String, String] = {}
-var _peer_art_entitlements: Dictionary[int, Dictionary] = {}
-
-
-func setup(
- session: NetworkSession,
- spawn_service: PlayerSpawnService,
- relationships: PlayerRelationshipStore,
- local_player: Player,
- drawing_root: Node3D,
- bag: PlayerBag,
- hotbar: PlayerHotbar,
- art_unlocks: PlayerArtUnlocks,
- data_root: PlayerDataRoot = null,
-) -> void:
- _session = session
- _spawn_service = spawn_service
- _relationships = relationships
- _local_player = local_player
- _drawing_root = drawing_root
- _bag = bag
- _hotbar = hotbar
- _art_unlocks = art_unlocks
- _data_root = data_root
- _refresh_local_unlocks()
- if _session != null:
- _session.state_changed.connect(_on_session_state_changed)
- _session.peer_removed.connect(_on_peer_removed)
- if _relationships != null:
- _relationships.relationship_changed.connect(
- _on_relationship_changed
- )
- if _bag != null and not _bag.contents_changed.is_connected(
- _on_local_art_entitlement_changed
- ):
- _bag.contents_changed.connect(_on_local_art_entitlement_changed)
- if _art_unlocks != null and not _art_unlocks.unlocks_changed.is_connected(
- _on_local_art_unlocks_changed
- ):
- _art_unlocks.unlocks_changed.connect(_on_local_art_unlocks_changed)
- _create_brush_preview()
- _create_placement_preview()
- set_process(true)
- _emit_hud_state("")
-
-
-func handle_input(
- event: InputEvent,
- can_open: bool,
- pointer_screen_position: Vector2 = INVALID_POINTER_SCREEN_POSITION,
-) -> bool:
- if not _active:
- return false
- if not can_open:
- deactivate()
- return false
- if event.is_action_pressed("ui_cancel"):
- if _armed_guide_action != GuideAction.NONE:
- _cancel_armed_guide_action()
- return true
- if _placing_grid:
- _set_placement_mode(false)
- return true
- return false
- if event is InputEventKey:
- var key_event := event as InputEventKey
- if not key_event.pressed or key_event.echo:
- return false
- if key_event.ctrl_pressed and key_event.physical_keycode == KEY_Z:
- request_undo_last_stroke()
- return true
- match key_event.physical_keycode:
- KEY_Q:
- if not _placing_grid:
- _cycle_color(-1)
- return true
- KEY_E:
- if not _placing_grid:
- _cycle_color(1)
- return true
- KEY_R:
- _set_placement_mode(not _placing_grid)
- return true
- if event is InputEventMouseButton:
- var mouse_event := event as InputEventMouseButton
- # Ordinary wheel input always belongs to Hotbar selection. Brush size is
- # selected explicitly in the Art Kit toolbar.
- if mouse_event.button_index in [
- MOUSE_BUTTON_WHEEL_UP,
- MOUSE_BUTTON_WHEEL_DOWN,
- ]:
- return false
- _set_pointer_from_input(
- mouse_event.position,
- pointer_screen_position,
- )
- _update_aim()
- match mouse_event.button_index:
- MOUSE_BUTTON_LEFT:
- if _placing_grid:
- if mouse_event.pressed:
- if _armed_guide_action != GuideAction.NONE:
- _execute_armed_guide_action()
- elif _stamp_mode:
- _request_stamp_at_aim()
- elif mouse_event.shift_pressed and mouse_event.ctrl_pressed:
- _finalize_selected_guide()
- elif mouse_event.shift_pressed:
- _remove_selected_guide()
- else:
- _request_canvas_at_aim()
- return true
- if mouse_event.pressed:
- _begin_stroke()
- _erasing = (
- mouse_event.pressed
- and (mouse_event.shift_pressed or _eraser_mode)
- )
- _painting = mouse_event.pressed and not _erasing
- if _painting or _erasing:
- _submit_current_brush(_erasing, true)
- else:
- _finish_stroke()
- return true
- MOUSE_BUTTON_RIGHT:
- _camera_look_active = mouse_event.pressed
- _reset_stroke()
- return false
- if event is InputEventMouseMotion:
- var motion_event := event as InputEventMouseMotion
- if _camera_look_active:
- return false
- _set_pointer_from_input(
- motion_event.position,
- pointer_screen_position,
- )
- _update_aim()
- return true
- return false
-
-
-func activate(
- pointer_screen_position: Vector2 = INVALID_POINTER_SCREEN_POSITION,
-) -> void:
- if _active or not can_activate():
- return
- _active = true
- _placing_grid = false
- _last_hovered_canvas_id = ""
- _clear_stamp_mode()
- _eraser_mode = false
- _clear_armed_guide_action(false)
- _refresh_local_unlocks()
- _rebuild_placement_preview()
- _reset_stroke()
- var initial_pointer_position: Vector2 = pointer_screen_position
- if not _is_valid_pointer_screen_position(initial_pointer_position):
- initial_pointer_position = get_viewport().get_mouse_position()
- _pointer_screen_position = _clamped_pointer_position(
- initial_pointer_position
- )
- # Art-kit activation can occur while the prior right-click camera drag is
- # still held. Stop that drag before recording a return mode, then restore
- # normal camera availability so right-click continues to work while drawing.
- _stop_camera_drag_for_art_kit()
- _prior_mouse_mode = _get_restorable_mouse_mode(Input.mouse_mode)
- Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
- _update_aim()
- _refresh_stencil_visibility()
- _emit_hud_state("marker selected")
-
-
-func deactivate() -> void:
- if not _active:
- return
- _active = false
- _placing_grid = false
- _clear_stamp_mode()
- _eraser_mode = false
- _clear_armed_guide_action(false)
- _camera_look_active = false
- # Do not leave a drag captured when drawing is closed by a menu, hotbar
- # change, or session transition. The next camera drag must start on a fresh
- # physical right-click press.
- _stop_camera_drag_for_art_kit()
- _reset_stroke()
- _selected_canvas_id = ""
- _hovered_canvas_id = ""
- _last_hovered_canvas_id = ""
- _aim_hit.clear()
- _hide_previews()
- _refresh_stencil_visibility()
- Input.mouse_mode = _get_restorable_mouse_mode(_prior_mouse_mode)
- _emit_hud_state("")
-
-
-func _stop_camera_drag_for_art_kit() -> void:
- if _local_player == null or not is_instance_valid(_local_player):
- return
- var camera_input_enabled: bool = _local_player.is_camera_input_enabled()
- _local_player.set_camera_input_enabled(false)
- _local_player.set_camera_input_enabled(camera_input_enabled)
-
-
-func _get_restorable_mouse_mode(mouse_mode: Input.MouseMode) -> Input.MouseMode:
- if mouse_mode == Input.MOUSE_MODE_CAPTURED:
- return Input.MOUSE_MODE_VISIBLE
- return mouse_mode
-
-
-func is_active() -> bool:
- return _active
-
-
-func is_placement_mode() -> bool:
- return _placing_grid
-
-
-func can_activate() -> bool:
- return (
- _drawing_available()
- and _owns_art_kit()
- and _art_kit_is_selected()
- )
-
-
-func set_placement_mode(enabled: bool) -> void:
- _set_placement_mode(enabled)
-
-
-func set_color_id(color_id: StringName) -> bool:
- var color_index: int = _color_ids.find(color_id)
- if color_index < 0:
- return false
- _clear_armed_guide_action(true)
- _clear_stamp_mode()
- _eraser_mode = false
- _color_index = color_index
- _reset_stroke()
- _emit_hud_state("")
- return true
-
-
-func set_eraser_mode(enabled: bool) -> void:
- if not _active:
- return
- _clear_armed_guide_action(true)
- _clear_stamp_mode()
- _eraser_mode = enabled
- if _eraser_mode and _placing_grid:
- _placing_grid = false
- _reset_stroke()
- _selected_canvas_id = ""
- _update_aim()
- _refresh_stencil_visibility()
- _emit_hud_state("eraser selected" if _eraser_mode else "marker selected")
-
-
-func is_eraser_mode() -> bool:
- return _eraser_mode
-
-
-func arm_guide_action(action: int) -> bool:
- if (
- not _active
- or action not in [GuideAction.HIDE, GuideAction.RESTORE, GuideAction.FINALIZE]
- ):
- return false
- if _armed_guide_action == action:
- _cancel_armed_guide_action()
- return false
- if _armed_guide_action == GuideAction.NONE:
- _armed_return_placement_mode = _placing_grid
- _armed_return_eraser_mode = _eraser_mode
- _clear_stamp_mode()
- _armed_guide_action = action
- _placing_grid = true
- _eraser_mode = false
- _reset_stroke()
- _selected_canvas_id = ""
- _update_aim()
- _refresh_stencil_visibility()
- _emit_hud_state(_guide_action_status(action))
- return true
-
-
-func get_armed_guide_action() -> int:
- return _armed_guide_action
-
-
-func cancel_armed_guide_action() -> void:
- _cancel_armed_guide_action()
-
-
-func set_brush_size(value: int) -> bool:
- if _art_unlocks == null or not _art_unlocks.is_brush_size_unlocked(value):
- return false
- _set_brush_size(value)
- return true
-
-
-func set_grid_size(value: int) -> bool:
- if _art_unlocks == null or not _art_unlocks.is_grid_size_unlocked(value):
- return false
- if _grid_size == value:
- return true
- _clear_stamp_mode()
- _grid_size = value
- _rebuild_placement_preview()
- _update_previews()
- _emit_hud_state("")
- return true
-
-
-func get_brush_size() -> int:
- return _brush_size
-
-
-func get_grid_size() -> int:
- return _grid_size
-
-
-func get_color_id() -> StringName:
- return _current_color_id()
-
-
-func get_pointer_screen_position() -> Vector2:
- return _pointer_screen_position
-
-
-func set_unlocked_color_ids(unlocked_ids: Array[StringName]) -> void:
- _color_ids = SurfaceDrawingPalette.filter_unlocked_ids(unlocked_ids)
- _color_index = clampi(_color_index, 0, _color_ids.size() - 1)
- _emit_hud_state("")
-
-
-func _refresh_local_unlocks() -> void:
- if _art_unlocks == null:
- _color_ids = [SurfaceDrawingPalette.DEFAULT_COLOR_ID]
- return
- var previous_color: StringName = _current_color_id()
- _color_ids = _art_unlocks.get_unlocked_color_ids()
- var previous_index: int = _color_ids.find(previous_color)
- _color_index = previous_index if previous_index >= 0 else 0
- if not _art_unlocks.is_brush_size_unlocked(_brush_size):
- _brush_size = PlayerArtUnlocks.BASE_BRUSH_SIZE
- if not _art_unlocks.is_grid_size_unlocked(_grid_size):
- _grid_size = PlayerArtUnlocks.BASE_GRID_SIZE
-
-
-func _owns_art_kit() -> bool:
- return _bag != null and _bag.owns_item(ArtShopStockType.ART_KIT_ITEM_ID)
-
-
-func _art_kit_is_selected() -> bool:
- return (
- _hotbar != null
- and _hotbar.get_selected_assignment_kind()
- == PlayerHotbar.AssignmentKind.ITEM
- and _hotbar.get_selected_item_id() == ArtShopStockType.ART_KIT_ITEM_ID
- )
-
-
-func _local_entitlement() -> Dictionary:
- return {
- "has_kit": _owns_art_kit(),
- "unlock_mask": (
- _art_unlocks.get_unlock_mask() if _art_unlocks != null else 0
- ),
- }
-
-
-func _publish_local_entitlement() -> void:
- if _session == null or not _session.is_gameplay_session_active():
- return
- var entitlement: Dictionary = _local_entitlement()
- if _session.is_host():
- _peer_art_entitlements[_session.get_local_peer_id()] = entitlement
- else:
- submit_art_entitlement.rpc_id(
- 1,
- _session.get_session_id(),
- bool(entitlement["has_kit"]),
- int(entitlement["unlock_mask"]),
- )
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- SurfaceDrawingProtocol.RELIABLE_CHANNEL,
-)
-func submit_art_entitlement(
- session_id: String,
- has_kit: bool,
- unlock_mask: int,
-) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if (
- _session == null
- or not _session.is_host()
- or not _session.is_authenticated_peer(sender_id)
- or session_id != _session.get_session_id()
- or unlock_mask < 0
- or (unlock_mask & ~PlayerArtUnlocks.ALL_UNLOCK_MASK) != 0
- ):
- return
- _peer_art_entitlements[sender_id] = {
- "has_kit": has_kit,
- "unlock_mask": unlock_mask,
- }
-
-
-func _peer_entitlement(peer_id: int) -> Dictionary:
- if _session != null and peer_id == _session.get_local_peer_id():
- return _local_entitlement()
- return Dictionary(_peer_art_entitlements.get(peer_id, {}))
-
-
-func _peer_owns_art_kit(peer_id: int) -> bool:
- return bool(_peer_entitlement(peer_id).get("has_kit", false))
-
-
-func _peer_can_place_grid(peer_id: int, grid_size: int) -> bool:
- var entitlement: Dictionary = _peer_entitlement(peer_id)
- return (
- bool(entitlement.get("has_kit", false))
- and _mask_unlocks_grid(
- int(entitlement.get("unlock_mask", 0)), grid_size
- )
- )
-
-
-func _peer_can_stamp(peer_id: int, request: Dictionary) -> bool:
- var entitlement: Dictionary = _peer_entitlement(peer_id)
- var unlock_mask: int = int(entitlement.get("unlock_mask", 0))
- if (
- not bool(entitlement.get("has_kit", false))
- or not _mask_owns_product(
- unlock_mask, PlayerArtUnlocks.STAMP_PRODUCT_ID
- )
- or not _mask_unlocks_grid(unlock_mask, int(request["width"]))
- ):
- return false
- var palette_ids: Array[StringName] = SurfaceDrawingPalette.get_color_ids()
- var pixels: PackedByteArray = request["pixels"]
- for palette_index: int in pixels:
- if palette_index <= 0:
- continue
- var color_id: StringName = palette_ids[palette_index - 1]
- if not _mask_unlocks_color(unlock_mask, color_id):
- return false
- return true
-
-
-func _peer_can_edit(peer_id: int, request: Dictionary) -> bool:
- var entitlement: Dictionary = _peer_entitlement(peer_id)
- if not bool(entitlement.get("has_kit", false)):
- return false
- var unlock_mask: int = int(entitlement.get("unlock_mask", 0))
- if not _mask_unlocks_brush(unlock_mask, int(request["brush_size"])):
- return false
- for value: Variant in request["edits"]:
- var edit: Dictionary = value
- var color_id := StringName(str(edit.get("color_id", "")))
- if not color_id.is_empty() and not _mask_unlocks_color(
- unlock_mask, color_id
- ):
- return false
- return true
-
-
-func _mask_unlocks_color(unlock_mask: int, color_id: StringName) -> bool:
- if color_id == SurfaceDrawingPalette.DEFAULT_COLOR_ID:
- return true
- for product_id: StringName in PlayerArtUnlocks.COLOR_PRODUCTS:
- if (
- PlayerArtUnlocks.color_id_for_product(product_id) == color_id
- and _mask_owns_product(unlock_mask, product_id)
- ):
- return true
- return false
-
-
-func _mask_unlocks_brush(unlock_mask: int, brush_size: int) -> bool:
- if brush_size == PlayerArtUnlocks.BASE_BRUSH_SIZE:
- return true
- for product_id: StringName in PlayerArtUnlocks.BRUSH_PRODUCTS:
- if (
- PlayerArtUnlocks.brush_size_for_product(product_id) == brush_size
- and _mask_owns_product(unlock_mask, product_id)
- ):
- return true
- return false
-
-
-func _mask_unlocks_grid(unlock_mask: int, grid_size: int) -> bool:
- if grid_size == PlayerArtUnlocks.BASE_GRID_SIZE:
- return true
- for product_id: StringName in PlayerArtUnlocks.GRID_PRODUCTS:
- if (
- PlayerArtUnlocks.grid_size_for_product(product_id) == grid_size
- and _mask_owns_product(unlock_mask, product_id)
- ):
- return true
- return false
-
-
-func _mask_owns_product(unlock_mask: int, product_id: StringName) -> bool:
- var bit: int = PlayerArtUnlocks.get_product_bit(product_id)
- return bit >= 0 and (unlock_mask & (1 << bit)) != 0
-
-
-func request_canvas_at_surface(
- origin: Vector3,
- normal: Vector3,
- tangent: Vector3,
-) -> bool:
- if (
- not can_activate()
- or _art_unlocks == null
- or not _art_unlocks.is_grid_size_unlocked(_grid_size)
- or normal.is_zero_approx()
- or tangent.is_zero_approx()
- ):
- return false
- var data: Dictionary = {
- "request_id": _new_request_id("canvas"),
- "session_id": _session.get_session_id(),
- "origin": SurfaceDrawingProtocol.vector_to_array(origin),
- "normal": SurfaceDrawingProtocol.vector_to_array(normal.normalized()),
- "tangent": SurfaceDrawingProtocol.vector_to_array(tangent.normalized()),
- "width": _grid_size,
- "height": _grid_size,
- "cell_size": SurfaceDrawingProtocol.CELL_SIZE,
- }
- if _session.is_host():
- _handle_canvas_request(_session.get_local_peer_id(), data)
- else:
- submit_canvas_request.rpc_id(1, data)
- return true
-
-
-func request_stamp_at_surface(
- origin: Vector3,
- normal: Vector3,
- tangent: Vector3,
-) -> bool:
- if (
- not can_activate()
- or not _stamp_mode
- or _stamp_pixels.size() != _grid_size * _grid_size
- or _art_unlocks == null
- or not _art_unlocks.is_stamp_unlocked()
- or not _art_unlocks.is_grid_size_unlocked(_grid_size)
- or normal.is_zero_approx()
- or tangent.is_zero_approx()
- ):
- return false
- var data: Dictionary = {
- "request_id": _new_request_id("stamp"),
- "session_id": _session.get_session_id(),
- "origin": SurfaceDrawingProtocol.vector_to_array(origin),
- "normal": SurfaceDrawingProtocol.vector_to_array(normal.normalized()),
- "tangent": SurfaceDrawingProtocol.vector_to_array(tangent.normalized()),
- "width": _grid_size,
- "height": _grid_size,
- "cell_size": SurfaceDrawingProtocol.CELL_SIZE,
- "pixels": _stamp_pixels,
- }
- if not SurfaceDrawingProtocol.validate_stamp_request(data):
- return false
- if _session.is_host():
- _handle_stamp_request(_session.get_local_peer_id(), data)
- else:
- submit_stamp_request.rpc_id(1, data)
- return true
-
-
-func request_cell_edits(
- canvas_id: String,
- edits: Array[Dictionary],
- stroke_id: String = "",
-) -> bool:
- if (
- not can_activate()
- or canvas_id.is_empty()
- or edits.is_empty()
- ):
- return false
- var resolved_stroke_id: String = stroke_id
- if resolved_stroke_id.is_empty():
- resolved_stroke_id = _new_request_id("stroke")
- _last_local_stroke_id = resolved_stroke_id
- var data: Dictionary = {
- "request_id": _new_request_id("edit"),
- "session_id": _session.get_session_id(),
- "canvas_id": canvas_id,
- "stroke_id": resolved_stroke_id,
- "brush_size": _brush_size,
- "edits": edits,
- }
- if not SurfaceDrawingProtocol.validate_edit_request(data):
- return false
- if _session.is_host():
- _handle_edit_request(_session.get_local_peer_id(), data)
- else:
- submit_edit_request.rpc_id(1, data)
- return true
-
-
-func request_guide_visibility(
- canvas_id: String,
- should_be_visible: bool,
- should_finalize: bool = false,
-) -> bool:
- if not can_activate() or canvas_id.is_empty():
- return false
- var data: Dictionary = {
- "request_id": _new_request_id("guide"),
- "session_id": _session.get_session_id(),
- "canvas_id": canvas_id,
- "guide_visible": should_be_visible and not should_finalize,
- "finalized": should_finalize,
- }
- if not SurfaceDrawingProtocol.validate_guide_request(data):
- return false
- if _session.is_host():
- _handle_guide_request(_session.get_local_peer_id(), data)
- else:
- submit_guide_request.rpc_id(1, data)
- return true
-
-
-func request_undo_last_stroke() -> bool:
- if (
- not can_activate()
- or _last_local_stroke_id.is_empty()
- ):
- _emit_hud_state("nothing to undo")
- return false
- var data: Dictionary = {
- "request_id": _new_request_id("undo"),
- "session_id": _session.get_session_id(),
- "stroke_id": _last_local_stroke_id,
- }
- if not SurfaceDrawingProtocol.validate_undo_request(data):
- return false
- _last_local_stroke_id = ""
- if _session.is_host():
- _handle_undo_request(_session.get_local_peer_id(), data)
- else:
- submit_undo_request.rpc_id(1, data)
- _emit_hud_state("undoing last stroke...")
- return true
-
-
-func get_canvas_count() -> int:
- return _canvas_states.size()
-
-
-func get_painted_cell_count() -> int:
- var count: int = 0
- for state: Dictionary in _canvas_states.values():
- count += (state.get("cells", []) as Array).size()
- return count
-
-
-func clear_session_artwork() -> bool:
- if not _drawing_available() or not _session.is_host():
- return false
- _clear_session_artwork_state()
- for peer_id: int in _session.get_authenticated_peer_ids():
- if (
- peer_id != _session.get_local_peer_id()
- and _session.peer_supports_capability(
- peer_id, SurfaceDrawingProtocol.CAPABILITY
- )
- ):
- receive_session_artwork_reset.rpc_id(
- peer_id, _session.get_session_id()
- )
- return true
-
-
-func clear_artwork_by_fingerprint(fingerprint: String) -> int:
- if (
- not _drawing_available()
- or not _session.is_host()
- or not NetworkIdentityCrypto.valid_fingerprint(fingerprint)
- ):
- return -1
- var affected_layers: int = _apply_artwork_fingerprint_clear(fingerprint)
- for peer_id: int in _session.get_authenticated_peer_ids():
- if (
- peer_id != _session.get_local_peer_id()
- and _session.peer_supports_capability(
- peer_id, SurfaceDrawingProtocol.CAPABILITY
- )
- ):
- receive_artwork_fingerprint_clear.rpc_id(
- peer_id,
- _session.get_session_id(),
- fingerprint,
- )
- return affected_layers
-
-
-func get_canvas_ids() -> Array[String]:
- var result: Array[String] = []
- for canvas_id: String in _canvas_states:
- result.append(canvas_id)
- result.sort()
- return result
-
-
-func get_canvas_state(canvas_id: String) -> Dictionary:
- return Dictionary(_canvas_states.get(canvas_id, {})).duplicate(true)
-
-
-func is_stamp_mode() -> bool:
- return _stamp_mode
-
-
-func get_saved_stamp_entries() -> Array[Dictionary]:
- var result: Array[Dictionary] = []
- var directory: String = _artwork_directory()
- if directory.is_empty() or not DirAccess.dir_exists_absolute(directory):
- return result
- var file_names: PackedStringArray = DirAccess.get_files_at(directory)
- file_names.sort()
- file_names.reverse()
- for file_name: String in file_names:
- if file_name.get_extension().to_lower() != "png":
- continue
- var entry: Dictionary = _decode_stamp_file(
- directory.path_join(file_name)
- )
- if entry.is_empty():
- continue
- entry["available"] = _stamp_entry_is_unlocked(entry)
- result.append(entry)
- if result.size() >= 64:
- break
- return result
-
-
-func select_saved_stamp(path: String) -> bool:
- if (
- not _active
- or _art_unlocks == null
- or not _art_unlocks.is_stamp_unlocked()
- ):
- _emit_hud_state("unlock the stamp tool in Art Supplies")
- return false
- var entry: Dictionary = _decode_stamp_file(path)
- if entry.is_empty():
- _emit_hud_state("this artwork PNG cannot be used as a stamp")
- return false
- if not _stamp_entry_is_unlocked(entry):
- _emit_hud_state("this stamp uses an art supply that is still locked")
- return false
- _clear_armed_guide_action(true)
- _stamp_path = str(entry["path"])
- _stamp_pixels = entry["pixels"]
- _stamp_image = entry["image"]
- _stamp_mode = true
- _placing_grid = true
- _eraser_mode = false
- _grid_size = int(entry["grid_size"])
- _reset_stroke()
- _selected_canvas_id = ""
- _rebuild_placement_preview()
- _rebuild_stamp_preview()
- _update_aim()
- _refresh_stencil_visibility()
- _emit_hud_state(
- "stamp selected • click a solid surface to place finalized artwork"
- )
- return true
-
-
-func export_aimed_canvas() -> String:
- _update_aim()
- var canvas_id: String = (
- _hovered_canvas_id
- if not _hovered_canvas_id.is_empty()
- else _last_hovered_canvas_id
- )
- var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id)
- if (
- canvas_id.is_empty()
- or canvas == null
- or not is_instance_valid(canvas)
- or canvas.is_hidden_by_relationship()
- ):
- _last_hovered_canvas_id = ""
- _emit_hud_state("aim at artwork before exporting")
- return ""
- return export_canvas_png(canvas_id)
-
-
-func export_canvas_png(canvas_id: String) -> String:
- var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id)
- if canvas == null or not is_instance_valid(canvas):
- _emit_hud_state("artwork is no longer available")
- return ""
- if canvas.get_rendered_pixel_count() <= 0:
- _emit_hud_state("paint something before exporting")
- return ""
- if _data_root == null or _data_root.root_path.is_empty():
- _emit_hud_state("choose a straywild data folder before exporting")
- return ""
- var export_directory: String = _artwork_directory()
- if DirAccess.make_dir_recursive_absolute(export_directory) != OK:
- _emit_hud_state("the artwork folder could not be created")
- return ""
- var image: Image = canvas.get_export_image()
- if image == null or image.is_empty():
- _emit_hud_state("the artwork image could not be prepared")
- return ""
- var export_path: String = _next_artwork_export_path(export_directory)
- if image.save_png(export_path) != OK:
- _emit_hud_state("the artwork PNG could not be saved")
- return ""
- print("Art Kit artwork exported: ", export_path)
- _emit_hud_state("artwork exported to the data folder • artwork")
- return export_path
-
-
-func _artwork_directory() -> String:
- if _data_root == null or _data_root.root_path.is_empty():
- return ""
- return _data_root.root_path.path_join(ARTWORK_DIRECTORY_NAME)
-
-
-func _decode_stamp_file(path: String) -> Dictionary:
- var directory: String = _artwork_directory()
- if directory.is_empty():
- return {}
- var normalized_directory: String = directory.simplify_path().trim_suffix(
- "/"
- )
- var normalized_path: String = path.simplify_path()
- if not normalized_path.begins_with(normalized_directory + "/"):
- return {}
- if normalized_path.get_extension().to_lower() != "png":
- return {}
- var image: Image = Image.load_from_file(normalized_path)
- if image == null or image.is_empty():
- return {}
- if (
- image.get_width() != image.get_height()
- or image.get_width() not in SurfaceDrawingProtocol.GRID_SIZES
- ):
- return {}
- if image.is_compressed() and image.decompress() != OK:
- return {}
- image.convert(Image.FORMAT_RGBA8)
- var grid_size: int = image.get_width()
- var pixels := PackedByteArray()
- pixels.resize(grid_size * grid_size)
- var used_color_ids: Array[StringName] = []
- var painted_count: int = 0
- for cell_y: int in range(grid_size):
- var image_y: int = grid_size - 1 - cell_y
- for x: int in range(grid_size):
- var color: Color = image.get_pixel(x, image_y)
- var palette_index: int = 0
- if color.a >= STAMP_ALPHA_THRESHOLD:
- palette_index = _stamp_palette_index(color)
- if palette_index <= 0:
- return {}
- painted_count += 1
- var color_id: StringName = SurfaceDrawingPalette.get_color_ids()[
- palette_index - 1
- ]
- if color_id not in used_color_ids:
- used_color_ids.append(color_id)
- pixels[cell_y * grid_size + x] = palette_index
- if painted_count <= 0:
- return {}
- return {
- "path": normalized_path,
- "file_name": normalized_path.get_file(),
- "grid_size": grid_size,
- "painted_count": painted_count,
- "used_color_ids": used_color_ids,
- "pixels": pixels,
- "image": image,
- }
-
-
-func _stamp_palette_index(color: Color) -> int:
- var palette_ids: Array[StringName] = SurfaceDrawingPalette.get_color_ids()
- for index: int in range(palette_ids.size()):
- var palette_color: Color = SurfaceDrawingPalette.get_color(
- palette_ids[index]
- )
- var difference := Vector3(
- color.r - palette_color.r,
- color.g - palette_color.g,
- color.b - palette_color.b,
- )
- if difference.length() <= STAMP_COLOR_TOLERANCE:
- return index + 1
- return 0
-
-
-func _stamp_entry_is_unlocked(entry: Dictionary) -> bool:
- if _art_unlocks == null or not _art_unlocks.is_stamp_unlocked():
- return false
- if not _art_unlocks.is_grid_size_unlocked(int(entry.get("grid_size", 0))):
- return false
- for color_id: StringName in entry.get("used_color_ids", []):
- if not _art_unlocks.is_color_unlocked(color_id):
- return false
- return true
-
-
-func _next_artwork_export_path(directory: String) -> String:
- var date_time: Dictionary = Time.get_datetime_dict_from_system()
- var stem: String = "straywild-artwork-%04d%02d%02d-%02d%02d%02d" % [
- int(date_time.get("year", 0)),
- int(date_time.get("month", 0)),
- int(date_time.get("day", 0)),
- int(date_time.get("hour", 0)),
- int(date_time.get("minute", 0)),
- int(date_time.get("second", 0)),
- ]
- var candidate: String = directory.path_join(stem + ".png")
- var suffix: int = 2
- while FileAccess.file_exists(candidate):
- candidate = directory.path_join("%s-%d.png" % [stem, suffix])
- suffix += 1
- return candidate
-
-
-func _process(_delta: float) -> void:
- if not _active:
- return
- if not can_activate():
- deactivate()
- return
- _update_aim()
- if _placing_grid:
- return
- if _painting:
- _submit_current_brush(false)
- elif _erasing:
- _submit_current_brush(true)
-
-
-func _drawing_available() -> bool:
- return (
- _session != null
- and _session.is_gameplay_session_active()
- and (
- _session.is_host()
- or _session.supports_server_capability(
- SurfaceDrawingProtocol.CAPABILITY
- )
- )
- )
-
-
-func _update_aim() -> void:
- _aim_hit = _raycast_from_pointer()
- var previous_canvas_id: String = _selected_canvas_id
- var found_canvas_id: String = ""
- var found_hovered_canvas_id: String = ""
- var nearest_plane_distance: float = INF
- var nearest_hovered_plane_distance: float = INF
- var selected_layer: int = -1
- var hovered_layer: int = -1
- if not _aim_hit.is_empty():
- var hit_position: Vector3 = _aim_hit["position"]
- for canvas_id: String in _canvas_nodes:
- var canvas: SurfaceDrawingCanvas = _canvas_nodes[canvas_id]
- if canvas.is_hidden_by_relationship():
- continue
- var plane_distance: float = canvas.get_surface_plane_distance(
- hit_position
- )
- if not canvas.contains_world_point(
- hit_position,
- SurfaceDrawingCanvas.SURFACE_SAMPLE_DEPTH + 0.05,
- ):
- continue
- var state: Dictionary = _canvas_states.get(canvas_id, {})
- var layer: int = int(state.get("layer", 0))
- if (
- plane_distance < nearest_hovered_plane_distance
- or (
- is_equal_approx(
- plane_distance, nearest_hovered_plane_distance
- )
- and layer > hovered_layer
- )
- ):
- found_hovered_canvas_id = canvas_id
- nearest_hovered_plane_distance = plane_distance
- hovered_layer = layer
- if canvas.is_finalized():
- continue
- if (
- plane_distance < nearest_plane_distance
- or (
- is_equal_approx(plane_distance, nearest_plane_distance)
- and layer > selected_layer
- )
- ):
- found_canvas_id = canvas_id
- nearest_plane_distance = plane_distance
- selected_layer = layer
- _selected_canvas_id = found_canvas_id
- _hovered_canvas_id = found_hovered_canvas_id
- if not _hovered_canvas_id.is_empty():
- _last_hovered_canvas_id = _hovered_canvas_id
- if previous_canvas_id != _selected_canvas_id:
- _reset_stroke()
- _update_previews()
-
-
-func _raycast_from_pointer() -> Dictionary:
- if _local_player == null or not is_instance_valid(_local_player):
- return {}
- var camera: Camera3D = _local_player.get_active_gameplay_camera()
- if camera == null or not camera.current:
- return {}
- var from: Vector3 = camera.project_ray_origin(_pointer_screen_position)
- var direction: Vector3 = camera.project_ray_normal(
- _pointer_screen_position
- ).normalized()
- var to: Vector3 = from + direction * MAX_DRAW_DISTANCE
- var query := PhysicsRayQueryParameters3D.create(
- from,
- to,
- SOLID_SURFACE_MASK,
- )
- query.collide_with_areas = false
- query.collide_with_bodies = true
- query.exclude = [_local_player.get_rid()]
- var hit: Dictionary = camera.get_world_3d().direct_space_state.intersect_ray(query)
- if hit.is_empty() or not _is_static_surface(hit.get("collider")):
- return {}
- return hit
-
-
-func _update_previews() -> void:
- if _placing_grid:
- _update_placement_preview()
- _hide_brush_preview()
- return
- if _placement_preview != null:
- _placement_preview.hide()
- if _stamp_preview != null:
- _stamp_preview.hide()
- _update_brush_preview()
-
-
-func _update_placement_preview() -> void:
- if _placement_preview == null or _aim_hit.is_empty():
- if _placement_preview != null:
- _placement_preview.hide()
- if _stamp_preview != null:
- _stamp_preview.hide()
- return
- var placement: Dictionary = _resolved_placement()
- var normal: Vector3 = placement["normal"]
- var hit_position: Vector3 = placement["origin"]
- var tangent: Vector3 = placement["tangent"]
- var bitangent: Vector3 = normal.cross(tangent).normalized()
- _placement_preview.global_transform = Transform3D(
- Basis(tangent, bitangent, normal),
- hit_position + normal * GRID_PREVIEW_SURFACE_OFFSET,
- )
- if _stamp_preview != null:
- _stamp_preview.global_transform = Transform3D(
- Basis(tangent, bitangent, normal),
- hit_position + normal * (GRID_PREVIEW_SURFACE_OFFSET + 0.001),
- )
- if _placement_preview_material != null:
- _placement_preview_material.albedo_color = (
- Color(0.34, 1.0, 0.72, 0.88)
- if bool(placement["snapped"])
- else Color(0.46, 0.91, 0.95, 0.72)
- )
- _placement_preview.show()
- if _stamp_preview != null:
- _stamp_preview.visible = _stamp_mode
-
-
-func _update_brush_preview() -> void:
- if (
- _brush_preview == null
- or _brush_preview_multimesh == null
- or _aim_hit.is_empty()
- or _selected_canvas_id.is_empty()
- ):
- _hide_brush_preview()
- return
- var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(
- _selected_canvas_id
- )
- if canvas == null:
- _hide_brush_preview()
- return
- var center: Vector2i = canvas.cell_at_world_point(_aim_hit["position"])
- var cells: Array[Vector2i] = _brush_cells(canvas, center)
- if cells.is_empty():
- _hide_brush_preview()
- return
- var root_inverse: Transform3D = _drawing_root.global_transform.affine_inverse()
- for index: int in range(cells.size()):
- var cell: Vector2i = cells[index]
- var world_transform: Transform3D = canvas.get_cell_surface_transform(
- cell.x, cell.y, 0.94
- )
- _brush_preview_multimesh.set_instance_transform(
- index, root_inverse * world_transform
- )
- _brush_preview_multimesh.visible_instance_count = cells.size()
- _brush_preview.show()
-
-
-func _hide_brush_preview() -> void:
- if _brush_preview_multimesh != null:
- _brush_preview_multimesh.visible_instance_count = 0
- if _brush_preview != null:
- _brush_preview.hide()
-
-
-func _hide_previews() -> void:
- _hide_brush_preview()
- if _placement_preview != null:
- _placement_preview.hide()
- if _stamp_preview != null:
- _stamp_preview.hide()
-
-
-func _resolved_placement() -> Dictionary:
- var normal: Vector3 = _normalized_or(
- _aim_hit.get("normal", Vector3.UP), Vector3.UP
- )
- var states: Array[Dictionary] = []
- for state: Dictionary in _canvas_states.values():
- states.append(state)
- return SurfaceDrawingPlacement.resolve(
- _aim_hit.get("position", Vector3.ZERO),
- normal,
- _surface_tangent(normal),
- states,
- _grid_size,
- )
-
-
-func _request_canvas_at_aim() -> void:
- if _aim_hit.is_empty():
- _emit_hud_state("aim at a solid surface before placing a grid")
- return
- var placement: Dictionary = _resolved_placement()
- var normal: Vector3 = placement["normal"]
- var origin: Vector3 = placement["origin"]
- if not _selected_canvas_id.is_empty():
- var existing_state: Dictionary = _canvas_states.get(
- _selected_canvas_id, {}
- )
- if not existing_state.is_empty():
- if not bool(existing_state.get("guide_visible", true)):
- if request_guide_visibility(_selected_canvas_id, true):
- if not _session.is_host():
- _emit_hud_state("restoring grid guide...")
- else:
- _emit_hud_state("a shared grid already covers this area")
- return
- if _overlaps_existing_canvas(origin, normal, _grid_size):
- _emit_hud_state("a shared grid already covers this area")
- return
- if request_canvas_at_surface(origin, normal, placement["tangent"]):
- _emit_hud_state(
- "placing snapped shared grid..."
- if bool(placement["snapped"])
- else "placing shared grid..."
- )
-
-
-func _request_stamp_at_aim() -> void:
- if _aim_hit.is_empty():
- _emit_hud_state("aim at a solid surface before placing a stamp")
- return
- var placement: Dictionary = _resolved_placement()
- var normal: Vector3 = placement["normal"]
- var origin: Vector3 = placement["origin"]
- if _overlaps_existing_canvas(origin, normal, _grid_size):
- _emit_hud_state("an active shared grid already covers this area")
- return
- if request_stamp_at_surface(origin, normal, placement["tangent"]):
- _emit_hud_state("placing finalized stamp...")
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- SurfaceDrawingProtocol.RELIABLE_CHANNEL,
-)
-func submit_canvas_request(data: Dictionary) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(sender_id):
- _handle_canvas_request(sender_id, data)
-
-
-func _handle_canvas_request(peer_id: int, data: Dictionary) -> void:
- var requested_size: int = int(data.get("width", 0))
- if (
- not _session.is_host()
- or not SurfaceDrawingProtocol.validate_canvas_request(data)
- or str(data["session_id"]) != _session.get_session_id()
- or not _accept_request(peer_id, str(data["request_id"]))
- or not _peer_can_place_grid(peer_id, requested_size)
- or _canvas_states.size() >= SurfaceDrawingProtocol.MAX_CANVASES
- or _active_canvas_count() >= SurfaceDrawingProtocol.MAX_ACTIVE_CANVASES
- or _allocated_grid_cells() + requested_size * requested_size
- > SurfaceDrawingProtocol.MAX_SESSION_GRID_CELLS
- ):
- return
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- var requested_origin: Vector3 = SurfaceDrawingProtocol.array_to_vector(
- data["origin"]
- )
- if (
- avatar == null
- or avatar.global_position.distance_to(requested_origin)
- > MAX_DRAW_DISTANCE + 2.0
- ):
- return
- var requested_normal: Vector3 = SurfaceDrawingProtocol.array_to_vector(
- data["normal"]
- ).normalized()
- var surface_hit: Dictionary = _validate_surface(
- requested_origin, requested_normal
- )
- if surface_hit.is_empty():
- return
- var normal: Vector3 = _normalized_or(
- surface_hit.get("normal", requested_normal), requested_normal
- )
- if _overlaps_existing_canvas(
- surface_hit["position"], normal, requested_size
- ):
- return
- var tangent: Vector3 = SurfaceDrawingProtocol.array_to_vector(
- data["tangent"]
- )
- tangent = (tangent - normal * tangent.dot(normal)).normalized()
- if tangent.is_zero_approx():
- tangent = _surface_tangent(normal)
- _canvas_sequence += 1
- var canvas_id: String = "%s-%d" % [
- _session.get_session_id().left(16), _canvas_sequence,
- ]
- var record: PeerRegistry.PeerRecord = _session.get_peer_record(peer_id)
- if record == null or not record.identity_authenticated:
- return
- var state: Dictionary = {
- "session_id": _session.get_session_id(),
- "canvas_id": canvas_id,
- "origin": SurfaceDrawingProtocol.vector_to_array(
- surface_hit["position"]
- ),
- "normal": SurfaceDrawingProtocol.vector_to_array(normal),
- "tangent": SurfaceDrawingProtocol.vector_to_array(tangent),
- "width": requested_size,
- "height": requested_size,
- "cell_size": SurfaceDrawingProtocol.CELL_SIZE,
- "revision": 0,
- "guide_visible": true,
- "finalized": false,
- "layer": _canvas_sequence,
- "creator_fingerprint": record.identity_fingerprint,
- "participant_fingerprints": [record.identity_fingerprint],
- "cells": [],
- }
- _apply_canvas_state(state)
- _broadcast_canvas_state(state)
- _emit_hud_state("shared grid placed • everyone can draw here")
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- SurfaceDrawingProtocol.RELIABLE_CHANNEL,
-)
-func submit_stamp_request(data: Dictionary) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(sender_id):
- _handle_stamp_request(sender_id, data)
-
-
-func _handle_stamp_request(peer_id: int, data: Dictionary) -> void:
- var requested_size: int = int(data.get("width", 0))
- if (
- not _session.is_host()
- or not SurfaceDrawingProtocol.validate_stamp_request(data)
- or str(data["session_id"]) != _session.get_session_id()
- or not _accept_request(peer_id, str(data["request_id"]))
- or not _peer_can_stamp(peer_id, data)
- or _canvas_states.size() >= SurfaceDrawingProtocol.MAX_CANVASES
- or _allocated_grid_cells() + requested_size * requested_size
- > SurfaceDrawingProtocol.MAX_SESSION_GRID_CELLS
- ):
- return
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- var requested_origin: Vector3 = SurfaceDrawingProtocol.array_to_vector(
- data["origin"]
- )
- if (
- avatar == null
- or avatar.global_position.distance_to(requested_origin)
- > MAX_DRAW_DISTANCE + 2.0
- ):
- return
- var requested_normal: Vector3 = SurfaceDrawingProtocol.array_to_vector(
- data["normal"]
- ).normalized()
- var surface_hit: Dictionary = _validate_surface(
- requested_origin, requested_normal
- )
- if surface_hit.is_empty():
- return
- var normal: Vector3 = _normalized_or(
- surface_hit.get("normal", requested_normal), requested_normal
- )
- if _overlaps_existing_canvas(
- surface_hit["position"], normal, requested_size
- ):
- return
- var tangent: Vector3 = SurfaceDrawingProtocol.array_to_vector(
- data["tangent"]
- )
- tangent = (tangent - normal * tangent.dot(normal)).normalized()
- if tangent.is_zero_approx():
- tangent = _surface_tangent(normal)
- var record: PeerRegistry.PeerRecord = _session.get_peer_record(peer_id)
- if record == null or not record.identity_authenticated:
- return
- _canvas_sequence += 1
- var canvas_id: String = "%s-%d" % [
- _session.get_session_id().left(16), _canvas_sequence,
- ]
- var state: Dictionary = {
- "session_id": _session.get_session_id(),
- "canvas_id": canvas_id,
- "origin": SurfaceDrawingProtocol.vector_to_array(
- surface_hit["position"]
- ),
- "normal": SurfaceDrawingProtocol.vector_to_array(normal),
- "tangent": SurfaceDrawingProtocol.vector_to_array(tangent),
- "width": requested_size,
- "height": requested_size,
- "cell_size": SurfaceDrawingProtocol.CELL_SIZE,
- "revision": 1,
- "guide_visible": false,
- "finalized": true,
- "layer": _canvas_sequence,
- "creator_fingerprint": record.identity_fingerprint,
- "participant_fingerprints": [record.identity_fingerprint],
- "cells": _stamp_cells(
- data["pixels"], requested_size, record.identity_fingerprint
- ),
- "stamp": true,
- }
- _apply_canvas_state(state)
- _broadcast_canvas_state(state)
- _emit_hud_state("stamp placed • finalized artwork shared with everyone")
-
-
-func _stamp_cells(
- pixels: PackedByteArray,
- grid_size: int,
- author_fingerprint: String,
-) -> Array[Dictionary]:
- var result: Array[Dictionary] = []
- var palette_ids: Array[StringName] = SurfaceDrawingPalette.get_color_ids()
- for cell_key: int in range(pixels.size()):
- var palette_index: int = pixels[cell_key]
- if palette_index <= 0:
- continue
- var cell_y: int = floori(float(cell_key) / float(grid_size))
- result.append({
- "x": cell_key % grid_size,
- "y": cell_y,
- "color_id": str(palette_ids[palette_index - 1]),
- "author_fingerprint": author_fingerprint,
- })
- return result
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- SurfaceDrawingProtocol.RELIABLE_CHANNEL,
-)
-func submit_guide_request(data: Dictionary) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(sender_id):
- _handle_guide_request(sender_id, data)
-
-
-func _handle_guide_request(peer_id: int, data: Dictionary) -> void:
- if (
- not _session.is_host()
- or not SurfaceDrawingProtocol.validate_guide_request(data)
- or str(data["session_id"]) != _session.get_session_id()
- or not _accept_request(peer_id, str(data["request_id"]))
- or not _peer_owns_art_kit(peer_id)
- ):
- return
- var canvas_id: String = str(data["canvas_id"])
- var state: Dictionary = _canvas_states.get(canvas_id, {})
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- var record: PeerRegistry.PeerRecord = _session.get_peer_record(peer_id)
- if (
- state.is_empty()
- or avatar == null
- or record == null
- or not record.identity_authenticated
- ):
- return
- var origin: Vector3 = SurfaceDrawingProtocol.array_to_vector(state["origin"])
- if avatar.global_position.distance_to(origin) > MAX_DRAW_DISTANCE + 2.0:
- return
- var was_finalized: bool = bool(state.get("finalized", false))
- var finalized: bool = bool(data["finalized"])
- if was_finalized:
- return
- var guide_visible: bool = bool(data["guide_visible"]) and not finalized
- if (
- bool(state.get("guide_visible", true)) == guide_visible
- and was_finalized == finalized
- ):
- return
- if not _register_canvas_participant(
- canvas_id, record.identity_fingerprint
- ):
- return
- state = _canvas_states.get(canvas_id, {})
- state["revision"] = int(state["revision"]) + 1
- state["guide_visible"] = guide_visible
- state["finalized"] = finalized
- _canvas_states[canvas_id] = state
- var update: Dictionary = {
- "session_id": _session.get_session_id(),
- "canvas_id": canvas_id,
- "revision": int(state["revision"]),
- "guide_visible": guide_visible,
- "finalized": finalized,
- "participant_fingerprints": state.get(
- "participant_fingerprints", []
- ),
- }
- var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id)
- if canvas != null:
- canvas.apply_guide_update(update)
- _broadcast_guide_update(update)
- _emit_hud_state(
- "grid finalized • artwork remains editable through new grids"
- if finalized
- else "grid guide restored" if guide_visible else "grid guide hidden"
- )
- _emit_artwork_changed()
-
-
-func _submit_current_brush(erasing: bool, force: bool = false) -> void:
- if _selected_canvas_id.is_empty() or _aim_hit.is_empty():
- return
- var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(
- _selected_canvas_id
- )
- if canvas == null:
- return
- var center: Vector2i = canvas.cell_at_world_point(_aim_hit["position"])
- if center.x < 0:
- return
- var stroke_key := Vector3i(
- _selected_canvas_id.hash(), center.x, center.y
- )
- var now: int = Time.get_ticks_msec()
- if (
- not force
- and (
- stroke_key == _last_edited_cell
- or now - _last_edit_msec < EDIT_INTERVAL_MSEC
- )
- ):
- return
- var edits: Array[Dictionary] = []
- for cell: Vector2i in _brush_cells(canvas, center):
- edits.append({
- "x": cell.x,
- "y": cell.y,
- "color_id": "" if erasing else str(_current_color_id()),
- })
- if edits.is_empty():
- return
- _last_edited_cell = stroke_key
- _last_edit_msec = now
- if _active_stroke_id.is_empty():
- _begin_stroke()
- request_cell_edits(_selected_canvas_id, edits, _active_stroke_id)
-
-
-func _brush_cells(
- canvas: SurfaceDrawingCanvas,
- center: Vector2i,
-) -> Array[Vector2i]:
- var result: Array[Vector2i] = []
- if canvas == null or center.x < 0 or center.y < 0:
- return result
- var start_offset: int = -floori(float(_brush_size - 1) * 0.5)
- for y_offset: int in range(start_offset, start_offset + _brush_size):
- for x_offset: int in range(start_offset, start_offset + _brush_size):
- var cell := Vector2i(center.x + x_offset, center.y + y_offset)
- if (
- cell.x < 0
- or cell.x >= canvas.grid_width
- or cell.y < 0
- or cell.y >= canvas.grid_height
- ):
- continue
- result.append(cell)
- return result
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- SurfaceDrawingProtocol.RELIABLE_CHANNEL,
-)
-func submit_edit_request(data: Dictionary) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(sender_id):
- _handle_edit_request(sender_id, data)
-
-
-func _handle_edit_request(peer_id: int, data: Dictionary) -> void:
- if (
- not _session.is_host()
- or not SurfaceDrawingProtocol.validate_edit_request(data)
- or str(data["session_id"]) != _session.get_session_id()
- or not _accept_request(peer_id, str(data["request_id"]))
- or not _peer_can_edit(peer_id, data)
- ):
- return
- var canvas_id: String = str(data["canvas_id"])
- var state: Dictionary = _canvas_states.get(canvas_id, {})
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- var record: PeerRegistry.PeerRecord = _session.get_peer_record(peer_id)
- if (
- state.is_empty()
- or bool(state.get("finalized", false))
- or avatar == null
- or record == null
- ):
- return
- var stroke_id: String = str(data["stroke_id"])
- var grid_width: int = int(state["width"])
- var grid_height: int = int(state["height"])
- var mutations: Dictionary[String, Dictionary] = {}
- for edit_value: Variant in data["edits"]:
- var edit: Dictionary = edit_value
- var x: int = int(edit["x"])
- var y: int = int(edit["y"])
- if x >= grid_width or y >= grid_height:
- continue
- var cell_position: Vector3 = _state_cell_position(
- state, x, y
- )
- if (
- avatar.global_position.distance_to(cell_position)
- > MAX_DRAW_DISTANCE + 2.0
- or _validate_surface(
- cell_position,
- SurfaceDrawingProtocol.array_to_vector(state["normal"]),
- ).is_empty()
- ):
- continue
- var color_id := StringName(str(edit["color_id"]))
- var authoritative: Dictionary = {
- "x": x,
- "y": y,
- "color_id": str(color_id),
- "author_fingerprint": (
- record.identity_fingerprint if not color_id.is_empty() else ""
- ),
- }
- _queue_cell_mutation(
- mutations,
- canvas_id,
- authoritative,
- peer_id,
- stroke_id,
- )
- _queue_overlapping_finished_mutations(
- mutations,
- canvas_id,
- cell_position,
- peer_id,
- stroke_id,
- )
- if mutations.is_empty():
- return
- _publish_cell_mutations(mutations)
-
-
-func _queue_cell_mutation(
- mutations: Dictionary[String, Dictionary],
- canvas_id: String,
- edit: Dictionary,
- peer_id: int,
- stroke_id: String,
-) -> void:
- var state: Dictionary = _canvas_states.get(canvas_id, {})
- if state.is_empty():
- return
- var x: int = int(edit["x"])
- var y: int = int(edit["y"])
- var cell_key: int = _cell_key_for_state(state, x, y)
- var canvas_mutations: Dictionary = mutations.get(
- canvas_id, {}
- )
- var current: Dictionary = (
- _cell_value_from_edit(canvas_mutations[cell_key])
- if canvas_mutations.has(cell_key)
- else _state_cell(state, x, y)
- )
- var next: Dictionary = _cell_value_from_edit(edit)
- if _same_cell_value(current, next):
- return
- var record: PeerRegistry.PeerRecord = _session.get_peer_record(peer_id)
- if (
- record == null
- or not record.identity_authenticated
- or not _register_canvas_participant(
- canvas_id, record.identity_fingerprint
- )
- ):
- return
- _record_stroke_change(
- peer_id, stroke_id, canvas_id, x, y, current, next
- )
- canvas_mutations[cell_key] = edit.duplicate(true)
- mutations[canvas_id] = canvas_mutations
- _cell_last_stroke[_mutation_key(canvas_id, x, y)] = stroke_id
-
-
-func _queue_overlapping_finished_mutations(
- mutations: Dictionary[String, Dictionary],
- target_canvas_id: String,
- world_position: Vector3,
- peer_id: int,
- stroke_id: String,
-) -> void:
- var target_state: Dictionary = _canvas_states.get(target_canvas_id, {})
- if target_state.is_empty():
- return
- var target_normal: Vector3 = SurfaceDrawingProtocol.array_to_vector(
- target_state["normal"]
- ).normalized()
- for canvas_id: String in _canvas_states:
- if canvas_id == target_canvas_id:
- continue
- var state: Dictionary = _canvas_states[canvas_id]
- if not bool(state.get("finalized", false)):
- continue
- var normal: Vector3 = SurfaceDrawingProtocol.array_to_vector(
- state["normal"]
- ).normalized()
- if normal.dot(target_normal) < 0.9:
- continue
- var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id)
- if (
- canvas == null
- or not canvas.contains_world_point(
- world_position,
- SurfaceDrawingCanvas.SURFACE_SAMPLE_DEPTH + 0.05,
- )
- ):
- continue
- var center_cell: Vector2i = canvas.cell_at_world_point(world_position)
- if center_cell.x < 0:
- continue
- for y_offset: int in range(-1, 2):
- for x_offset: int in range(-1, 2):
- var cell := Vector2i(
- center_cell.x + x_offset,
- center_cell.y + y_offset,
- )
- if (
- cell.x < 0
- or cell.x >= canvas.grid_width
- or cell.y < 0
- or cell.y >= canvas.grid_height
- ):
- continue
- var relative: Vector3 = (
- canvas.get_cell_world_position(cell.x, cell.y)
- - world_position
- )
- if (
- absf(relative.dot(canvas.get_surface_tangent()))
- >= SurfaceDrawingProtocol.CELL_SIZE
- or absf(relative.dot(canvas.get_surface_bitangent()))
- >= SurfaceDrawingProtocol.CELL_SIZE
- or _state_cell(state, cell.x, cell.y).is_empty()
- ):
- continue
- _queue_cell_mutation(
- mutations,
- canvas_id,
- {
- "x": cell.x,
- "y": cell.y,
- "color_id": "",
- "author_fingerprint": "",
- },
- peer_id,
- stroke_id,
- )
-
-
-func _record_stroke_change(
- peer_id: int,
- stroke_id: String,
- canvas_id: String,
- x: int,
- y: int,
- before: Dictionary,
- after: Dictionary,
-) -> void:
- var history: Dictionary = _stroke_history_by_peer.get(peer_id, {})
- if str(history.get("stroke_id", "")) != stroke_id:
- history = {"stroke_id": stroke_id, "changes": {}}
- var changes: Dictionary = history["changes"]
- var key: String = _mutation_key(canvas_id, x, y)
- var change: Dictionary = changes.get(key, {})
- if change.is_empty():
- change = {
- "canvas_id": canvas_id,
- "x": x,
- "y": y,
- "before": before.duplicate(true),
- "before_stroke": str(_cell_last_stroke.get(key, "")),
- }
- change["after"] = after.duplicate(true)
- changes[key] = change
- history["changes"] = changes
- _stroke_history_by_peer[peer_id] = history
-
-
-func _publish_cell_mutations(
- mutations: Dictionary[String, Dictionary],
-) -> void:
- var canvas_ids: Array[String] = mutations.keys()
- canvas_ids.sort()
- for canvas_id: String in canvas_ids:
- var state: Dictionary = _canvas_states.get(canvas_id, {})
- if state.is_empty():
- continue
- var cells: Dictionary[int, Dictionary] = _cells_by_key(
- state["cells"], int(state["width"])
- )
- var canvas_mutations: Dictionary = mutations[canvas_id]
- var cell_keys: Array[int] = []
- for cell_key_value: Variant in canvas_mutations.keys():
- cell_keys.append(int(cell_key_value))
- cell_keys.sort()
- var edits: Array[Dictionary] = []
- for cell_key: int in cell_keys:
- var edit: Dictionary = canvas_mutations[cell_key]
- if str(edit["color_id"]).is_empty():
- cells.erase(cell_key)
- else:
- cells[cell_key] = edit.duplicate(true)
- edits.append(edit.duplicate(true))
- if edits.is_empty():
- continue
- state["revision"] = int(state["revision"]) + 1
- state["cells"] = _sorted_cells(cells)
- state["participant_fingerprints"] = _merged_participants(
- state, {"edits": edits}
- )
- _canvas_states[canvas_id] = state
- var update: Dictionary = {
- "session_id": _session.get_session_id(),
- "canvas_id": canvas_id,
- "revision": int(state["revision"]),
- "edits": edits,
- "participant_fingerprints": state.get(
- "participant_fingerprints", []
- ),
- }
- var local_canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id)
- if local_canvas != null:
- local_canvas.apply_update(update)
- _broadcast_canvas_update(update)
- _emit_artwork_changed()
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- SurfaceDrawingProtocol.RELIABLE_CHANNEL,
-)
-func submit_undo_request(data: Dictionary) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(sender_id):
- _handle_undo_request(sender_id, data)
-
-
-func _handle_undo_request(peer_id: int, data: Dictionary) -> void:
- if (
- not _session.is_host()
- or not SurfaceDrawingProtocol.validate_undo_request(data)
- or str(data["session_id"]) != _session.get_session_id()
- or not _accept_request(peer_id, str(data["request_id"]))
- or not _peer_owns_art_kit(peer_id)
- ):
- return
- var stroke_id: String = str(data["stroke_id"])
- var history: Dictionary = _stroke_history_by_peer.get(peer_id, {})
- var mutations: Dictionary[String, Dictionary] = {}
- var restored_count: int = 0
- if str(history.get("stroke_id", "")) == stroke_id:
- var changes: Dictionary = history.get("changes", {})
- for mutation_key: String in changes:
- if str(_cell_last_stroke.get(mutation_key, "")) != stroke_id:
- continue
- var change: Dictionary = changes[mutation_key]
- var canvas_id: String = str(change["canvas_id"])
- var state: Dictionary = _canvas_states.get(canvas_id, {})
- if state.is_empty():
- continue
- var x: int = int(change["x"])
- var y: int = int(change["y"])
- var current: Dictionary = _state_cell(state, x, y)
- if not _same_cell_value(current, change.get("after", {})):
- continue
- var before: Dictionary = change.get("before", {})
- var restored: Dictionary = _authoritative_edit_from_value(
- x, y, before
- )
- var canvas_mutations: Dictionary = mutations.get(
- canvas_id, {}
- )
- canvas_mutations[_cell_key_for_state(state, x, y)] = restored
- mutations[canvas_id] = canvas_mutations
- var previous_stroke: String = str(
- change.get("before_stroke", "")
- )
- if previous_stroke.is_empty():
- _cell_last_stroke.erase(mutation_key)
- else:
- _cell_last_stroke[mutation_key] = previous_stroke
- restored_count += 1
- _stroke_history_by_peer.erase(peer_id)
- if not mutations.is_empty():
- _publish_cell_mutations(mutations)
- _send_undo_result(peer_id, restored_count)
-
-
-func _send_undo_result(peer_id: int, restored_count: int) -> void:
- if peer_id == _session.get_local_peer_id():
- _receive_undo_result(restored_count)
- else:
- receive_undo_result.rpc_id(peer_id, restored_count)
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- SurfaceDrawingProtocol.RELIABLE_CHANNEL,
-)
-func receive_undo_result(restored_count: int) -> void:
- _receive_undo_result(restored_count)
-
-
-func _receive_undo_result(restored_count: int) -> void:
- _emit_hud_state(
- "last stroke undone"
- if restored_count > 0
- else "last stroke changed elsewhere and could not be undone"
- )
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- SurfaceDrawingProtocol.RELIABLE_CHANNEL,
-)
-func receive_canvas_state(data: Dictionary) -> void:
- _apply_canvas_state(data)
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- SurfaceDrawingProtocol.RELIABLE_CHANNEL,
-)
-func receive_canvas_update(data: Dictionary) -> void:
- _apply_canvas_update(data)
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- SurfaceDrawingProtocol.RELIABLE_CHANNEL,
-)
-func receive_guide_update(data: Dictionary) -> void:
- _apply_guide_update(data)
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- SurfaceDrawingProtocol.RELIABLE_CHANNEL,
-)
-func receive_session_artwork_reset(session_id: String) -> void:
- if session_id == _session.get_session_id():
- _clear_session_artwork_state()
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- SurfaceDrawingProtocol.RELIABLE_CHANNEL,
-)
-func receive_artwork_fingerprint_clear(
- session_id: String,
- fingerprint: String,
-) -> void:
- if (
- session_id == _session.get_session_id()
- and NetworkIdentityCrypto.valid_fingerprint(fingerprint)
- ):
- _apply_artwork_fingerprint_clear(fingerprint)
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- SurfaceDrawingProtocol.RELIABLE_CHANNEL,
-)
-func request_canvas_snapshot(request_id: String) -> void:
- var sender_id: int = multiplayer.get_remote_sender_id()
- if (
- not _session.is_host()
- or not _session.is_authenticated_peer(sender_id)
- or request_id.is_empty()
- or request_id.length() > SurfaceDrawingProtocol.MAX_REQUEST_ID_LENGTH
- or not _accept_request(sender_id, request_id)
- or not _session.peer_supports_capability(
- sender_id, SurfaceDrawingProtocol.CAPABILITY
- )
- ):
- return
- for state: Dictionary in _canvas_states.values():
- receive_canvas_state.rpc_id(sender_id, state)
-
-
-func _apply_canvas_state(data: Dictionary) -> void:
- if (
- not SurfaceDrawingProtocol.validate_canvas_state(data)
- or str(data["session_id"]) != _session.get_session_id()
- ):
- return
- var canvas_id: String = str(data["canvas_id"])
- var previous_state: Dictionary = _canvas_states.get(canvas_id, {})
- if (
- not previous_state.is_empty()
- and int(data["revision"]) <= int(previous_state["revision"])
- ):
- return
- var normalized_state: Dictionary = data.duplicate(true)
- normalized_state["participant_fingerprints"] = _state_participants(
- normalized_state
- )
- _canvas_states[canvas_id] = normalized_state
- _rebuild_canvas_node(normalized_state)
- _refresh_stencil_visibility()
- _emit_artwork_changed()
- if (
- _active
- and str(normalized_state.get("creator_fingerprint", ""))
- == _session.get_local_identity_fingerprint()
- ):
- _emit_hud_state(
- "stamp placed • move and click to place another"
- if bool(normalized_state.get("stamp", false))
- else "shared grid placed • move and click to place another"
- )
-
-
-func _rebuild_canvas_node(data: Dictionary) -> void:
- var canvas_id: String = str(data["canvas_id"])
- var previous_canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id)
- if previous_canvas != null:
- previous_canvas.queue_free()
- var canvas := SurfaceDrawingCanvas.new()
- canvas.name = "Drawing_%s" % canvas_id
- _drawing_root.add_child(canvas)
- if not canvas.setup(data, _relationships, SOLID_SURFACE_MASK):
- canvas.queue_free()
- return
- _canvas_nodes[canvas_id] = canvas
-
-
-func _apply_canvas_update(data: Dictionary) -> void:
- if (
- not SurfaceDrawingProtocol.validate_canvas_update(data)
- or str(data["session_id"]) != _session.get_session_id()
- ):
- return
- var canvas_id: String = str(data["canvas_id"])
- var state: Dictionary = _canvas_states.get(canvas_id, {})
- var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id)
- if state.is_empty() or canvas == null or int(data["revision"]) <= int(state["revision"]):
- return
- var cells: Dictionary[int, Dictionary] = _cells_by_key(
- state["cells"], int(state["width"])
- )
- var grid_width: int = int(state["width"])
- var grid_height: int = int(state["height"])
- for edit_value: Variant in data["edits"]:
- var edit: Dictionary = edit_value
- var x: int = int(edit["x"])
- var y: int = int(edit["y"])
- if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
- return
- var key: int = y * grid_width + x
- if str(edit["color_id"]).is_empty():
- cells.erase(key)
- else:
- cells[key] = edit.duplicate(true)
- state["revision"] = int(data["revision"])
- state["cells"] = _sorted_cells(cells)
- state["participant_fingerprints"] = _merged_participants(
- state, data
- )
- _canvas_states[canvas_id] = state
- canvas.apply_update(data)
- _emit_artwork_changed()
-
-
-func _apply_guide_update(data: Dictionary) -> void:
- if (
- not SurfaceDrawingProtocol.validate_guide_update(data)
- or str(data["session_id"]) != _session.get_session_id()
- ):
- return
- var canvas_id: String = str(data["canvas_id"])
- var state: Dictionary = _canvas_states.get(canvas_id, {})
- var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id)
- if (
- state.is_empty()
- or canvas == null
- or int(data["revision"]) <= int(state["revision"])
- ):
- return
- state["revision"] = int(data["revision"])
- state["finalized"] = bool(data["finalized"])
- state["guide_visible"] = (
- bool(data["guide_visible"]) and not bool(data["finalized"])
- )
- state["participant_fingerprints"] = _merged_participants(
- state, data
- )
- _canvas_states[canvas_id] = state
- canvas.apply_guide_update(data)
- if _active:
- _emit_hud_state(
- "grid finalized • new grids may overlap its artwork"
- if bool(data["finalized"])
- else "grid guide restored"
- if bool(data["guide_visible"])
- else "grid guide hidden • pixels now meet edge-to-edge"
- )
- _emit_artwork_changed()
-
-
-func _broadcast_canvas_state(data: Dictionary) -> void:
- for peer_id: int in _session.get_authenticated_peer_ids():
- if peer_id == _session.get_local_peer_id():
- continue
- if _session.peer_supports_capability(
- peer_id, SurfaceDrawingProtocol.CAPABILITY
- ):
- receive_canvas_state.rpc_id(peer_id, data)
-
-
-func _broadcast_canvas_update(data: Dictionary) -> void:
- for peer_id: int in _session.get_authenticated_peer_ids():
- if peer_id == _session.get_local_peer_id():
- continue
- if _session.peer_supports_capability(
- peer_id, SurfaceDrawingProtocol.CAPABILITY
- ):
- receive_canvas_update.rpc_id(peer_id, data)
-
-
-func _broadcast_guide_update(data: Dictionary) -> void:
- for peer_id: int in _session.get_authenticated_peer_ids():
- if peer_id == _session.get_local_peer_id():
- continue
- if _session.peer_supports_capability(
- peer_id, SurfaceDrawingProtocol.CAPABILITY
- ):
- receive_guide_update.rpc_id(peer_id, data)
-
-
-func _validate_surface(origin: Vector3, normal: Vector3) -> Dictionary:
- if _drawing_root == null or normal.is_zero_approx():
- return {}
- var world: World3D = _drawing_root.get_world_3d()
- if world == null:
- return {}
- var direction: Vector3 = normal.normalized()
- var query := PhysicsRayQueryParameters3D.create(
- origin + direction * SURFACE_VALIDATION_DEPTH,
- origin - direction * SURFACE_VALIDATION_DEPTH,
- SOLID_SURFACE_MASK,
- )
- query.collide_with_areas = false
- query.collide_with_bodies = true
- var hit: Dictionary = world.direct_space_state.intersect_ray(query)
- if hit.is_empty() or not _is_static_surface(hit.get("collider")):
- return {}
- var hit_normal: Vector3 = _normalized_or(
- hit.get("normal", direction), direction
- )
- if absf(hit_normal.dot(direction)) < MIN_SURFACE_NORMAL_DOT:
- return {}
- return hit
-
-
-func _is_static_surface(value: Variant) -> bool:
- return value is StaticBody3D
-
-
-func _surface_tangent(normal: Vector3) -> Vector3:
- var camera: Camera3D = (
- _local_player.get_active_gameplay_camera()
- if _local_player != null
- else null
- )
- var candidate: Vector3 = (
- camera.global_basis.x if camera != null else Vector3.RIGHT
- )
- candidate = (candidate - normal * candidate.dot(normal)).normalized()
- if candidate.is_zero_approx():
- candidate = Vector3.UP.cross(normal).normalized()
- if candidate.is_zero_approx():
- candidate = Vector3.RIGHT
- return candidate
-
-
-func _state_cell_position(state: Dictionary, x: int, y: int) -> Vector3:
- var origin: Vector3 = SurfaceDrawingProtocol.array_to_vector(state["origin"])
- var normal: Vector3 = SurfaceDrawingProtocol.array_to_vector(
- state["normal"]
- ).normalized()
- var tangent: Vector3 = SurfaceDrawingProtocol.array_to_vector(
- state["tangent"]
- ).normalized()
- var bitangent: Vector3 = normal.cross(tangent).normalized()
- var size: float = float(state["cell_size"])
- return (
- origin
- + tangent * (float(x) + 0.5 - float(state["width"]) * 0.5) * size
- + bitangent * (float(y) + 0.5 - float(state["height"]) * 0.5) * size
- )
-
-
-func _overlaps_existing_canvas(
- origin: Vector3,
- normal: Vector3,
- requested_size: int,
-) -> bool:
- for state: Dictionary in _canvas_states.values():
- if bool(state.get("finalized", false)):
- continue
- var existing_origin: Vector3 = SurfaceDrawingProtocol.array_to_vector(
- state["origin"]
- )
- var existing_normal: Vector3 = SurfaceDrawingProtocol.array_to_vector(
- state["normal"]
- ).normalized()
- if absf(normal.dot(existing_normal)) < 0.85:
- continue
- var relative: Vector3 = origin - existing_origin
- if absf(relative.dot(existing_normal)) > SURFACE_VALIDATION_DEPTH:
- continue
- var tangent: Vector3 = SurfaceDrawingProtocol.array_to_vector(
- state["tangent"]
- ).normalized()
- var bitangent: Vector3 = existing_normal.cross(tangent).normalized()
- var width: float = (
- (float(state["width"]) + float(requested_size))
- * float(state["cell_size"])
- * 0.5
- )
- var height: float = (
- (float(state["height"]) + float(requested_size))
- * float(state["cell_size"])
- * 0.5
- )
- var clearance: float = float(state["cell_size"]) * 0.5
- if (
- absf(relative.dot(tangent)) < width - clearance
- and absf(relative.dot(bitangent)) < height - clearance
- ):
- return true
- return false
-
-
-func _active_canvas_count() -> int:
- var count: int = 0
- for state: Dictionary in _canvas_states.values():
- if not bool(state.get("finalized", false)):
- count += 1
- return count
-
-
-func _allocated_grid_cells() -> int:
- var count: int = 0
- for state: Dictionary in _canvas_states.values():
- count += int(state.get("width", 0)) * int(state.get("height", 0))
- return count
-
-
-func _create_brush_preview() -> void:
- if _drawing_root == null:
- return
- var quad := QuadMesh.new()
- quad.size = Vector2.ONE
- _brush_preview_material = ShaderMaterial.new()
- _brush_preview_material.shader = BrushHighlightShader
- quad.material = _brush_preview_material
- _brush_preview_multimesh = MultiMesh.new()
- _brush_preview_multimesh.transform_format = MultiMesh.TRANSFORM_3D
- _brush_preview_multimesh.mesh = quad
- _brush_preview_multimesh.instance_count = (
- SurfaceDrawingProtocol.MAX_EDITS_PER_REQUEST
- )
- _brush_preview_multimesh.visible_instance_count = 0
- _brush_preview = MultiMeshInstance3D.new()
- _brush_preview.name = "MarkerBrushPreview"
- _brush_preview.multimesh = _brush_preview_multimesh
- _brush_preview.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
- _brush_preview.hide()
- _drawing_root.add_child(_brush_preview)
-
-
-func _create_placement_preview() -> void:
- _rebuild_placement_preview()
-
-
-func _rebuild_placement_preview() -> void:
- if _drawing_root == null:
- return
- if _placement_preview != null:
- _placement_preview.queue_free()
- _placement_preview = null
- var mesh := ImmediateMesh.new()
- _placement_preview_material = StandardMaterial3D.new()
- _placement_preview_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
- _placement_preview_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
- _placement_preview_material.cull_mode = BaseMaterial3D.CULL_DISABLED
- _placement_preview_material.albedo_color = Color(0.46, 0.91, 0.95, 0.72)
- mesh.surface_begin(Mesh.PRIMITIVE_LINES, _placement_preview_material)
- var half_width: float = (
- float(_grid_size)
- * SurfaceDrawingProtocol.CELL_SIZE
- * 0.5
- )
- var half_height: float = (
- float(_grid_size)
- * SurfaceDrawingProtocol.CELL_SIZE
- * 0.5
- )
- for x: int in range(_grid_size + 1):
- var horizontal: float = (
- -half_width + float(x) * SurfaceDrawingProtocol.CELL_SIZE
- )
- mesh.surface_add_vertex(Vector3(horizontal, -half_height, 0.0))
- mesh.surface_add_vertex(Vector3(horizontal, half_height, 0.0))
- for y: int in range(_grid_size + 1):
- var vertical: float = (
- -half_height + float(y) * SurfaceDrawingProtocol.CELL_SIZE
- )
- mesh.surface_add_vertex(Vector3(-half_width, vertical, 0.0))
- mesh.surface_add_vertex(Vector3(half_width, vertical, 0.0))
- mesh.surface_end()
- _placement_preview = MeshInstance3D.new()
- _placement_preview.name = "MarkerGridPlacementPreview"
- _placement_preview.mesh = mesh
- _placement_preview.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
- _placement_preview.hide()
- _drawing_root.add_child(_placement_preview)
-
-
-func _rebuild_stamp_preview() -> void:
- if _stamp_preview != null:
- _stamp_preview.queue_free()
- _stamp_preview = null
- _stamp_preview_material = null
- if (
- _drawing_root == null
- or not _stamp_mode
- or _stamp_image == null
- or _stamp_image.is_empty()
- ):
- return
- var quad := QuadMesh.new()
- quad.size = Vector2.ONE * (
- float(_grid_size) * SurfaceDrawingProtocol.CELL_SIZE
- )
- _stamp_preview_material = StandardMaterial3D.new()
- _stamp_preview_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
- _stamp_preview_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
- _stamp_preview_material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST
- _stamp_preview_material.cull_mode = BaseMaterial3D.CULL_DISABLED
- _stamp_preview_material.albedo_color = Color(1.0, 1.0, 1.0, 0.72)
- _stamp_preview_material.albedo_texture = ImageTexture.create_from_image(
- _stamp_image
- )
- quad.material = _stamp_preview_material
- _stamp_preview = MeshInstance3D.new()
- _stamp_preview.name = "ArtworkStampPlacementPreview"
- _stamp_preview.mesh = quad
- _stamp_preview.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
- _stamp_preview.hide()
- _drawing_root.add_child(_stamp_preview)
-
-
-func _set_placement_mode(enabled: bool) -> void:
- if not _active:
- return
- _clear_armed_guide_action(true)
- if _stamp_mode:
- _clear_stamp_mode()
- if _placing_grid == enabled:
- _emit_hud_state("")
- return
- _placing_grid = enabled
- _reset_stroke()
- _selected_canvas_id = ""
- _update_aim()
- _refresh_stencil_visibility()
- _emit_hud_state(
- (
- "click place/restore • shift hide • ctrl shift finish • shift scroll zoom"
- if _placing_grid
- else "click draw • shift click erase • ctrl z undo • shift scroll zoom"
- )
- )
-
-
-func _clear_stamp_mode() -> void:
- _stamp_mode = false
- _stamp_path = ""
- _stamp_pixels = PackedByteArray()
- _stamp_image = null
- if _stamp_preview != null:
- _stamp_preview.queue_free()
- _stamp_preview = null
- _stamp_preview_material = null
-
-
-func _execute_armed_guide_action() -> void:
- var action: int = _armed_guide_action
- match action:
- GuideAction.HIDE:
- _remove_selected_guide()
- GuideAction.RESTORE:
- _restore_selected_guide()
- GuideAction.FINALIZE:
- _finalize_selected_guide()
- # Guide actions are persistent modes. Keep the selected action armed so
- # multiple grids can be handled without returning to the toolbar after
- # every click; selecting the active toggle again explicitly exits it.
- _emit_hud_state(_guide_action_status(action))
-
-
-func _cancel_armed_guide_action() -> void:
- if _armed_guide_action == GuideAction.NONE:
- return
- _clear_armed_guide_action(true)
- _emit_hud_state("")
-
-
-func _clear_armed_guide_action(restore_tool: bool) -> void:
- if _armed_guide_action == GuideAction.NONE:
- return
- _armed_guide_action = GuideAction.NONE
- if restore_tool:
- _placing_grid = _armed_return_placement_mode
- _eraser_mode = _armed_return_eraser_mode
- _armed_return_placement_mode = false
- _armed_return_eraser_mode = false
- _reset_stroke()
- _update_aim()
- _refresh_stencil_visibility()
-
-
-func _guide_action_verb(action: int) -> String:
- match action:
- GuideAction.HIDE:
- return "hide"
- GuideAction.RESTORE:
- return "show"
- GuideAction.FINALIZE:
- return "finish"
- return "use"
-
-
-func _guide_action_status(action: int) -> String:
- return "%s grid mode • click grids • toggle again to exit" % (
- _guide_action_verb(action).capitalize()
- )
-
-
-func _remove_selected_guide() -> void:
- if _selected_canvas_id.is_empty():
- _emit_hud_state("aim at a placed grid before removing its guide")
- return
- var state: Dictionary = _canvas_states.get(_selected_canvas_id, {})
- if state.is_empty():
- return
- if not bool(state.get("guide_visible", true)):
- _emit_hud_state("this grid guide is hidden • click to restore")
- return
- if request_guide_visibility(_selected_canvas_id, false):
- if not _session.is_host():
- _emit_hud_state("hiding grid guide...")
-
-
-func _restore_selected_guide() -> void:
- if _selected_canvas_id.is_empty():
- _emit_hud_state("aim at a hidden grid before restoring its guide")
- return
- var state: Dictionary = _canvas_states.get(_selected_canvas_id, {})
- if state.is_empty() or bool(state.get("finalized", false)):
- return
- if bool(state.get("guide_visible", true)):
- _emit_hud_state("this grid guide is already visible")
- return
- if request_guide_visibility(_selected_canvas_id, true):
- if not _session.is_host():
- _emit_hud_state("restoring grid guide...")
-
-
-func _finalize_selected_guide() -> void:
- if _selected_canvas_id.is_empty():
- _emit_hud_state("aim at an active grid before finishing it")
- return
- var state: Dictionary = _canvas_states.get(_selected_canvas_id, {})
- if state.is_empty() or bool(state.get("finalized", false)):
- return
- if request_guide_visibility(_selected_canvas_id, false, true):
- if not _session.is_host():
- _emit_hud_state("finishing grid • artwork remains shared...")
-
-
-func _clamped_pointer_position(value: Vector2) -> Vector2:
- var viewport_size: Vector2 = get_viewport().get_visible_rect().size
- return Vector2(
- clampf(value.x, POINTER_EDGE_MARGIN, viewport_size.x - POINTER_EDGE_MARGIN),
- clampf(value.y, POINTER_EDGE_MARGIN, viewport_size.y - POINTER_EDGE_MARGIN),
- )
-
-
-func _set_pointer_from_input(
- event_position: Vector2,
- pointer_screen_position: Vector2,
-) -> void:
- # GameUI receives input through a scaled SubViewport, so event_position is
- # expressed in that UI viewport's render coordinates. Camera3D ray methods
- # require coordinates in the camera's root viewport instead. GameUI supplies
- # that authoritative window position; direct callers may still use an event
- # position when they share this service's viewport.
- var resolved_position: Vector2 = pointer_screen_position
- if not _is_valid_pointer_screen_position(resolved_position):
- resolved_position = event_position
- _pointer_screen_position = _clamped_pointer_position(resolved_position)
-
-
-func _is_valid_pointer_screen_position(value: Vector2) -> bool:
- return value.x >= 0.0 and value.y >= 0.0
-
-
-func _cycle_color(direction: int) -> void:
- if _color_ids.is_empty():
- return
- _clear_armed_guide_action(true)
- _eraser_mode = false
- _color_index = posmod(_color_index + direction, _color_ids.size())
- _reset_stroke()
- _emit_hud_state("")
-
-
-func _set_brush_size(value: int) -> void:
- var requested: int = clampi(value, 1, 4)
- if _art_unlocks != null and not _art_unlocks.is_brush_size_unlocked(
- requested
- ):
- return
- _brush_size = requested
- _reset_stroke()
- _emit_hud_state("")
-
-
-func _current_color_id() -> StringName:
- return (
- _color_ids[_color_index]
- if not _color_ids.is_empty()
- else SurfaceDrawingPalette.DEFAULT_COLOR_ID
- )
-
-
-func _current_color() -> Color:
- return SurfaceDrawingPalette.get_color(_current_color_id())
-
-
-func _emit_hud_state(status: String) -> void:
- hud_state_changed.emit(
- _active,
- (
- "stamp"
- if _stamp_mode
- else "place grid" if _placing_grid else "marker"
- ),
- SurfaceDrawingPalette.get_display_name(_current_color_id()),
- _current_color(),
- _brush_size,
- _grid_size,
- status,
- )
-
-
-func _refresh_stencil_visibility() -> void:
- for canvas_id: String in _canvas_nodes:
- _canvas_nodes[canvas_id].set_stencil_visible(_active)
-
-
-func _begin_stroke() -> void:
- _active_stroke_id = _new_request_id("stroke")
- _last_edited_cell = Vector3i(-1, -1, -1)
-
-
-func _finish_stroke() -> void:
- _painting = false
- _erasing = false
- _active_stroke_id = ""
- _last_edited_cell = Vector3i(-1, -1, -1)
-
-
-func _reset_stroke() -> void:
- _finish_stroke()
-
-
-func _new_request_id(prefix: String) -> String:
- _request_sequence += 1
- return "%s-%d-%d" % [prefix, Time.get_ticks_msec(), _request_sequence]
-
-
-func _cells_by_key(
- values: Array,
- grid_width: int,
-) -> Dictionary[int, Dictionary]:
- var result: Dictionary[int, Dictionary] = {}
- for value: Variant in values:
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var cell: Dictionary = value
- var key: int = (
- int(cell.get("y", -1)) * grid_width
- + int(cell.get("x", -1))
- )
- result[key] = cell.duplicate(true)
- return result
-
-
-func _state_cell(state: Dictionary, x: int, y: int) -> Dictionary:
- var cells: Dictionary[int, Dictionary] = _cells_by_key(
- state.get("cells", []), int(state["width"])
- )
- return Dictionary(
- cells.get(_cell_key_for_state(state, x, y), {})
- ).duplicate(true)
-
-
-func _register_canvas_participant(
- canvas_id: String,
- fingerprint: String,
-) -> bool:
- if not NetworkIdentityCrypto.valid_fingerprint(fingerprint):
- return false
- var state: Dictionary = _canvas_states.get(canvas_id, {})
- if state.is_empty():
- return false
- var participants: Array[String] = _state_participants(state)
- if fingerprint in participants:
- return true
- if participants.size() >= SurfaceDrawingProtocol.MAX_PARTICIPANTS:
- return false
- participants.append(fingerprint)
- state["participant_fingerprints"] = participants
- _canvas_states[canvas_id] = state
- return true
-
-
-func _merged_participants(
- state: Dictionary,
- update: Dictionary,
-) -> Array[String]:
- var result: Array[String] = _state_participants(state)
- for value: Variant in update.get("participant_fingerprints", []):
- _append_participant(result, str(value))
- for edit_value: Variant in update.get("edits", []):
- var edit: Dictionary = edit_value
- _append_participant(
- result, str(edit.get("author_fingerprint", ""))
- )
- return result
-
-
-func _state_participants(state: Dictionary) -> Array[String]:
- var result: Array[String] = []
- _append_participant(
- result, str(state.get("creator_fingerprint", ""))
- )
- for value: Variant in state.get("participant_fingerprints", []):
- _append_participant(result, str(value))
- for cell_value: Variant in state.get("cells", []):
- var cell: Dictionary = cell_value
- _append_participant(
- result, str(cell.get("author_fingerprint", ""))
- )
- return result
-
-
-func _append_participant(
- participants: Array[String],
- fingerprint: String,
-) -> void:
- if (
- NetworkIdentityCrypto.valid_fingerprint(fingerprint)
- and fingerprint not in participants
- and participants.size() < SurfaceDrawingProtocol.MAX_PARTICIPANTS
- ):
- participants.append(fingerprint)
-
-
-func _cell_value_from_edit(edit: Dictionary) -> Dictionary:
- if edit.is_empty() or str(edit.get("color_id", "")).is_empty():
- return {}
- return {
- "color_id": str(edit["color_id"]),
- "author_fingerprint": str(edit.get("author_fingerprint", "")),
- }
-
-
-func _authoritative_edit_from_value(
- x: int,
- y: int,
- value: Dictionary,
-) -> Dictionary:
- return {
- "x": x,
- "y": y,
- "color_id": str(value.get("color_id", "")),
- "author_fingerprint": str(value.get("author_fingerprint", "")),
- }
-
-
-func _same_cell_value(first: Dictionary, second: Dictionary) -> bool:
- return (
- str(first.get("color_id", "")) == str(second.get("color_id", ""))
- and str(first.get("author_fingerprint", ""))
- == str(second.get("author_fingerprint", ""))
- )
-
-
-func _cell_key_for_state(state: Dictionary, x: int, y: int) -> int:
- return y * int(state["width"]) + x
-
-
-func _mutation_key(canvas_id: String, x: int, y: int) -> String:
- return "%s:%d:%d" % [canvas_id, x, y]
-
-
-func _sorted_cells(values: Dictionary[int, Dictionary]) -> Array[Dictionary]:
- var keys: Array[int] = values.keys()
- keys.sort()
- var result: Array[Dictionary] = []
- for key: int in keys:
- result.append(values[key].duplicate(true))
- return result
-
-
-func _normalized_or(value: Variant, fallback: Vector3) -> Vector3:
- var vector: Vector3 = value if typeof(value) == TYPE_VECTOR3 else fallback
- return fallback if vector.is_zero_approx() else vector.normalized()
-
-
-func _accept_request(peer_id: int, request_id: String) -> bool:
- var recent_ids: PackedStringArray = _peer_request_ids.get(
- peer_id, PackedStringArray()
- )
- if request_id in recent_ids:
- return false
- var now: int = Time.get_ticks_msec()
- var request_times: PackedInt64Array = _peer_request_times.get(
- peer_id, PackedInt64Array()
- )
- while (
- not request_times.is_empty()
- and now - request_times[0] >= REQUEST_WINDOW_MSEC
- ):
- request_times.remove_at(0)
- if request_times.size() >= MAX_REQUESTS_PER_WINDOW:
- _peer_request_times[peer_id] = request_times
- return false
- request_times.append(now)
- recent_ids.append(request_id)
- while recent_ids.size() > MAX_RECENT_REQUEST_IDS:
- recent_ids.remove_at(0)
- _peer_request_times[peer_id] = request_times
- _peer_request_ids[peer_id] = recent_ids
- return true
-
-
-func _on_peer_removed(peer_id: int) -> void:
- _peer_request_times.erase(peer_id)
- _peer_request_ids.erase(peer_id)
- _stroke_history_by_peer.erase(peer_id)
- _peer_art_entitlements.erase(peer_id)
-
-
-func _on_local_art_entitlement_changed() -> void:
- if _active and not can_activate():
- deactivate()
- _publish_local_entitlement()
-
-
-func _on_local_art_unlocks_changed(_unlock_mask: int) -> void:
- _refresh_local_unlocks()
- _rebuild_placement_preview()
- _publish_local_entitlement()
- _emit_hud_state("")
-
-
-func _on_relationship_changed(_fingerprint: String) -> void:
- for canvas: SurfaceDrawingCanvas in _canvas_nodes.values():
- canvas.refresh_relationship_visibility()
-
-
-func _on_session_state_changed(state: NetworkSession.State) -> void:
- if state == NetworkSession.State.JOINED_CLIENT:
- _publish_local_entitlement()
- request_canvas_snapshot.rpc_id(1, _new_request_id("snapshot"))
- return
- if state in [
- NetworkSession.State.PRIVATE_HOST,
- NetworkSession.State.OPEN_HOST,
- ]:
- _publish_local_entitlement()
- return
- if state not in [
- NetworkSession.State.INACTIVE,
- NetworkSession.State.DISCONNECTING,
- NetworkSession.State.CONNECTION_FAILED,
- NetworkSession.State.SERVER_LOST,
- ]:
- return
- deactivate()
- _clear_session_artwork_state()
- _canvas_sequence = 0
- _peer_request_times.clear()
- _peer_request_ids.clear()
- _peer_art_entitlements.clear()
-
-
-func _clear_session_artwork_state() -> void:
- _canvas_states.clear()
- for canvas: SurfaceDrawingCanvas in _canvas_nodes.values():
- canvas.queue_free()
- _canvas_nodes.clear()
- _canvas_sequence = 0
- _selected_canvas_id = ""
- _hovered_canvas_id = ""
- _last_hovered_canvas_id = ""
- _stroke_history_by_peer.clear()
- _cell_last_stroke.clear()
- _last_local_stroke_id = ""
- _hide_previews()
- _emit_artwork_changed()
-
-
-func _apply_artwork_fingerprint_clear(fingerprint: String) -> int:
- var affected_canvas_ids: Array[String] = []
- for canvas_id: String in _canvas_states:
- var state: Dictionary = _canvas_states[canvas_id]
- if fingerprint in _state_participants(state):
- affected_canvas_ids.append(canvas_id)
- if affected_canvas_ids.is_empty():
- return 0
- for canvas_id: String in affected_canvas_ids:
- _remove_canvas_state(canvas_id)
- _stroke_history_by_peer.clear()
- _cell_last_stroke.clear()
- _last_local_stroke_id = ""
- _hide_previews()
- _emit_artwork_changed()
- return affected_canvas_ids.size()
-
-
-func _remove_canvas_state(canvas_id: String) -> void:
- _canvas_states.erase(canvas_id)
- var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id)
- if canvas != null:
- canvas.queue_free()
- _canvas_nodes.erase(canvas_id)
- if _selected_canvas_id == canvas_id:
- _selected_canvas_id = ""
- if _hovered_canvas_id == canvas_id:
- _hovered_canvas_id = ""
- if _last_hovered_canvas_id == canvas_id:
- _last_hovered_canvas_id = ""
-
-
-func _emit_artwork_changed() -> void:
- session_artwork_changed.emit(
- get_canvas_count(), get_painted_cell_count()
- )
diff --git a/network/network_surface_drawing_service.gd.uid b/network/network_surface_drawing_service.gd.uid
deleted file mode 100644
index 2f78f04..0000000
--- a/network/network_surface_drawing_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bkiepgu8j5r6t
diff --git a/network/network_transport.gd b/network/network_transport.gd
deleted file mode 100644
index 5aed6ca..0000000
--- a/network/network_transport.gd
+++ /dev/null
@@ -1,54 +0,0 @@
-class_name NetworkTransport
-extends Node
-
-@warning_ignore("unused_signal")
-signal transport_error(message: String)
-
-var _peer: MultiplayerPeer
-var _route_description: String = ""
-
-
-func start_host(
- _port: int,
- _max_clients: int,
- _bind_address: String = "*",
-) -> Error:
- return ERR_UNAVAILABLE
-
-
-func connect_to_route(_route: ConnectionRoute) -> Error:
- return ERR_UNAVAILABLE
-
-
-func disconnect_transport() -> void:
- if _peer != null:
- # Give ENet one poll cycle to emit its graceful disconnect packets before
- # closing the socket. This keeps a host from retaining a departed player
- # until the extended generated-world liveness timeout expires.
- if (
- _peer is ENetMultiplayerPeer
- and _peer.get_connection_status()
- == MultiplayerPeer.CONNECTION_CONNECTED
- ):
- var enet_peer := _peer as ENetMultiplayerPeer
- # A host explicitly notifies each still-live client before closing.
- # Clients rely on ENetMultiplayerPeer.close(); their cached SceneTree
- # peer list can briefly outlive the underlying ENet peer after a remote
- # disconnect, making disconnect_peer() race and report an engine error.
- if enet_peer.get_unique_id() == 1:
- for peer_id: int in multiplayer.get_peers():
- if enet_peer.get_peer(peer_id) != null:
- enet_peer.disconnect_peer(peer_id, false)
- if enet_peer.host != null:
- enet_peer.host.flush()
- _peer.close()
- _peer = null
- _route_description = ""
-
-
-func get_multiplayer_peer() -> MultiplayerPeer:
- return _peer
-
-
-func get_route_description() -> String:
- return _route_description
diff --git a/network/network_transport.gd.uid b/network/network_transport.gd.uid
deleted file mode 100644
index 42076b6..0000000
--- a/network/network_transport.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://pcqgk35ifhxc
diff --git a/network/network_world_spawn_protocol.gd b/network/network_world_spawn_protocol.gd
deleted file mode 100644
index 4bd15b0..0000000
--- a/network/network_world_spawn_protocol.gd
+++ /dev/null
@@ -1,94 +0,0 @@
-class_name NetworkWorldSpawnProtocol
-extends RefCounted
-
-const CAPABILITY: StringName = &"world_spawn_envelope_v1"
-const ENVELOPE_VERSION: int = 1
-const RELIABLE_CHANNEL: int = NetworkProtocol.WORLD_SPAWN_RELIABLE_CHANNEL
-const SNAPSHOT_CHANNEL: int = 15
-const MAX_SESSION_ID_LENGTH: int = 96
-const MAX_EVENT_ID_LENGTH: int = 48
-const MAX_ENTITY_ID_LENGTH: int = 128
-const MAX_TYPE_ID_LENGTH: int = 96
-const MAX_ENTITIES_PER_SNAPSHOT: int = 128
-## Keeps unreliable movement envelopes below ENet's practical MTU while
-## allowing reliable population envelopes to carry a larger initial state.
-const SNAPSHOT_ENTITIES_PER_ENVELOPE: int = 4
-
-
-static func make_envelope(
- session_id: String,
- sequence: int,
- event_id: StringName,
- payload: Dictionary,
-) -> Dictionary:
- return {
- "envelope_version": ENVELOPE_VERSION,
- "session_id": session_id,
- "sequence": sequence,
- "event_id": str(event_id),
- "payload": payload.duplicate(true),
- }
-
-
-static func validate_envelope(value: Variant) -> bool:
- if typeof(value) != TYPE_DICTIONARY:
- return false
- var data: Dictionary = value
- return (
- typeof(data.get("envelope_version")) == TYPE_INT
- and int(data["envelope_version"]) == ENVELOPE_VERSION
- and typeof(data.get("session_id")) == TYPE_STRING
- and not str(data["session_id"]).is_empty()
- and str(data["session_id"]).length() <= MAX_SESSION_ID_LENGTH
- and typeof(data.get("sequence")) == TYPE_INT
- and int(data["sequence"]) >= 0
- and typeof(data.get("event_id")) == TYPE_STRING
- and not str(data["event_id"]).is_empty()
- and str(data["event_id"]).length() <= MAX_EVENT_ID_LENGTH
- and typeof(data.get("payload")) == TYPE_DICTIONARY
- )
-
-
-static func validate_entity_state(value: Variant) -> bool:
- if typeof(value) != TYPE_DICTIONARY:
- return false
- var data: Dictionary = value
- var position: Variant = data.get("position")
- if typeof(position) != TYPE_ARRAY or position.size() != 3:
- return false
- for component: Variant in position:
- if typeof(component) not in [TYPE_FLOAT, TYPE_INT]:
- return false
- if not is_finite(float(component)):
- return false
- return (
- typeof(data.get("entity_id")) == TYPE_STRING
- and not str(data["entity_id"]).is_empty()
- and str(data["entity_id"]).length() <= MAX_ENTITY_ID_LENGTH
- and typeof(data.get("type_id")) == TYPE_STRING
- and not str(data["type_id"]).is_empty()
- and str(data["type_id"]).length() <= MAX_TYPE_ID_LENGTH
- and typeof(data.get("yaw")) in [TYPE_FLOAT, TYPE_INT]
- and is_finite(float(data["yaw"]))
- and typeof(data.get("revision")) == TYPE_INT
- and int(data["revision"]) >= 0
- )
-
-
-static func vector3_to_array(value: Vector3) -> Array[float]:
- return [value.x, value.y, value.z]
-
-
-static func array_to_vector3(value: Variant) -> Vector3:
- if typeof(value) != TYPE_ARRAY or value.size() != 3:
- return Vector3(INF, INF, INF)
- for component: Variant in value:
- if typeof(component) not in [TYPE_FLOAT, TYPE_INT]:
- return Vector3(INF, INF, INF)
- if not is_finite(float(component)):
- return Vector3(INF, INF, INF)
- return _array_to_vector3(value)
-
-
-static func _array_to_vector3(value: Array) -> Vector3:
- return Vector3(float(value[0]), float(value[1]), float(value[2]))
diff --git a/network/network_world_spawn_protocol.gd.uid b/network/network_world_spawn_protocol.gd.uid
deleted file mode 100644
index d245393..0000000
--- a/network/network_world_spawn_protocol.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://wstcwpwk41mf
diff --git a/network/network_world_spawn_service.gd b/network/network_world_spawn_service.gd
deleted file mode 100644
index a45ad37..0000000
--- a/network/network_world_spawn_service.gd
+++ /dev/null
@@ -1,1483 +0,0 @@
-class_name NetworkWorldSpawnService
-extends Node
-
-const FishCatchType = preload("res://fish/fish_catch.gd")
-const FishDataType = preload("res://fish/fish_data.gd")
-const FishExperienceType = preload("res://fish/fish_experience.gd")
-const FishPoolType = preload("res://fish/fish_pool.gd")
-const FishQualityType = preload("res://fish/fish_quality.gd")
-const FishSelectorType = preload("res://fish/fish_selector.gd")
-const GatherableCatalogType = preload("res://gathering/gatherable_catalog.gd")
-const GatherableDataType = preload("res://gathering/gatherable_data.gd")
-const WorldGatherableType = preload("res://gathering/world_gatherable.gd")
-const TestWorldType = preload("res://world/test_world.gd")
-const CalendarSeasonType = preload("res://world/calendar_season.gd")
-
-signal local_capture_received(fish_catch: FishCatch)
-signal local_interaction_finished(accepted: bool, message: String)
-
-const SIMULATION_INTERVAL_SECONDS: float = 1.0 / 15.0
-const SNAPSHOT_INTERVAL_SECONDS: float = 0.1
-const PLAYER_SPATIAL_CELL_SIZE: float = 8.0
-const CAPACITY_RESPONSE_TIMEOUT_SECONDS: float = 3.0
-const MAX_REQUEST_ID_LENGTH: int = 128
-const MAX_LEDGER_ENTRIES: int = 96
-
-var _session: NetworkSession
-var _spawn_service: PlayerSpawnService
-var _world: TestWorldType
-var _world_root: Node3D
-var _world_time: WorldTimeService
-var _catalog: GatherableCatalogType
-var _fish_catalog: FishPoolType
-var _local_inventory: FishInventory
-var _local_collection: CollectionLog
-var _local_capacity: PlayerCoolerCapacity
-var _local_experience: PlayerExperience
-var _save_manager: PlayerSaveManager
-var _item_use: NetworkItemUseService
-
-var _entities: Dictionary = {}
-var _presentations: Dictionary = {}
-var _entity_revisions: Dictionary = {}
-var _surface_triangles: Dictionary = {}
-var _surface_areas: Dictionary = {}
-var _surface_total_areas: Dictionary = {}
-var _spawn_anchor_positions: Dictionary = {}
-var _respawns: Array[Dictionary] = []
-var _next_respawn_by_type: Dictionary[StringName, float] = {}
-var _charge_requests: Dictionary = {}
-var _pending_captures: Dictionary = {}
-var _received_results: Dictionary = {}
-var _showcase_deadlines: Dictionary = {}
-var _envelope_sequence: int = 0
-var _simulation_elapsed: float = 0.0
-var _snapshot_elapsed: float = 0.0
-var _population_session_id: String = ""
-var _dirty_entity_ids: Dictionary[String, bool] = {}
-var _moving_players_by_cell: Dictionary[Vector2i, Array] = {}
-var _presentation_enabled: bool = true
-var _spawn_events_sent: int = 0
-var _despawn_events_sent: int = 0
-var _snapshot_packets_sent: int = 0
-var _snapshot_states_sent: int = 0
-var _rng := RandomNumberGenerator.new()
-
-
-func setup(
- session: NetworkSession,
- spawn_service: PlayerSpawnService,
- world: TestWorldType,
- world_root: Node3D,
- catalog: GatherableCatalogType,
- fish_catalog: FishPoolType,
- local_inventory: FishInventory,
- local_collection: CollectionLog,
- local_capacity: PlayerCoolerCapacity,
- local_experience: PlayerExperience,
- save_manager: PlayerSaveManager,
- item_use: NetworkItemUseService,
- world_time: WorldTimeService = null,
- presentation_enabled: bool = true,
-) -> void:
- _session = session
- _spawn_service = spawn_service
- _world = world
- _world_root = world_root
- _catalog = catalog
- _fish_catalog = fish_catalog
- _local_inventory = local_inventory
- _local_collection = local_collection
- _local_capacity = local_capacity
- _local_experience = local_experience
- _save_manager = save_manager
- _item_use = item_use
- _world_time = world_time
- _presentation_enabled = presentation_enabled
- _rng.randomize()
- _session.peer_authenticated.connect(_on_peer_authenticated)
- _session.peer_removed.connect(_on_peer_removed)
- _session.state_changed.connect(_on_session_state_changed)
- if (
- _world_time != null
- and not _world_time.calendar_date_changed.is_connected(
- _on_calendar_date_changed
- )
- ):
- _world_time.calendar_date_changed.connect(_on_calendar_date_changed)
- set_physics_process(true)
- _begin_population_if_ready.call_deferred()
-
-
-func begin_local_interaction() -> String:
- if (
- _session == null
- or not _session.is_gameplay_session_active()
- or (
- not _session.is_host()
- and not _session.supports_server_capability(
- NetworkWorldSpawnProtocol.CAPABILITY
- )
- )
- ):
- local_interaction_finished.emit(false, "Catching is unavailable.")
- return ""
- var request_id: String = _new_id("gather")
- var data: Dictionary = {
- "session_id": _session.get_session_id(),
- "request_id": request_id,
- }
- if _session.is_host():
- _handle_interaction_begin(_session.get_local_peer_id(), data)
- else:
- submit_interaction_begin.rpc_id(1, data)
- return request_id
-
-
-func finish_local_interaction(
- request_id: String,
- entity_id: String,
- target_position: Vector3,
-) -> void:
- if request_id.is_empty() or not target_position.is_finite():
- return
- var data: Dictionary = {
- "session_id": _session.get_session_id(),
- "request_id": request_id,
- "entity_id": entity_id,
- "target_position": NetworkWorldSpawnProtocol.vector3_to_array(
- target_position
- ),
- "movement_sequence": _session.get_latest_movement_input_sequence(),
- }
- if _session.is_host():
- _handle_interaction_finish(_session.get_local_peer_id(), data)
- else:
- submit_interaction_finish.rpc_id(1, data)
-
-
-func cancel_local_interaction(request_id: String) -> void:
- if request_id.is_empty() or _session == null:
- return
- if _session.is_host():
- _handle_interaction_cancel(_session.get_local_peer_id(), request_id)
- else:
- submit_interaction_cancel.rpc_id(1, request_id)
-
-
-func find_entity_near(
- position: Vector3,
- radius: float,
-) -> String:
- var best_id: String = ""
- var best_distance_squared: float = radius * radius
- for entity_id: String in _entities:
- var state: Dictionary = _entities[entity_id]
- if bool(state.get("locked", false)):
- continue
- var entity_position: Vector3 = state.get("position", Vector3.ZERO)
- var distance_squared: float = entity_position.distance_squared_to(
- position
- )
- if distance_squared <= best_distance_squared:
- best_distance_squared = distance_squared
- best_id = entity_id
- return best_id
-
-
-func find_capture_target(
- position: Vector3,
- tool_id: StringName,
-) -> String:
- if tool_id.is_empty():
- return ""
- var best_id: String = ""
- var best_distance_squared: float = INF
- for entity_id: String in _entities:
- var state: Dictionary = _entities[entity_id]
- if bool(state.get("locked", false)):
- continue
- var entry := state.get("data") as GatherableDataType
- if entry == null or entry.required_tool_id != tool_id:
- continue
- var entity_position: Vector3 = state.get("position", Vector3.ZERO)
- var distance_squared: float = entity_position.distance_squared_to(
- position
- )
- if (
- distance_squared <= entry.capture_radius * entry.capture_radius
- and distance_squared <= best_distance_squared
- ):
- best_distance_squared = distance_squared
- best_id = entity_id
- return best_id
-
-
-func get_charge_duration_for_tool(tool_id: StringName) -> float:
- var duration: float = 0.0
- if _catalog == null or tool_id.is_empty():
- return duration
- for entry: GatherableDataType in _catalog.get_available_entries(
- _current_season()
- ):
- if entry.required_tool_id == tool_id:
- duration = maxf(duration, entry.charge_duration)
- return duration
-
-
-func get_entry_for_entity(entity_id: String) -> GatherableDataType:
- var state: Dictionary = _entities.get(entity_id, {})
- return state.get("data") as GatherableDataType
-
-
-func refresh_world_context() -> void:
- _clear_world()
- _population_session_id = ""
- if _session != null and _session.is_gameplay_session_active():
- _begin_population_if_ready.call_deferred()
-
-
-func _physics_process(delta: float) -> void:
- if (
- _session == null
- or not _session.is_host()
- or not _session.is_gameplay_session_active()
- ):
- return
- _begin_population_if_ready()
- # There is no gameplay state to advance while a dedicated server has no
- # spawned players. Pausing the roaming/respawn loop also prevents idle
- # gatherables from producing a continuous stream of snapshots to nobody.
- if _spawn_service == null or _spawn_service.get_peer_ids().is_empty():
- _simulation_elapsed = 0.0
- _snapshot_elapsed = 0.0
- return
- _simulation_elapsed += delta
- _snapshot_elapsed += delta
- if _simulation_elapsed < SIMULATION_INTERVAL_SECONDS:
- return
- var simulation_delta: float = minf(_simulation_elapsed, 0.2)
- _simulation_elapsed = fmod(
- _simulation_elapsed,
- SIMULATION_INTERVAL_SECONDS,
- )
- _update_pending_capture_timeouts()
- _update_respawns()
- _update_showcase_deadlines()
- _rebuild_moving_player_spatial_index()
- _update_host_entities(simulation_delta)
- if _snapshot_elapsed >= SNAPSHOT_INTERVAL_SECONDS:
- _snapshot_elapsed = fmod(
- _snapshot_elapsed,
- SNAPSHOT_INTERVAL_SECONDS,
- )
- _broadcast_entity_snapshots()
-
-
-func _begin_population_if_ready() -> void:
- if (
- _session == null
- or not _session.is_host()
- or not _session.is_gameplay_session_active()
- or _catalog == null
- or _world == null
- or _world_root == null
- or not _world.is_world_ready()
- ):
- return
- var session_id: String = _session.get_session_id()
- if session_id.is_empty() or session_id == _population_session_id:
- return
- _clear_world()
- _population_session_id = session_id
- for entry: GatherableDataType in _catalog.get_available_entries(
- _current_season()
- ):
- _cache_spawn_surface(entry)
- for _spawn_index: int in _target_population(entry):
- _spawn_entity(entry)
-
-
-func _cache_spawn_surface(entry: GatherableDataType) -> void:
- if (
- entry == null
- or _surface_triangles.has(entry.type_id)
- or _spawn_anchor_positions.has(entry.type_id)
- ):
- return
- if not entry.spawn_anchor_set_id.is_empty():
- var anchor_positions: PackedVector3Array = (
- _world.get_gatherable_spawn_positions(entry.spawn_anchor_set_id)
- )
- _spawn_anchor_positions[entry.type_id] = anchor_positions
- if (
- anchor_positions.is_empty()
- and _world.get_world_layout() == WorldLayout.GENERATED
- ):
- push_warning(
- "No gatherable spawn anchors were found for %s." % entry.type_id
- )
- return
- var triangles: Array[PackedVector3Array]
- if not entry.diggable_area_id.is_empty():
- triangles = _world.get_diggable_area_triangles(entry.diggable_area_id)
- else:
- triangles = _world.get_spawn_surface_triangles(
- entry.surface_materials,
- entry.minimum_surface_y,
- )
- var areas := PackedFloat32Array()
- var total_area: float = 0.0
- for triangle: PackedVector3Array in triangles:
- var area: float = (
- (triangle[1] - triangle[0]).cross(
- triangle[2] - triangle[0]
- ).length() * 0.5
- )
- total_area += area
- areas.append(total_area)
- _surface_triangles[entry.type_id] = triangles
- _surface_areas[entry.type_id] = areas
- _surface_total_areas[entry.type_id] = total_area
- if (
- triangles.is_empty()
- and _world.get_world_layout() == WorldLayout.GENERATED
- ):
- push_warning(
- "No valid spawn surface was found for %s." % entry.type_id
- )
-
-
-func _spawn_entity(entry: GatherableDataType) -> void:
- var position: Vector3 = _sample_surface_position(entry)
- if not position.is_finite():
- return
- var target: Vector3 = (
- position
- if entry.is_stationary_spawn()
- else _sample_surface_position(entry, position, entry.roam_radius)
- )
- var quality: int = FishQualityType.roll(_rng)
- var entity_id: String = _new_id("world")
- var state: Dictionary = {
- "entity_id": entity_id,
- "type_id": entry.type_id,
- "data": entry,
- "position": position,
- "target": target,
- "yaw": _rng.randf_range(-PI, PI),
- "quality": quality,
- "revision": 1,
- "locked": false,
- "expires_at": (
- _now() + entry.active_lifetime_seconds
- if entry.active_lifetime_seconds > 0.0
- else INF
- ),
- }
- if not (state["target"] as Vector3).is_finite():
- state["target"] = position
- _entities[entity_id] = state
- var envelope: Dictionary = _make_envelope(
- &"spawn",
- {"entity": _state_to_network(state)},
- )
- _apply_envelope(envelope)
- receive_world_envelope.rpc(envelope)
- _spawn_events_sent += 1
-
-
-func _update_host_entities(delta: float) -> void:
- for entity_id: String in _entities.keys():
- var state: Dictionary = _entities.get(entity_id, {})
- if state.is_empty() or bool(state.get("locked", false)):
- continue
- var entry := state.get("data") as GatherableDataType
- if entry == null:
- continue
- if _now() >= float(state.get("expires_at", INF)):
- _despawn_entity(entity_id, &"expired", false, true)
- continue
- if entry.is_stationary_spawn():
- continue
- var quality: int = _get_state_quality(state)
- var position: Vector3 = state["position"]
- var target: Vector3 = state["target"]
- var horizontal_delta := Vector3(
- target.x - position.x,
- 0.0,
- target.z - position.z,
- )
- if horizontal_delta.length_squared() <= 0.04:
- target = _sample_surface_position(
- entry,
- position,
- entry.roam_radius,
- )
- if not target.is_finite():
- target = position
- state["target"] = target
- horizontal_delta = Vector3(
- target.x - position.x,
- 0.0,
- target.z - position.z,
- )
- if not horizontal_delta.is_zero_approx():
- var step: float = minf(
- entry.get_movement_speed_for_quality(quality) * delta,
- horizontal_delta.length(),
- )
- var direction: Vector3 = horizontal_delta.normalized()
- position.x += direction.x * step
- position.z += direction.z * step
- position.y = lerpf(
- position.y,
- target.y,
- minf(step / maxf(horizontal_delta.length(), 0.001), 1.0),
- )
- state["yaw"] = atan2(-direction.x, -direction.z)
- state["revision"] = int(state["revision"]) + 1
- _dirty_entity_ids[entity_id] = true
- state["position"] = position
- _entities[entity_id] = state
- if entry.can_be_scared() and _should_scare(entry, position, quality):
- _despawn_entity(entity_id, &"scared", true, true)
-
-
-func _should_scare(
- entry: GatherableDataType,
- position: Vector3,
- quality: int,
-) -> bool:
- var scare_radius: float = entry.get_scare_radius_for_quality(quality)
- var radius_squared: float = scare_radius * scare_radius
- var center_cell := _player_spatial_cell(position)
- var cell_radius: int = ceili(scare_radius / PLAYER_SPATIAL_CELL_SIZE)
- for cell_x: int in range(
- center_cell.x - cell_radius,
- center_cell.x + cell_radius + 1,
- ):
- for cell_y: int in range(
- center_cell.y - cell_radius,
- center_cell.y + cell_radius + 1,
- ):
- for value: Variant in _moving_players_by_cell.get(
- Vector2i(cell_x, cell_y),
- [],
- ):
- var avatar := value as Player
- if avatar == null:
- continue
- var player_delta := Vector2(
- avatar.global_position.x - position.x,
- avatar.global_position.z - position.z,
- )
- if player_delta.length_squared() <= radius_squared:
- return true
- return false
-
-
-func _rebuild_moving_player_spatial_index() -> void:
- _moving_players_by_cell.clear()
- for peer_id: int in _spawn_service.get_peer_ids():
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- if (
- avatar == null
- or not avatar.is_moving_horizontally()
- or avatar.is_sneaking()
- ):
- continue
- var cell: Vector2i = _player_spatial_cell(avatar.global_position)
- var players: Array = _moving_players_by_cell.get(cell, [])
- players.append(avatar)
- _moving_players_by_cell[cell] = players
-
-
-static func _player_spatial_cell(position: Vector3) -> Vector2i:
- return Vector2i(
- floori(position.x / PLAYER_SPATIAL_CELL_SIZE),
- floori(position.z / PLAYER_SPATIAL_CELL_SIZE),
- )
-
-
-func _sample_surface_position(
- entry: GatherableDataType,
- origin: Vector3 = Vector3(INF, INF, INF),
- maximum_distance: float = INF,
-) -> Vector3:
- _cache_spawn_surface(entry)
- if not entry.spawn_anchor_set_id.is_empty():
- return _sample_anchor_position(entry, origin, maximum_distance)
- var triangles: Array = _surface_triangles.get(entry.type_id, [])
- var cumulative_areas: PackedFloat32Array = _surface_areas.get(
- entry.type_id,
- PackedFloat32Array(),
- )
- var total_area: float = float(
- _surface_total_areas.get(entry.type_id, 0.0)
- )
- if triangles.is_empty() or total_area <= 0.0:
- return Vector3(INF, INF, INF)
- var validates_diggable_surface := not entry.diggable_area_id.is_empty()
- var attempts: int = (
- 48
- if validates_diggable_surface or is_finite(maximum_distance)
- else 1
- )
- var fallback: Vector3 = Vector3(INF, INF, INF)
- for _attempt: int in attempts:
- var roll: float = _rng.randf() * total_area
- var triangle_index: int = cumulative_areas.bsearch(roll)
- triangle_index = clampi(triangle_index, 0, triangles.size() - 1)
- var triangle: PackedVector3Array = triangles[triangle_index]
- var root: float = sqrt(_rng.randf())
- var barycentric_b: float = root * (1.0 - _rng.randf())
- var barycentric_c: float = root - barycentric_b
- var point: Vector3 = (
- triangle[0] * (1.0 - root)
- + triangle[1] * barycentric_b
- + triangle[2] * barycentric_c
- )
- fallback = point
- var within_requested_distance := (
- not is_finite(maximum_distance)
- or Vector2(point.x - origin.x, point.z - origin.z).length()
- <= maximum_distance
- )
- var valid_diggable_surface := (
- not validates_diggable_surface
- or _world.is_diggable_surface_point_valid(
- entry.diggable_area_id,
- point,
- )
- )
- if within_requested_distance and valid_diggable_surface:
- return point
- if validates_diggable_surface:
- return Vector3(INF, INF, INF)
- if origin.is_finite() and is_finite(maximum_distance):
- return origin
- return fallback
-
-
-func _sample_anchor_position(
- entry: GatherableDataType,
- origin: Vector3,
- maximum_distance: float,
-) -> Vector3:
- var anchors: PackedVector3Array = _spawn_anchor_positions.get(
- entry.type_id,
- PackedVector3Array(),
- )
- var candidates := PackedVector3Array()
- for anchor: Vector3 in anchors:
- if not anchor.is_finite() or _anchor_is_occupied(entry.type_id, anchor):
- continue
- if (
- origin.is_finite()
- and is_finite(maximum_distance)
- and anchor.distance_to(origin) > maximum_distance
- ):
- continue
- candidates.append(anchor)
- if candidates.is_empty():
- return Vector3(INF, INF, INF)
- return candidates[_rng.randi_range(0, candidates.size() - 1)]
-
-
-func _anchor_is_occupied(type_id: StringName, anchor: Vector3) -> bool:
- for state: Dictionary in _entities.values():
- if StringName(state.get("type_id", StringName())) != type_id:
- continue
- var position: Variant = state.get("position")
- if (
- typeof(position) == TYPE_VECTOR3
- and (position as Vector3).distance_squared_to(anchor) <= 0.0001
- ):
- return true
- return false
-
-
-func _broadcast_entity_snapshots() -> void:
- if _dirty_entity_ids.is_empty() or not _can_broadcast_snapshots():
- return
- var has_remote_recipients: bool = _has_authenticated_remote_peers()
- var snapshots: Array[Dictionary] = []
- for entity_id: String in _dirty_entity_ids:
- var state: Dictionary = _entities.get(entity_id, {})
- if not state.is_empty():
- snapshots.append(_state_to_network(state))
- _dirty_entity_ids.clear()
- var maximum_chunk_size: int = (
- NetworkWorldSpawnProtocol.SNAPSHOT_ENTITIES_PER_ENVELOPE
- )
- for start_index: int in range(
- 0,
- snapshots.size(),
- maximum_chunk_size,
- ):
- var chunk: Array[Dictionary] = []
- for value: Dictionary in snapshots.slice(
- start_index,
- mini(start_index + maximum_chunk_size, snapshots.size()),
- ):
- chunk.append(value)
- var envelope: Dictionary = _make_envelope(
- &"snapshot",
- {"entities": chunk},
- )
- _apply_envelope(envelope)
- if not has_remote_recipients:
- continue
- # A peer-disconnected callback can resume validation/gameplay code and
- # close the host while this physics update is still unwinding. Never send
- # through the replacement OfflineMultiplayerPeer in that narrow window.
- if not _can_broadcast_snapshots():
- return
- receive_world_snapshot_envelope.rpc(envelope)
- _snapshot_packets_sent += 1
- _snapshot_states_sent += chunk.size()
-
-
-func _has_authenticated_remote_peers() -> bool:
- if _session == null:
- return false
- var local_peer_id: int = _session.get_local_peer_id()
- for peer_id: int in _session.get_authenticated_peer_ids():
- if peer_id != local_peer_id:
- return true
- return false
-
-
-func _can_broadcast_snapshots() -> bool:
- if (
- _session == null
- or not _session.is_host()
- or not _session.is_gameplay_session_active()
- ):
- return false
- var peer: MultiplayerPeer = multiplayer.multiplayer_peer
- return (
- peer != null
- and peer.get_connection_status()
- == MultiplayerPeer.CONNECTION_CONNECTED
- )
-
-
-func _despawn_entity(
- entity_id: String,
- reason: StringName,
- with_dust: bool,
- schedule_respawn: bool,
-) -> void:
- var state: Dictionary = _entities.get(entity_id, {})
- if state.is_empty():
- return
- var entry := state.get("data") as GatherableDataType
- _entities.erase(entity_id)
- _dirty_entity_ids.erase(entity_id)
- var envelope: Dictionary = _make_envelope(
- &"despawn",
- {
- "entity_id": entity_id,
- "reason": str(reason),
- "with_dust": with_dust,
- },
- )
- _apply_envelope(envelope)
- receive_world_envelope.rpc(envelope)
- _despawn_events_sent += 1
- if schedule_respawn and entry != null:
- _respawns.append({
- "type_id": entry.type_id,
- "due": _now() + entry.get_respawn_delay(reason, _rng),
- })
-
-
-func _update_respawns() -> void:
- var now: float = _now()
- var remaining: Array[Dictionary] = []
- for respawn: Dictionary in _respawns:
- var type_id := StringName(str(respawn.get("type_id", "")))
- var next_allowed: float = float(
- _next_respawn_by_type.get(type_id, 0.0)
- )
- if now < maxf(float(respawn.get("due", INF)), next_allowed):
- remaining.append(respawn)
- continue
- var entry: GatherableDataType = _catalog.get_entry(
- type_id
- )
- if entry != null and entry.is_available(_current_season()):
- _spawn_entity(entry)
- _next_respawn_by_type[type_id] = (
- now + entry.minimum_respawn_spacing_seconds
- )
- _respawns = remaining
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- NetworkWorldSpawnProtocol.RELIABLE_CHANNEL,
-)
-func submit_interaction_begin(data: Dictionary) -> void:
- var peer_id: int = multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(peer_id):
- _handle_interaction_begin(peer_id, data)
-
-
-func _handle_interaction_begin(peer_id: int, data: Dictionary) -> void:
- var request_id: String = str(data.get("request_id", ""))
- if (
- str(data.get("session_id", "")) != _session.get_session_id()
- or request_id.is_empty()
- or request_id.length() > MAX_REQUEST_ID_LENGTH
- or _pending_captures.has(peer_id)
- ):
- _send_interaction_result(
- peer_id, request_id, false, "Cannot use that gathering tool now."
- )
- return
- _charge_requests[peer_id] = {
- "request_id": request_id,
- "started": _now(),
- }
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- NetworkWorldSpawnProtocol.RELIABLE_CHANNEL,
-)
-func submit_interaction_finish(data: Dictionary) -> void:
- var peer_id: int = multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(peer_id):
- _handle_interaction_finish(peer_id, data)
-
-
-func _handle_interaction_finish(peer_id: int, data: Dictionary) -> void:
- var request_id: String = str(data.get("request_id", ""))
- var entity_id: String = str(data.get("entity_id", ""))
- var charge: Dictionary = _charge_requests.get(peer_id, {})
- _charge_requests.erase(peer_id)
- var target_position: Vector3 = NetworkWorldSpawnProtocol.array_to_vector3(
- data.get("target_position", [])
- )
- var state: Dictionary = _entities.get(entity_id, {})
- var entry := state.get("data") as GatherableDataType
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- var movement_sequence_value: Variant = data.get("movement_sequence", null)
- var error: String = ""
- if (
- str(data.get("session_id", "")) != _session.get_session_id()
- or request_id.is_empty()
- or request_id.length() > MAX_REQUEST_ID_LENGTH
- or str(charge.get("request_id", "")) != request_id
- or not target_position.is_finite()
- or typeof(movement_sequence_value) != TYPE_INT
- or int(movement_sequence_value) < 0
- or int(movement_sequence_value) > 2147483647
- ):
- error = "The catch attempt was invalid."
- elif state.is_empty() or entry == null or bool(state.get("locked", false)):
- error = "That gathering spot is no longer there."
- elif _now() - float(charge.get("started", _now())) + 0.05 < entry.charge_duration:
- error = "Finish readying the tool first."
- elif _item_use.get_equipped_item_id(peer_id) != entry.required_tool_id:
- error = "Equip the correct gathering tool."
- elif avatar == null:
- error = "The player is unavailable."
- else:
- var movement_state: Dictionary = (
- avatar.get_lag_compensated_movement_state(
- int(movement_sequence_value)
- )
- )
- if entry.requires_sneaking and not bool(
- movement_state.get("sneaking", avatar.is_sneaking())
- ):
- error = "Sneak closer before using the tool."
- if not error.is_empty():
- _send_interaction_result(peer_id, request_id, false, error)
- return
- var entity_position: Vector3 = state["position"]
- var target_distance: float = entity_position.distance_to(target_position)
- var compensated_position: Vector3 = movement_state.get(
- "position",
- avatar.global_position,
- )
- var player_distance: float = Vector2(
- compensated_position.x - entity_position.x,
- compensated_position.z - entity_position.z,
- ).length()
- if target_distance > entry.capture_radius:
- error = "The gathering tool missed."
- elif player_distance > entry.interaction_range:
- error = "Move closer before using the tool."
- if not error.is_empty():
- _send_interaction_result(peer_id, request_id, false, error)
- return
- var fish_catch: FishCatch = _create_catch_for_state(entry, state)
- if fish_catch == null or not fish_catch.is_valid():
- _send_interaction_result(peer_id, request_id, false, "The catch could not be recorded.")
- return
- state["locked"] = true
- _entities[entity_id] = state
- var capacity_nonce: String = _new_id("capacity")
- _pending_captures[peer_id] = {
- "request_id": request_id,
- "entity_id": entity_id,
- "catch": fish_catch.to_network_dict(),
- "result_id": _new_id("gather_result"),
- "capacity_nonce": capacity_nonce,
- "deadline": _now() + CAPACITY_RESPONSE_TIMEOUT_SECONDS,
- }
- var probe: Dictionary = {
- "session_id": _session.get_session_id(),
- "request_id": request_id,
- "capacity_nonce": capacity_nonce,
- "catch_id": str(fish_catch.catch_id),
- }
- if peer_id == _session.get_local_peer_id():
- _handle_local_capacity_probe(probe)
- else:
- receive_capacity_probe.rpc_id(peer_id, probe)
-
-
-func _create_catch_for_state(
- entry: GatherableDataType,
- state: Dictionary,
-) -> FishCatch:
- var selector := FishSelectorType.new()
- selector.begin_roll()
- var fish_catch: FishCatch = selector.create_catch(entry.catch_data)
- if fish_catch == null:
- return null
- var quality: int = _get_state_quality(state)
- fish_catch.quality = quality
- fish_catch.sale_value = FishQualityType.apply_sale_value(
- entry.catch_data.get_sale_value_for_weight(fish_catch.weight_lb),
- quality,
- )
- return fish_catch
-
-
-func _get_state_quality(state: Dictionary) -> int:
- var quality: int = int(
- state.get("quality", FishQualityType.Tier.BORING)
- )
- return (
- quality
- if FishQualityType.is_valid(quality)
- else FishQualityType.Tier.BORING
- )
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- NetworkWorldSpawnProtocol.RELIABLE_CHANNEL,
-)
-func submit_interaction_cancel(request_id: String) -> void:
- var peer_id: int = multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(peer_id):
- _handle_interaction_cancel(peer_id, request_id)
-
-
-func _handle_interaction_cancel(peer_id: int, request_id: String) -> void:
- var charge: Dictionary = _charge_requests.get(peer_id, {})
- if str(charge.get("request_id", "")) == request_id:
- _charge_requests.erase(peer_id)
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- NetworkWorldSpawnProtocol.RELIABLE_CHANNEL,
-)
-func receive_capacity_probe(data: Dictionary) -> void:
- _handle_local_capacity_probe(data)
-
-
-func _handle_local_capacity_probe(data: Dictionary) -> void:
- if str(data.get("session_id", "")) != _session.get_session_id():
- return
- var catch_id := StringName(str(data.get("catch_id", "")))
- var can_accept: bool = (
- _local_inventory != null
- and _local_inventory.can_accept_catch(catch_id)
- )
- if _session.is_host():
- _handle_capacity_response(
- _session.get_local_peer_id(),
- str(data.get("request_id", "")),
- str(data.get("capacity_nonce", "")),
- can_accept,
- )
- else:
- submit_capacity_response.rpc_id(
- 1,
- str(data.get("request_id", "")),
- str(data.get("capacity_nonce", "")),
- can_accept,
- )
-
-
-@rpc(
- "any_peer",
- "call_remote",
- "reliable",
- NetworkWorldSpawnProtocol.RELIABLE_CHANNEL,
-)
-func submit_capacity_response(
- request_id: String,
- capacity_nonce: String,
- can_accept: bool,
-) -> void:
- var peer_id: int = multiplayer.get_remote_sender_id()
- if _session.is_host() and _session.is_authenticated_peer(peer_id):
- _handle_capacity_response(
- peer_id,
- request_id,
- capacity_nonce,
- can_accept,
- )
-
-
-func _handle_capacity_response(
- peer_id: int,
- request_id: String,
- capacity_nonce: String,
- can_accept: bool,
-) -> void:
- var pending: Dictionary = _pending_captures.get(peer_id, {})
- if (
- pending.is_empty()
- or str(pending.get("request_id", "")) != request_id
- or str(pending.get("capacity_nonce", "")) != capacity_nonce
- ):
- return
- if not can_accept:
- _reject_pending_capture(peer_id, "Inventory is full.")
- return
- _pending_captures.erase(peer_id)
- var entity_id: String = str(pending["entity_id"])
- _despawn_entity(entity_id, &"captured", false, true)
- var outcome: Dictionary = {
- "session_id": _session.get_session_id(),
- "result_id": str(pending["result_id"]),
- "request_id": request_id,
- "target_peer_id": peer_id,
- "catch": (pending["catch"] as Dictionary).duplicate(true),
- }
- if peer_id == _session.get_local_peer_id():
- _apply_capture_result(outcome)
- else:
- receive_capture_result.rpc_id(peer_id, outcome)
- _broadcast_showcase(peer_id, outcome["catch"], true)
- _showcase_deadlines[peer_id] = _now() + 4.5
-
-
-func _reject_pending_capture(peer_id: int, message: String) -> void:
- var pending: Dictionary = _pending_captures.get(peer_id, {})
- _pending_captures.erase(peer_id)
- if pending.is_empty():
- return
- var entity_id: String = str(pending.get("entity_id", ""))
- var state: Dictionary = _entities.get(entity_id, {})
- if not state.is_empty():
- state["locked"] = false
- _entities[entity_id] = state
- _send_interaction_result(
- peer_id,
- str(pending.get("request_id", "")),
- false,
- message,
- )
-
-
-func _update_pending_capture_timeouts() -> void:
- var now: float = _now()
- for peer_id: int in _pending_captures.keys():
- var pending: Dictionary = _pending_captures[peer_id]
- if now >= float(pending.get("deadline", INF)):
- _reject_pending_capture(peer_id, "The catch attempt timed out.")
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- NetworkWorldSpawnProtocol.RELIABLE_CHANNEL,
-)
-func receive_capture_result(data: Dictionary) -> void:
- _apply_capture_result(data)
-
-
-func _apply_capture_result(data: Dictionary) -> void:
- if (
- str(data.get("session_id", "")) != _session.get_session_id()
- or int(data.get("target_peer_id", 0)) != _session.get_local_peer_id()
- or typeof(data.get("catch")) != TYPE_DICTIONARY
- ):
- return
- var result_id: String = str(data.get("result_id", ""))
- var request_id: String = str(data.get("request_id", ""))
- if result_id.is_empty() or request_id.is_empty():
- return
- if _received_results.has(result_id):
- local_interaction_finished.emit(true, "Caught.")
- return
- var catch_data: Dictionary = data["catch"]
- var fish: FishDataType = _fish_catalog.get_fish_by_id(
- StringName(str(catch_data.get("fish_id", "")))
- )
- var fish_catch: FishCatch = FishCatchType.from_network_dict(
- catch_data,
- fish,
- )
- if fish_catch == null or not fish_catch.is_valid():
- return
- var already_owned: bool = _local_inventory.contains_catch_id(
- fish_catch.catch_id
- )
- if (
- not already_owned
- and not _local_inventory.can_accept_catch(fish_catch.catch_id)
- ):
- return
- if not already_owned:
- var experience: int = FishExperienceType.calculate_for_collection(
- fish_catch,
- _local_collection,
- )
- _local_inventory.add_catch(fish_catch)
- _local_collection.record_catch(
- fish_catch.fish_id,
- fish_catch.quality,
- )
- _local_experience.award_experience(experience)
- if not _save_manager.save_if_dirty():
- return
- _received_results[result_id] = true
- _bound(_received_results)
- local_interaction_finished.emit(true, "Caught %s." % fish.display_name)
- local_capture_received.emit(fish_catch)
-
-
-func _send_interaction_result(
- peer_id: int,
- request_id: String,
- accepted: bool,
- message: String,
-) -> void:
- var result: Dictionary = {
- "session_id": _session.get_session_id(),
- "request_id": request_id if not request_id.is_empty() else "invalid",
- "target_peer_id": peer_id,
- "accepted": accepted,
- "message": message.left(160),
- }
- if peer_id == _session.get_local_peer_id():
- _apply_interaction_result(result)
- else:
- receive_interaction_result.rpc_id(peer_id, result)
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- NetworkWorldSpawnProtocol.RELIABLE_CHANNEL,
-)
-func receive_interaction_result(data: Dictionary) -> void:
- _apply_interaction_result(data)
-
-
-func _apply_interaction_result(data: Dictionary) -> void:
- if (
- str(data.get("session_id", "")) != _session.get_session_id()
- or int(data.get("target_peer_id", 0)) != _session.get_local_peer_id()
- or typeof(data.get("accepted")) != TYPE_BOOL
- ):
- return
- local_interaction_finished.emit(
- bool(data["accepted"]),
- str(data.get("message", "")),
- )
-
-
-func _broadcast_showcase(
- peer_id: int,
- catch_data: Dictionary,
- visible: bool,
-) -> void:
- var envelope: Dictionary = _make_envelope(
- &"showcase",
- {
- "owner_peer_id": peer_id,
- "visible": visible,
- "catch": catch_data.duplicate(true) if visible else {},
- },
- )
- _apply_envelope(envelope)
- receive_world_envelope.rpc(envelope)
-
-
-func _update_showcase_deadlines() -> void:
- var now: float = _now()
- for peer_id: int in _showcase_deadlines.keys():
- if now < float(_showcase_deadlines[peer_id]):
- continue
- _showcase_deadlines.erase(peer_id)
- _broadcast_showcase(peer_id, {}, false)
-
-
-func _make_envelope(
- event_id: StringName,
- payload: Dictionary,
-) -> Dictionary:
- _envelope_sequence += 1
- return NetworkWorldSpawnProtocol.make_envelope(
- _session.get_session_id(),
- _envelope_sequence,
- event_id,
- payload,
- )
-
-
-@rpc(
- "authority",
- "call_remote",
- "reliable",
- NetworkWorldSpawnProtocol.RELIABLE_CHANNEL,
-)
-func receive_world_envelope(envelope: Dictionary) -> void:
- _apply_envelope(envelope)
-
-
-@rpc(
- "authority",
- "call_remote",
- "unreliable_ordered",
- NetworkWorldSpawnProtocol.SNAPSHOT_CHANNEL,
-)
-func receive_world_snapshot_envelope(envelope: Dictionary) -> void:
- _apply_envelope(envelope)
-
-
-func _apply_envelope(envelope: Dictionary) -> void:
- if (
- not NetworkWorldSpawnProtocol.validate_envelope(envelope)
- or str(envelope["session_id"]) != _session.get_session_id()
- ):
- return
- var event_id := StringName(str(envelope["event_id"]))
- var payload: Dictionary = envelope["payload"]
- match event_id:
- &"spawn":
- _apply_entity_state(payload.get("entity", {}), true)
- &"snapshot":
- _apply_entity_states(payload.get("entities", []), false, false)
- &"population":
- _apply_entity_states(payload.get("entities", []), true, true)
- &"despawn":
- _apply_despawn(payload)
- &"showcase":
- _apply_showcase(payload)
-
-
-func _apply_entity_states(
- values: Variant,
- immediate: bool,
- reconcile: bool,
-) -> void:
- if (
- typeof(values) != TYPE_ARRAY
- or values.size() > NetworkWorldSpawnProtocol.MAX_ENTITIES_PER_SNAPSHOT
- ):
- return
- var included: Dictionary = {}
- for value: Variant in values:
- if not NetworkWorldSpawnProtocol.validate_entity_state(value):
- continue
- var state: Dictionary = value
- included[str(state["entity_id"])] = true
- _apply_entity_state(state, immediate)
- if not reconcile:
- return
- for entity_id: String in _presentations.keys():
- if not included.has(entity_id):
- _remove_presentation(entity_id, false)
-
-
-func _apply_entity_state(value: Variant, immediate: bool) -> void:
- if not NetworkWorldSpawnProtocol.validate_entity_state(value):
- return
- var state: Dictionary = value
- var entity_id: String = str(state["entity_id"])
- var revision: int = int(state["revision"])
- if revision < int(_entity_revisions.get(entity_id, -1)):
- return
- var entry: GatherableDataType = _catalog.get_entry(
- StringName(str(state["type_id"]))
- )
- if entry == null:
- return
- var position: Vector3 = NetworkWorldSpawnProtocol.array_to_vector3(
- state["position"]
- )
- if not _presentation_enabled:
- _entity_revisions[entity_id] = revision
- return
- var presentation := _presentations.get(entity_id) as WorldGatherableType
- if presentation == null or not is_instance_valid(presentation):
- presentation = WorldGatherableType.new()
- presentation.name = "WorldGatherable_%s" % entity_id.right(12)
- _world_root.add_child(presentation)
- presentation.configure(
- entity_id,
- entry,
- position,
- float(state["yaw"]),
- )
- _presentations[entity_id] = presentation
- else:
- presentation.apply_network_state(
- position,
- float(state["yaw"]),
- immediate,
- )
- _entity_revisions[entity_id] = revision
- if not _session.is_host():
- _entities[entity_id] = {
- "entity_id": entity_id,
- "type_id": entry.type_id,
- "data": entry,
- "position": position,
- "yaw": float(state["yaw"]),
- "revision": revision,
- "locked": false,
- }
-
-
-func _apply_despawn(payload: Dictionary) -> void:
- var entity_id: String = str(payload.get("entity_id", ""))
- if entity_id.is_empty():
- return
- if not _session.is_host():
- _entities.erase(entity_id)
- _remove_presentation(entity_id, bool(payload.get("with_dust", false)))
-
-
-func _remove_presentation(entity_id: String, with_dust: bool) -> void:
- var presentation := _presentations.get(entity_id) as WorldGatherableType
- _presentations.erase(entity_id)
- _entity_revisions.erase(entity_id)
- if presentation != null and is_instance_valid(presentation):
- presentation.play_despawn(with_dust)
-
-
-func _apply_showcase(payload: Dictionary) -> void:
- if not _presentation_enabled:
- return
- if (
- typeof(payload.get("owner_peer_id")) != TYPE_INT
- or typeof(payload.get("visible")) != TYPE_BOOL
- ):
- return
- var peer_id: int = int(payload["owner_peer_id"])
- if peer_id == _session.get_local_peer_id():
- return
- var avatar: Player = _spawn_service.get_avatar(peer_id)
- if avatar == null:
- return
- if not bool(payload["visible"]):
- avatar.end_catch_showcase()
- return
- if typeof(payload.get("catch")) != TYPE_DICTIONARY:
- return
- var catch_data: Dictionary = payload["catch"]
- var fish: FishDataType = _fish_catalog.get_fish_by_id(
- StringName(str(catch_data.get("fish_id", "")))
- )
- var fish_catch: FishCatch = FishCatchType.from_network_dict(
- catch_data,
- fish,
- )
- if fish_catch != null:
- avatar.begin_remote_catch_showcase(fish_catch)
-
-
-func _state_to_network(state: Dictionary) -> Dictionary:
- return {
- "entity_id": str(state["entity_id"]),
- "type_id": str(state["type_id"]),
- "position": NetworkWorldSpawnProtocol.vector3_to_array(
- state["position"]
- ),
- "yaw": float(state["yaw"]),
- "revision": int(state["revision"]),
- }
-
-
-func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
- if not _session.is_host():
- return
- var states: Array[Dictionary] = []
- for state: Dictionary in _entities.values():
- states.append(_state_to_network(state))
- var envelope: Dictionary = _make_envelope(
- &"population",
- {"entities": states},
- )
- receive_world_envelope.rpc_id(peer_id, envelope)
-
-
-func _on_peer_removed(peer_id: int) -> void:
- _charge_requests.erase(peer_id)
- if _pending_captures.has(peer_id):
- _unlock_pending_capture(peer_id)
- _showcase_deadlines.erase(peer_id)
-
-
-func _unlock_pending_capture(peer_id: int) -> void:
- var pending: Dictionary = _pending_captures.get(peer_id, {})
- _pending_captures.erase(peer_id)
- if pending.is_empty():
- return
- var entity_id: String = str(pending.get("entity_id", ""))
- var state: Dictionary = _entities.get(entity_id, {})
- if state.is_empty():
- return
- state["locked"] = false
- _entities[entity_id] = state
-
-
-func _on_session_state_changed(_state: NetworkSession.State) -> void:
- if _session.is_gameplay_session_active():
- _begin_population_if_ready.call_deferred()
- return
- _clear_world()
- _population_session_id = ""
-
-
-func _on_calendar_date_changed(_date_id: String) -> void:
- if (
- _session == null
- or not _session.is_host()
- or not _session.is_gameplay_session_active()
- or _population_session_id.is_empty()
- ):
- return
- _reconcile_seasonal_population()
-
-
-func _reconcile_seasonal_population() -> void:
- if _catalog == null:
- return
- var season: int = _current_season()
- for entity_id: String in _entities.keys():
- var state: Dictionary = _entities.get(entity_id, {})
- var entry := state.get("data") as GatherableDataType
- if entry != null and not entry.is_available(season):
- _despawn_entity(entity_id, &"season_changed", false, false)
- var retained_respawns: Array[Dictionary] = []
- for respawn: Dictionary in _respawns:
- var entry: GatherableDataType = _catalog.get_entry(
- StringName(str(respawn.get("type_id", "")))
- )
- if entry != null and entry.is_available(season):
- retained_respawns.append(respawn)
- _respawns = retained_respawns
- for entry: GatherableDataType in _catalog.get_available_entries(season):
- _cache_spawn_surface(entry)
- var current_population: int = _population_for_type(entry.type_id)
- var target_population := _target_population(entry)
- while current_population < target_population:
- _spawn_entity(entry)
- current_population += 1
-
-
-func _target_population(entry: GatherableDataType) -> int:
- if entry == null or entry.spawn_anchor_set_id.is_empty():
- return entry.population if entry != null else 0
- var anchors: PackedVector3Array = _spawn_anchor_positions.get(
- entry.type_id,
- PackedVector3Array(),
- )
- return entry.target_population_for_anchor_count(anchors.size())
-
-
-func _population_for_type(type_id: StringName) -> int:
- var count: int = 0
- for state: Dictionary in _entities.values():
- if StringName(str(state.get("type_id", ""))) == type_id:
- count += 1
- for respawn: Dictionary in _respawns:
- if StringName(str(respawn.get("type_id", ""))) == type_id:
- count += 1
- return count
-
-
-func _current_season() -> int:
- return (
- _world_time.get_season()
- if _world_time != null
- else CalendarSeasonType.UNKNOWN
- )
-
-
-func _clear_world() -> void:
- for presentation: WorldGatherableType in _presentations.values():
- if presentation != null and is_instance_valid(presentation):
- presentation.queue_free()
- _entities.clear()
- _dirty_entity_ids.clear()
- _moving_players_by_cell.clear()
- _presentations.clear()
- _entity_revisions.clear()
- _surface_triangles.clear()
- _surface_areas.clear()
- _surface_total_areas.clear()
- _spawn_anchor_positions.clear()
- _respawns.clear()
- _next_respawn_by_type.clear()
- _charge_requests.clear()
- _pending_captures.clear()
- _showcase_deadlines.clear()
- _simulation_elapsed = 0.0
- _snapshot_elapsed = 0.0
-
-
-func get_network_metrics() -> Dictionary:
- return {
- "entities": _entities.size(),
- "presentations": _presentations.size(),
- "dirty_entities": _dirty_entity_ids.size(),
- "spawn_events_sent": _spawn_events_sent,
- "despawn_events_sent": _despawn_events_sent,
- "snapshot_packets_sent": _snapshot_packets_sent,
- "snapshot_states_sent": _snapshot_states_sent,
- }
-
-
-func _bound(values: Dictionary) -> void:
- while values.size() > MAX_LEDGER_ENTRIES:
- values.erase(values.keys().front())
-
-
-func _new_id(prefix: String) -> String:
- return "%s:%s" % [
- prefix,
- Crypto.new().generate_random_bytes(16).hex_encode(),
- ]
-
-
-func _now() -> float:
- return Time.get_ticks_msec() / 1000.0
diff --git a/network/network_world_spawn_service.gd.uid b/network/network_world_spawn_service.gd.uid
deleted file mode 100644
index e9badc5..0000000
--- a/network/network_world_spawn_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bmuhkkl2cxsck
diff --git a/network/network_world_time_service.gd b/network/network_world_time_service.gd
deleted file mode 100644
index 9cbd990..0000000
--- a/network/network_world_time_service.gd
+++ /dev/null
@@ -1,169 +0,0 @@
-class_name NetworkWorldTimeService
-extends Node
-
-const SYNC_INTERVAL_SECONDS: float = 5.0
-const MAX_SESSION_ID_LENGTH: int = 96
-
-var _session: NetworkSession
-var _world_time: WorldTimeService
-var _sync_elapsed: float = 0.0
-var _sequence: int = 0
-var _last_received_sequence: int = -1
-var _active_session_id: String = ""
-
-
-func setup(session: NetworkSession, world_time: WorldTimeService) -> void:
- _session = session
- _world_time = world_time
- _session.state_changed.connect(_on_session_state_changed)
- _session.peer_authenticated.connect(_on_peer_authenticated)
- if not _world_time.authoritative_time_set.is_connected(
- _on_authoritative_time_set
- ):
- _world_time.authoritative_time_set.connect(
- _on_authoritative_time_set
- )
- if not _world_time.calendar_date_changed.is_connected(
- _on_authoritative_calendar_date_changed
- ):
- _world_time.calendar_date_changed.connect(
- _on_authoritative_calendar_date_changed
- )
- set_process(true)
-
-
-func _process(delta: float) -> void:
- if _session == null or _world_time == null or not _session.is_host():
- return
- _sync_elapsed += delta
- if _sync_elapsed < SYNC_INTERVAL_SECONDS:
- return
- _sync_elapsed = 0.0
- _broadcast_snapshot()
-
-
-func _on_session_state_changed(state: NetworkSession.State) -> void:
- if _world_time == null or _session == null:
- return
- if state in [NetworkSession.State.PRIVATE_HOST, NetworkSession.State.OPEN_HOST]:
- if _active_session_id != _session.get_session_id():
- _active_session_id = _session.get_session_id()
- _sequence = 0
- _last_received_sequence = -1
- _sync_elapsed = 0.0
- _world_time.begin_authoritative_session()
- return
- if state == NetworkSession.State.JOINED_CLIENT:
- _active_session_id = _session.get_session_id()
- _last_received_sequence = -1
- _sync_elapsed = 0.0
- if _session.supports_server_capability(
- NetworkProtocol.WORLD_TIME_CAPABILITY
- ):
- _world_time.begin_remote_session()
- else:
- _world_time.end_session()
- return
- if state in [
- NetworkSession.State.INACTIVE,
- NetworkSession.State.DISCONNECTING,
- NetworkSession.State.CONNECTION_FAILED,
- NetworkSession.State.SERVER_LOST,
- ]:
- _active_session_id = ""
- _sequence = 0
- _last_received_sequence = -1
- _sync_elapsed = 0.0
- _world_time.end_session()
-
-
-func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
- if (
- _session == null
- or not _session.is_host()
- or not _session.peer_supports_capability(
- peer_id, NetworkProtocol.WORLD_TIME_CAPABILITY
- )
- ):
- return
- _send_snapshot(peer_id)
-
-
-func _on_authoritative_time_set(_time_hours: float) -> void:
- if _session == null or not _session.is_host():
- return
- _sync_elapsed = 0.0
- _broadcast_snapshot()
-
-
-func _on_authoritative_calendar_date_changed(_date_id: String) -> void:
- if _session == null or not _session.is_host():
- return
- _sync_elapsed = 0.0
- _broadcast_snapshot()
-
-
-func _broadcast_snapshot() -> void:
- if _session == null or not _session.is_host():
- return
- for peer_id: int in _session.get_authenticated_peer_ids():
- if peer_id == _session.get_local_peer_id():
- continue
- if _session.peer_supports_capability(
- peer_id, NetworkProtocol.WORLD_TIME_CAPABILITY
- ):
- _send_snapshot(peer_id)
-
-
-func _send_snapshot(peer_id: int) -> void:
- _sequence += 1
- receive_world_time_snapshot.rpc_id(peer_id, {
- "session_id": _session.get_session_id(),
- "time_hours": _world_time.get_time_hours(),
- "calendar_date_id": _world_time.get_calendar_date_id(),
- "sequence": _sequence,
- })
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_world_time_snapshot(data: Dictionary) -> void:
- if (
- _session == null
- or _world_time == null
- or not _session.is_joined_client()
- or not _session.supports_server_capability(
- NetworkProtocol.WORLD_TIME_CAPABILITY
- )
- or not validate_snapshot(data)
- or str(data["session_id"]) != _session.get_session_id()
- ):
- return
- var sequence: int = int(data["sequence"])
- if sequence <= _last_received_sequence:
- return
- _last_received_sequence = sequence
- _world_time.synchronize_calendar_time(
- float(data["time_hours"]),
- str(data["calendar_date_id"]),
- )
-
-
-static func validate_snapshot(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- return (
- typeof(value.get("session_id")) == TYPE_STRING
- and not str(value["session_id"]).is_empty()
- and str(value["session_id"]).length() <= MAX_SESSION_ID_LENGTH
- and typeof(value.get("time_hours")) in [TYPE_FLOAT, TYPE_INT]
- and is_finite(float(value["time_hours"]))
- and float(value["time_hours"]) >= 0.0
- and float(value["time_hours"]) < WorldTimeService.HOURS_PER_DAY
- and typeof(value.get("calendar_date_id")) == TYPE_STRING
- and WorldTimeService.is_valid_calendar_date_id(
- str(value["calendar_date_id"])
- )
- and typeof(value.get("sequence")) == TYPE_INT
- and int(value["sequence"]) >= 0
- )
diff --git a/network/network_world_time_service.gd.uid b/network/network_world_time_service.gd.uid
deleted file mode 100644
index a68e332..0000000
--- a/network/network_world_time_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://blig76aes25j
diff --git a/network/network_world_weather_service.gd b/network/network_world_weather_service.gd
deleted file mode 100644
index ba0e6e2..0000000
--- a/network/network_world_weather_service.gd
+++ /dev/null
@@ -1,156 +0,0 @@
-class_name NetworkWorldWeatherService
-extends Node
-
-const SYNC_INTERVAL_SECONDS: float = 15.0
-const MAX_SESSION_ID_LENGTH: int = 96
-const MAX_REMAINING_SECONDS: float = WorldWeatherService.WEATHER_PERIOD_SECONDS
-
-var _session: NetworkSession
-var _world_weather: WorldWeatherService
-var _sync_elapsed: float = 0.0
-var _sequence: int = 0
-var _last_received_sequence: int = -1
-var _active_session_id: String = ""
-
-
-func setup(
- session: NetworkSession,
- world_weather: WorldWeatherService,
-) -> void:
- _session = session
- _world_weather = world_weather
- _session.state_changed.connect(_on_session_state_changed)
- _session.peer_authenticated.connect(_on_peer_authenticated)
- _world_weather.weather_changed.connect(_on_weather_changed)
- set_process(true)
-
-
-func _process(delta: float) -> void:
- if _session == null or _world_weather == null or not _session.is_host():
- return
- _sync_elapsed += delta
- if _sync_elapsed < SYNC_INTERVAL_SECONDS:
- return
- _sync_elapsed = 0.0
- _broadcast_snapshot()
-
-
-func _on_session_state_changed(state: NetworkSession.State) -> void:
- if _world_weather == null or _session == null:
- return
- if state in [NetworkSession.State.PRIVATE_HOST, NetworkSession.State.OPEN_HOST]:
- if _active_session_id != _session.get_session_id():
- _active_session_id = _session.get_session_id()
- _sequence = 0
- _last_received_sequence = -1
- _sync_elapsed = 0.0
- _world_weather.begin_authoritative_session(
- _active_session_id.hash()
- )
- return
- if state == NetworkSession.State.JOINED_CLIENT:
- _active_session_id = _session.get_session_id()
- _last_received_sequence = -1
- _sync_elapsed = 0.0
- if _session.supports_server_capability(
- NetworkProtocol.WORLD_WEATHER_CAPABILITY
- ):
- _world_weather.begin_remote_session()
- else:
- _world_weather.end_session()
- return
- if state in [
- NetworkSession.State.INACTIVE,
- NetworkSession.State.DISCONNECTING,
- NetworkSession.State.CONNECTION_FAILED,
- NetworkSession.State.SERVER_LOST,
- ]:
- _active_session_id = ""
- _sequence = 0
- _last_received_sequence = -1
- _sync_elapsed = 0.0
- _world_weather.end_session()
-
-
-func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
- if (
- _session == null
- or not _session.is_host()
- or not _session.peer_supports_capability(
- peer_id, NetworkProtocol.WORLD_WEATHER_CAPABILITY
- )
- ):
- return
- _send_snapshot(peer_id)
-
-
-func _on_weather_changed(
- _weather: WorldWeatherService.Weather,
- _seconds_remaining: float,
-) -> void:
- if _session != null and _session.is_host():
- _broadcast_snapshot()
-
-
-func _broadcast_snapshot() -> void:
- if _session == null or not _session.is_host():
- return
- for peer_id: int in _session.get_authenticated_peer_ids():
- if peer_id == _session.get_local_peer_id():
- continue
- if _session.peer_supports_capability(
- peer_id, NetworkProtocol.WORLD_WEATHER_CAPABILITY
- ):
- _send_snapshot(peer_id)
-
-
-func _send_snapshot(peer_id: int) -> void:
- _sequence += 1
- receive_world_weather_snapshot.rpc_id(peer_id, {
- "session_id": _session.get_session_id(),
- "weather": int(_world_weather.get_weather()),
- "seconds_remaining": _world_weather.get_seconds_remaining(),
- "sequence": _sequence,
- })
-
-
-@rpc("authority", "call_remote", "reliable", 0)
-func receive_world_weather_snapshot(data: Dictionary) -> void:
- if (
- _session == null
- or _world_weather == null
- or not _session.is_joined_client()
- or not _session.supports_server_capability(
- NetworkProtocol.WORLD_WEATHER_CAPABILITY
- )
- or not validate_snapshot(data)
- or str(data["session_id"]) != _session.get_session_id()
- ):
- return
- var sequence: int = int(data["sequence"])
- if sequence <= _last_received_sequence:
- return
- _last_received_sequence = sequence
- _world_weather.apply_authoritative_snapshot(
- int(data["weather"]) as WorldWeatherService.Weather,
- float(data["seconds_remaining"]),
- )
-
-
-static func validate_snapshot(data: Variant) -> bool:
- if typeof(data) != TYPE_DICTIONARY:
- return false
- var value: Dictionary = data
- return (
- typeof(value.get("session_id")) == TYPE_STRING
- and not str(value["session_id"]).is_empty()
- and str(value["session_id"]).length() <= MAX_SESSION_ID_LENGTH
- and typeof(value.get("weather")) == TYPE_INT
- and WorldWeatherService.is_valid_weather(int(value["weather"]))
- and typeof(value.get("seconds_remaining")) in [TYPE_FLOAT, TYPE_INT]
- and is_finite(float(value["seconds_remaining"]))
- and float(value["seconds_remaining"]) >= 0.0
- and float(value["seconds_remaining"]) <= MAX_REMAINING_SECONDS
- and typeof(value.get("sequence")) == TYPE_INT
- and int(value["sequence"]) >= 0
- )
diff --git a/network/network_world_weather_service.gd.uid b/network/network_world_weather_service.gd.uid
deleted file mode 100644
index ffb9a1e..0000000
--- a/network/network_world_weather_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://llj46fx0oco7
diff --git a/network/peer_registry.gd b/network/peer_registry.gd
deleted file mode 100644
index 2cab061..0000000
--- a/network/peer_registry.gd
+++ /dev/null
@@ -1,116 +0,0 @@
-class_name PeerRegistry
-extends RefCounted
-
-class PeerRecord:
- extends RefCounted
-
- var peer_id: int = 0
- var profile_id: String = ""
- var display_name: String = ""
- var protocol_version: int = 0
- var joined_at_unix: int = 0
- var appearance_snapshot: Dictionary = (
- CharacterCustomizationCatalog.default_snapshot()
- )
- var identity_fingerprint: String = ""
- var identity_public_key: String = ""
- var identity_authenticated: bool = false
- var profile_authorization: Dictionary = {}
- var capability_flags: PackedStringArray = PackedStringArray()
-
-
-var _records: Dictionary[int, PeerRecord] = {}
-
-
-func add_peer(
- peer_id: int,
- profile_id: String,
- display_name: String,
- protocol_version: int,
- identity_fingerprint: String = "",
- identity_public_key: String = "",
- capability_flags: PackedStringArray = PackedStringArray(),
-) -> bool:
- if (
- peer_id <= 0
- or profile_id.is_empty()
- or (
- not identity_fingerprint.is_empty()
- and has_fingerprint(identity_fingerprint)
- )
- ):
- return false
- var record := PeerRecord.new()
- record.peer_id = peer_id
- record.profile_id = profile_id
- record.display_name = display_name
- record.protocol_version = protocol_version
- record.joined_at_unix = int(Time.get_unix_time_from_system())
- record.identity_fingerprint = identity_fingerprint
- record.identity_public_key = identity_public_key
- record.identity_authenticated = NetworkIdentityCrypto.valid_fingerprint(
- identity_fingerprint
- )
- record.capability_flags = capability_flags.duplicate()
- _records[peer_id] = record
- return true
-
-
-func remove_peer(peer_id: int) -> PeerRecord:
- var record: PeerRecord = _records.get(peer_id)
- _records.erase(peer_id)
- return record
-
-
-func get_peer(peer_id: int) -> PeerRecord:
- return _records.get(peer_id)
-
-
-func update_display_name(peer_id: int, display_name: String) -> bool:
- var record: PeerRecord = _records.get(peer_id)
- if record == null or display_name.is_empty():
- return false
- record.display_name = display_name
- return true
-
-
-func update_appearance(peer_id: int, snapshot: Dictionary) -> bool:
- var record: PeerRecord = _records.get(peer_id)
- if record == null or not CharacterCustomizationCatalog.validate_snapshot(snapshot):
- return false
- record.appearance_snapshot = snapshot.duplicate(true)
- return true
-
-
-func has_peer(peer_id: int) -> bool:
- return _records.has(peer_id)
-
-
-func has_profile(profile_id: String) -> bool:
- for record: PeerRecord in _records.values():
- if record.profile_id == profile_id:
- return true
- return false
-
-
-func has_fingerprint(fingerprint: String) -> bool:
- for record: PeerRecord in _records.values():
- if record.identity_fingerprint == fingerprint:
- return true
- return false
-
-
-func get_peer_ids() -> Array[int]:
- var result: Array[int] = []
- for peer_id: int in _records:
- result.append(peer_id)
- result.sort()
- return result
-
-
-func size() -> int:
- return _records.size()
-
-
-func clear() -> void:
- _records.clear()
diff --git a/network/peer_registry.gd.uid b/network/peer_registry.gd.uid
deleted file mode 100644
index da096b2..0000000
--- a/network/peer_registry.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://de3jtu2e0ut65
diff --git a/network/player_data_root.gd b/network/player_data_root.gd
deleted file mode 100644
index e173577..0000000
--- a/network/player_data_root.gd
+++ /dev/null
@@ -1,624 +0,0 @@
-class_name PlayerDataRoot
-extends Node
-
-signal status_changed(message: String)
-signal conflict_detected(message: String, conflict_path: String)
-
-const BOOTSTRAP_PATH := "user://data_root_bootstrap.json"
-const BOOTSTRAP_TEMP_PATH := "user://data_root_bootstrap.json.tmp"
-const BOOTSTRAP_VERSION := 1
-const MANIFEST_VERSION := 1
-const LAYOUT_VERSION := 1
-const MANIFEST_FILENAME := "straywild_data.json"
-const LEGACY_MANIFEST_FILENAME := "netfishing_data.json"
-const README_FILENAME := "README.txt"
-const ENVIRONMENT_VARIABLE := "straywild_DATA_DIR"
-const CREATE_ENVIRONMENT_VARIABLE := "straywild_CREATE_DATA_DIR"
-const LEGACY_ENVIRONMENT_VARIABLE := "NETFISHING_DATA_DIR"
-const LEGACY_CREATE_ENVIRONMENT_VARIABLE := "NETFISHING_CREATE_DATA_DIR"
-const APPLICATION_ID := "straywild"
-const LEGACY_APPLICATION_ID := "netfishing"
-const APP_DATA_PORTABLE_PATH := "user://portable-data"
-const LEGACY_USER_DIRECTORY_NAME := "NETFISHING"
-const LEGACY_USER_FILES := [
- "data_root_bootstrap.json",
- "data_root_bootstrap.json.backup",
- "network_discovery.cfg",
- "player_settings.json",
- "player_settings.json.backup",
- "controller_mappings.json",
- "controller_mappings.json.backup",
- "keyboard_mouse_bindings.json",
- "keyboard_mouse_bindings.json.backup",
- "player_save.json",
- "network_profile.json",
- "player_appearance.json",
- "saved_servers.json",
- "known_players.json",
- "player_relationships.json",
- "server_trust.json",
- "host_bans.json",
- "player_identity.key",
- "player_identity.pub",
- "player_identity.json",
- "host_identity.key",
- "host_identity.pub",
- "host_identity.json",
-]
-const PATH_ALLOWED: StringName = &"allowed"
-const PATH_SOURCE_PROJECT: StringName = &"source_project"
-const PATH_INSTALLATION: StringName = &"installation"
-
-enum Mode {
- UNRESOLVED,
- SELECTED_FOLDER,
- APP_DATA,
- ENVIRONMENT_OVERRIDE,
- COMMAND_LINE_OVERRIDE,
-}
-
-var mode := Mode.UNRESOLVED
-var root_path := ""
-var root_id := ""
-var device_id := ""
-var error_message := ""
-var requires_selection := false
-var override_active := false
-
-
-func resolve() -> bool:
- prepare_legacy_user_data()
- _load_bootstrap_identity()
- var command_line: String = _command_line_override()
- if not command_line.is_empty():
- override_active = true
- mode = Mode.COMMAND_LINE_OVERRIDE
- return _activate_existing(command_line, "", false)
- var environment_variable: String = _available_environment_variable(
- ENVIRONMENT_VARIABLE,
- LEGACY_ENVIRONMENT_VARIABLE,
- )
- if not environment_variable.is_empty():
- override_active = true
- mode = Mode.ENVIRONMENT_OVERRIDE
- var environment_path: String = OS.get_environment(environment_variable)
- if not environment_path.is_absolute_path():
- return _fail("straywild_DATA_DIR must be an absolute path.")
- if (
- _environment_flag(
- CREATE_ENVIRONMENT_VARIABLE,
- LEGACY_CREATE_ENVIRONMENT_VARIABLE,
- )
- and not has_data_manifest(environment_path)
- ):
- var created: Dictionary = create_unbound_root(environment_path)
- if not bool(created.get("ok", false)):
- return false
- return _activate_existing(environment_path, "", false)
- var bootstrap: Dictionary = _read_json(BOOTSTRAP_PATH, 64 * 1024)
- if bootstrap.is_empty() and FileAccess.file_exists(BOOTSTRAP_PATH + ".backup"):
- bootstrap = _read_json(BOOTSTRAP_PATH + ".backup", 64 * 1024)
- if not bootstrap.is_empty():
- if bootstrap.get("format_version") != BOOTSTRAP_VERSION:
- return _fail("The data-folder pointer uses an unsupported version.")
- var selected: String = str(bootstrap.get("selected_absolute_path", ""))
- var expected: String = str(bootstrap.get("expected_root_id", ""))
- if selected.is_empty():
- return _fail("The data-folder pointer is incomplete.")
- mode = (
- Mode.APP_DATA
- if selected == ProjectSettings.globalize_path(APP_DATA_PORTABLE_PATH)
- else Mode.SELECTED_FOLDER
- )
- return _activate_existing(selected, expected, false)
- requires_selection = true
- error_message = ""
- return false
-
-
-func prepare_legacy_user_data() -> void:
- var current_root: String = _normalize(
- ProjectSettings.globalize_path("user://")
- )
- if current_root.is_empty():
- return
- var legacy_root: String = _normalize(
- current_root.get_base_dir().path_join(LEGACY_USER_DIRECTORY_NAME)
- )
- if legacy_root == current_root or not DirAccess.dir_exists_absolute(legacy_root):
- return
- for filename: String in LEGACY_USER_FILES:
- var destination: String = current_root.path_join(filename)
- if FileAccess.file_exists(destination):
- continue
- var source: String = legacy_root.path_join(filename)
- if FileAccess.file_exists(source):
- _copy_legacy_user_file(source, destination)
-
-
-func has_data_manifest(path: String) -> bool:
- return not _existing_manifest_path(_normalize(path)).is_empty()
-
-
-func default_visible_path() -> String:
- var documents: String = OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS)
- if documents.is_empty():
- return ""
- var candidate: String = _normalize(documents.path_join("straywild"))
- if not candidate.is_absolute_path():
- return ""
- var classification: StringName = classify_candidate_path(
- candidate,
- ProjectSettings.globalize_path("res://"),
- OS.get_executable_path().get_base_dir(),
- )
- return candidate if classification == PATH_ALLOWED else ""
-
-
-func select_new_root(path: String, app_data: bool = false) -> bool:
- if override_active:
- return _fail("The data folder is controlled by a process override.")
- if device_id.length() != 32:
- device_id = Crypto.new().generate_random_bytes(16).hex_encode()
- var normalized: String = _normalize(path)
- if app_data:
- normalized = ProjectSettings.globalize_path(APP_DATA_PORTABLE_PATH)
- if not _validate_candidate(normalized, true):
- return false
- var manifest_path: String = _existing_manifest_path(normalized)
- if not manifest_path.is_empty():
- var manifest: Dictionary = _read_json(manifest_path)
- if not _valid_manifest(manifest):
- return _fail("The selected folder has a malformed straywild manifest.")
- root_id = str(manifest["root_id"])
- if not _upgrade_legacy_manifest(normalized, manifest, manifest_path):
- return _fail("The legacy data-folder manifest could not be upgraded.")
- else:
- if not _directory_is_empty(normalized) and not app_data:
- return _fail(
- "Choose an empty folder or an existing straywild data folder."
- )
- root_id = Crypto.new().generate_random_bytes(16).hex_encode()
- if not _create_layout(normalized, root_id):
- return _fail("The straywild data folder could not be created.")
- if not _write_bootstrap(normalized, root_id):
- return _fail("The data-folder pointer could not be saved.")
- root_path = normalized
- mode = Mode.APP_DATA if app_data else Mode.SELECTED_FOLDER
- requires_selection = false
- error_message = ""
- status_changed.emit("Data folder ready.")
- return true
-
-
-func create_unbound_root(path: String) -> Dictionary:
- var normalized: String = _normalize(path)
- if not _validate_candidate(normalized, true):
- return {"ok": false, "message": error_message}
- if not _directory_is_empty(normalized):
- return {"ok": false, "message": "The destination staging folder is not empty."}
- var id: String = Crypto.new().generate_random_bytes(16).hex_encode()
- if not _create_layout(normalized, id):
- return {"ok": false, "message": "The portable layout could not be created."}
- return {"ok": true, "root_id": id}
-
-
-func create_app_data_layout_for_migration(path: String) -> Dictionary:
- var normalized: String = _normalize(path)
- if not _validate_candidate(normalized, false):
- return {"ok": false, "message": error_message}
- var manifest_path: String = _existing_manifest_path(normalized)
- if not manifest_path.is_empty():
- var manifest: Dictionary = _read_json(manifest_path)
- if not _valid_manifest(manifest):
- return {"ok": false, "message": "The app-data manifest is malformed."}
- if not _upgrade_legacy_manifest(normalized, manifest, manifest_path):
- return {"ok": false, "message": "The app-data manifest could not be upgraded."}
- return {"ok": true, "root_id": str(manifest.get("root_id", ""))}
- var id: String = Crypto.new().generate_random_bytes(16).hex_encode()
- if not _create_layout(normalized, id):
- return {"ok": false, "message": "The app-data layout could not be created."}
- return {"ok": true, "root_id": id}
-
-
-func use_existing_root(path: String) -> bool:
- if override_active:
- return _fail("The data folder is controlled by a process override.")
- if device_id.length() != 32:
- device_id = Crypto.new().generate_random_bytes(16).hex_encode()
- var normalized: String = _normalize(path)
- if not _activate_existing(normalized, "", true):
- return false
- if not _write_bootstrap(root_path, root_id):
- root_path = ""
- root_id = ""
- return _fail("The data-folder pointer could not be saved.")
- mode = (
- Mode.APP_DATA
- if normalized == ProjectSettings.globalize_path(APP_DATA_PORTABLE_PATH)
- else Mode.SELECTED_FOLDER
- )
- return true
-
-
-func activate_process_root(path: String) -> bool:
- _load_bootstrap_identity()
- override_active = true
- mode = Mode.COMMAND_LINE_OVERRIDE
- return _activate_existing(path, "", false)
-
-
-func path_for(store_owner: StringName) -> String:
- var relative: String = {
- &"player_save": "player/player_save.nfsave",
- &"save_slots": "player/save_slots.json",
- &"network_profile": "player/network_profile.json",
- &"player_appearance": "player/player_appearance.json",
- &"saved_servers": "social/saved_servers.json",
- &"known_players": "social/known_players.json",
- &"player_relationships": "social/player_relationships.json",
- &"server_trust": "social/server_trust.json",
- &"host_bans": "social/host_bans.json",
- }.get(store_owner, "")
- return root_path.path_join(relative) if not relative.is_empty() else ""
-
-
-func conflict_directory() -> String:
- return root_path.path_join("backups/conflicts")
-
-
-func identity_backup_directory() -> String:
- return root_path.path_join("identity-backups")
-
-
-func progression_backup_directory() -> String:
- return root_path.path_join("progression-backups")
-
-
-func migration_backup_directory() -> String:
- return root_path.path_join("backups/migrations")
-
-
-func storage_mode_text() -> String:
- return {
- Mode.SELECTED_FOLDER: "Selected folder",
- Mode.APP_DATA: "App data",
- Mode.ENVIRONMENT_OVERRIDE: "Environment override",
- Mode.COMMAND_LINE_OVERRIDE: "Command-line override",
- }.get(mode, "Unavailable")
-
-
-func report_conflict(message: String, conflict_path: String) -> void:
- error_message = message
- conflict_detected.emit(message, conflict_path)
- status_changed.emit(message)
-
-
-func _activate_existing(path: String, expected_id: String, permit_creation: bool) -> bool:
- var normalized: String = _normalize(path)
- if not _validate_candidate(normalized, permit_creation):
- return false
- var manifest_path: String = _existing_manifest_path(normalized)
- if manifest_path.is_empty():
- return _fail("The selected folder is not a straywild data folder.")
- var manifest: Dictionary = _read_json(manifest_path)
- if not _valid_manifest(manifest):
- return _fail("The straywild data-folder manifest is malformed.")
- if not _upgrade_legacy_manifest(normalized, manifest, manifest_path):
- return _fail("The legacy data-folder manifest could not be upgraded.")
- var found_id: String = str(manifest["root_id"])
- if not expected_id.is_empty() and expected_id != found_id:
- return _fail("The selected data folder does not match this device pointer.")
- if not _test_writable(normalized):
- return _fail("The straywild data folder is unavailable or unwritable.")
- root_path = normalized
- root_id = found_id
- requires_selection = false
- error_message = ""
- if PortableFileGuard.has_syncthing_conflict(root_path.path_join("player")):
- status_changed.emit("Syncthing conflict copies were found. Review the data folder.")
- return true
-
-
-func _validate_candidate(path: String, create: bool) -> bool:
- var normalized: String = _normalize(path)
- if normalized.is_empty() or not normalized.is_absolute_path():
- return _fail("Choose an absolute filesystem folder.")
- var app_data_path: String = _normalize(
- ProjectSettings.globalize_path(APP_DATA_PORTABLE_PATH)
- )
- var exported_app_data: bool = (
- normalized == app_data_path
- and not OS.has_feature("editor")
- )
- var classification: StringName = classify_candidate_path(
- normalized,
- ProjectSettings.globalize_path("res://"),
- OS.get_executable_path().get_base_dir(),
- )
- if classification == PATH_SOURCE_PROJECT and not exported_app_data:
- return _fail("The project folder cannot be used as the player data folder.")
- if classification == PATH_INSTALLATION and not exported_app_data:
- return _fail(
- "The application installation folder cannot be used as the player data folder."
- )
- if not DirAccess.dir_exists_absolute(normalized):
- if not create or DirAccess.make_dir_recursive_absolute(normalized) != OK:
- return _fail("The selected data folder is unavailable.")
- return _test_writable(normalized)
-
-
-static func classify_candidate_path(
- candidate_path: String,
- project_reference: String,
- installation_reference: String,
- case_insensitive: bool = OS.get_name() == "Windows",
-) -> StringName:
- var candidate: String = _normalize_comparison_path(
- candidate_path, case_insensitive
- )
- if candidate.is_empty() or not candidate.is_absolute_path():
- return PATH_ALLOWED
- var project: String = _normalize_reference_path(
- project_reference, case_insensitive
- )
- if not project.is_empty() and _is_same_or_child_path(candidate, project):
- return PATH_SOURCE_PROJECT
- var installation: String = _normalize_reference_path(
- installation_reference, case_insensitive
- )
- if (
- not installation.is_empty()
- and _is_same_or_child_path(candidate, installation)
- ):
- return PATH_INSTALLATION
- return PATH_ALLOWED
-
-
-static func _normalize_reference_path(path: String, case_insensitive: bool) -> String:
- var normalized: String = _normalize_comparison_path(path, case_insensitive)
- if normalized.is_empty() or not normalized.is_absolute_path():
- return ""
- if normalized == "/" or _is_windows_drive_root(normalized):
- return ""
- return normalized
-
-
-static func _normalize_comparison_path(path: String, case_insensitive: bool) -> String:
- var normalized: String = path.strip_edges().replace("\\", "/").simplify_path()
- while normalized.length() > 1 and normalized.ends_with("/"):
- if _is_windows_drive_root(normalized):
- break
- normalized = normalized.left(-1)
- return normalized.to_lower() if case_insensitive else normalized
-
-
-static func _is_windows_drive_root(path: String) -> bool:
- return path.length() == 3 and path[1] == ":" and path[2] == "/"
-
-
-static func _is_same_or_child_path(candidate: String, reference: String) -> bool:
- return candidate == reference or candidate.begins_with(reference + "/")
-
-
-func _test_writable(path: String) -> bool:
- var probe: String = path.path_join(
- ".straywild-write-%s.tmp" % device_id.left(12)
- )
- var file: FileAccess = FileAccess.open(probe, FileAccess.WRITE)
- if file == null:
- return false
- file.store_string("probe")
- file.close()
- return DirAccess.remove_absolute(probe) == OK
-
-
-func _create_layout(path: String, id: String) -> bool:
- for relative: String in [
- "player", "player/saves", "social", "backups/saves", "backups/migrations",
- "backups/conflicts", "identity-backups", "progression-backups",
- ]:
- if DirAccess.make_dir_recursive_absolute(path.path_join(relative)) != OK:
- return false
- var now: int = int(Time.get_unix_time_from_system())
- var manifest: Dictionary = {
- "format_version": MANIFEST_VERSION,
- "layout_version": LAYOUT_VERSION,
- "application": APPLICATION_ID,
- "root_id": id,
- "created_at_unix": now,
- "last_opened_at_unix": now,
- }
- if not _write_text_atomic(
- path.path_join(MANIFEST_FILENAME), JSON.stringify(manifest, "\t")
- ):
- return false
- var readme: String = (
- "straywild player data\n\n"
- + "This folder is safe to synchronize with tools such as Syncthing.\n"
- + "Active private identity keys remain device-local.\n"
- + "Encrypted identity backups require their passphrase.\n"
- + "Progression exports do not contain identity keys or social data.\n"
- + "Chat and Session Mail are not stored here.\n"
- + "Do not play the same profile on two devices at the same time.\n"
- + "Conflicting edits are preserved under backups/conflicts; they are not merged.\n"
- )
- return _write_text_atomic(path.path_join(README_FILENAME), readme)
-
-
-func _write_bootstrap(path: String, id: String) -> bool:
- var now: int = int(Time.get_unix_time_from_system())
- var data: Dictionary = {
- "format_version": BOOTSTRAP_VERSION,
- "selected_absolute_path": path,
- "expected_root_id": id,
- "device_id": device_id,
- "selected_at_unix": now,
- "last_successfully_opened_at_unix": now,
- }
- return _write_text_atomic(
- ProjectSettings.globalize_path(BOOTSTRAP_PATH),
- JSON.stringify(data, "\t"),
- )
-
-
-func _load_bootstrap_identity() -> void:
- var data: Dictionary = _read_json(BOOTSTRAP_PATH, 64 * 1024)
- if data.is_empty():
- data = _read_json(BOOTSTRAP_PATH + ".backup", 64 * 1024)
- device_id = str(data.get("device_id", ""))
- if device_id.length() != 32:
- device_id = Crypto.new().generate_random_bytes(16).hex_encode()
-
-
-func _command_line_override() -> String:
- var args: PackedStringArray = OS.get_cmdline_user_args()
- for index: int in args.size():
- var value: String = args[index]
- if value.begins_with("--data-dir="):
- return value.trim_prefix("--data-dir=")
- if value == "--data-dir" and index + 1 < args.size():
- return args[index + 1]
- return ""
-
-
-func _valid_manifest(data: Dictionary) -> bool:
- return (
- data.get("format_version") == MANIFEST_VERSION
- and data.get("layout_version") == LAYOUT_VERSION
- and data.get("application") in [APPLICATION_ID, LEGACY_APPLICATION_ID]
- and typeof(data.get("root_id")) == TYPE_STRING
- and str(data["root_id"]).length() == 32
- )
-
-
-func _existing_manifest_path(path: String) -> String:
- var current: String = path.path_join(MANIFEST_FILENAME)
- if FileAccess.file_exists(current):
- return current
- var legacy: String = path.path_join(LEGACY_MANIFEST_FILENAME)
- return legacy if FileAccess.file_exists(legacy) else ""
-
-
-func _upgrade_legacy_manifest(
- path: String,
- manifest: Dictionary,
- manifest_path: String,
-) -> bool:
- if (
- manifest_path.get_file() == MANIFEST_FILENAME
- and manifest.get("application") == APPLICATION_ID
- ):
- return true
- var upgraded: Dictionary = manifest.duplicate(true)
- upgraded["application"] = APPLICATION_ID
- upgraded["last_opened_at_unix"] = int(Time.get_unix_time_from_system())
- return _write_text_atomic(
- path.path_join(MANIFEST_FILENAME),
- JSON.stringify(upgraded, "\t"),
- )
-
-
-func _available_environment_variable(primary: String, legacy: String) -> String:
- if OS.has_environment(primary):
- return primary
- return legacy if OS.has_environment(legacy) else ""
-
-
-func _environment_flag(primary: String, legacy: String) -> bool:
- var variable: String = _available_environment_variable(primary, legacy)
- return not variable.is_empty() and OS.get_environment(variable) == "1"
-
-
-func _copy_legacy_user_file(source: String, destination: String) -> bool:
- var bytes: PackedByteArray = PortableFileGuard.read_bytes(
- source,
- 16 * 1024 * 1024,
- )
- if bytes.is_empty() and FileAccess.get_open_error() != OK:
- return false
- if DirAccess.make_dir_recursive_absolute(destination.get_base_dir()) != OK:
- return false
- var file: FileAccess = FileAccess.open(destination, FileAccess.WRITE)
- if file == null:
- return false
- file.store_buffer(bytes)
- file.flush()
- var copied: bool = file.get_error() == OK
- file.close()
- if copied and source.ends_with(".key"):
- FileAccess.set_unix_permissions(destination, 384)
- return copied
-
-
-func _directory_is_empty(path: String) -> bool:
- var access: DirAccess = DirAccess.open(path)
- if access == null:
- return true
- access.list_dir_begin()
- var entry_name: String = access.get_next()
- while entry_name in [".", ".."]:
- entry_name = access.get_next()
- access.list_dir_end()
- return entry_name.is_empty()
-
-
-func _normalize(path: String) -> String:
- var normalized: String = path.replace("\\", "/").simplify_path()
- while normalized.length() > 1 and normalized.ends_with("/"):
- if _is_windows_drive_root(normalized):
- break
- normalized = normalized.left(-1)
- return normalized
-
-
-func _read_json(path: String, maximum := 1024 * 1024) -> Dictionary:
- if not FileAccess.file_exists(path):
- return {}
- var file: FileAccess = FileAccess.open(path, FileAccess.READ)
- if file == null or file.get_length() > maximum:
- return {}
- var json: JSON = JSON.new()
- var error: Error = json.parse(file.get_as_text())
- file.close()
- return json.data if error == OK and typeof(json.data) == TYPE_DICTIONARY else {}
-
-
-func _write_text_atomic(path: String, text: String) -> bool:
- var absolute: String = (
- ProjectSettings.globalize_path(path)
- if path.begins_with("user://")
- else path
- )
- if DirAccess.make_dir_recursive_absolute(absolute.get_base_dir()) != OK:
- return false
- var temporary: String = absolute + ".tmp"
- var file: FileAccess = FileAccess.open(temporary, FileAccess.WRITE)
- if file == null:
- return false
- file.store_string(text)
- file.flush()
- var ok: bool = file.get_error() == OK
- file.close()
- if not ok:
- return false
- var backup: String = absolute + ".backup"
- if FileAccess.file_exists(backup):
- DirAccess.remove_absolute(backup)
- var had_primary: bool = FileAccess.file_exists(absolute)
- if had_primary and DirAccess.rename_absolute(absolute, backup) != OK:
- DirAccess.remove_absolute(temporary)
- return false
- if DirAccess.rename_absolute(temporary, absolute) != OK:
- if had_primary:
- DirAccess.rename_absolute(backup, absolute)
- return false
- if FileAccess.file_exists(backup):
- DirAccess.remove_absolute(backup)
- return true
-
-
-func _fail(message: String) -> bool:
- error_message = message
- status_changed.emit(message)
- return false
diff --git a/network/player_data_root.gd.uid b/network/player_data_root.gd.uid
deleted file mode 100644
index 0aa1857..0000000
--- a/network/player_data_root.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dpiv6wttry0j4
diff --git a/network/player_identity_store.gd b/network/player_identity_store.gd
deleted file mode 100644
index 80d8aa3..0000000
--- a/network/player_identity_store.gd
+++ /dev/null
@@ -1,6 +0,0 @@
-class_name PlayerIdentityStore
-extends "res://network/local_signing_identity_store.gd"
-
-
-func _ready() -> void:
- configure("player_identity")
diff --git a/network/player_identity_store.gd.uid b/network/player_identity_store.gd.uid
deleted file mode 100644
index ce591b3..0000000
--- a/network/player_identity_store.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cqlgolkbx2riq
diff --git a/network/player_list_entry.gd b/network/player_list_entry.gd
deleted file mode 100644
index c75832a..0000000
--- a/network/player_list_entry.gd
+++ /dev/null
@@ -1,21 +0,0 @@
-class_name PlayerListEntry
-extends RefCounted
-
-var peer_id := 0
-var full_fingerprint := ""
-var compact_fingerprint := ""
-var display_name := ""
-var is_host := false
-var is_operator := false
-var is_local_player := false
-var continuity_state := ""
-var ping_to_host_ms := -1
-var muted := false
-var blocked := false
-var is_friend := false
-var can_request_friend := false
-var can_kick := false
-var can_ban := false
-var can_clear_art := false
-var can_manage_operator := false
-var revision := 0
diff --git a/network/player_list_entry.gd.uid b/network/player_list_entry.gd.uid
deleted file mode 100644
index fa479f9..0000000
--- a/network/player_list_entry.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c71playerentry001
diff --git a/network/player_relationship_store.gd b/network/player_relationship_store.gd
deleted file mode 100644
index de04d49..0000000
--- a/network/player_relationship_store.gd
+++ /dev/null
@@ -1,320 +0,0 @@
-class_name PlayerRelationshipStore
-extends Node
-
-signal relationship_changed(fingerprint: String)
-
-const FORMAT_VERSION := 2
-const MAX_RECORDS := 500
-const MAX_FRIENDS := 200
-const SOCIAL_TOKEN_LENGTH := 64
-const PRESENCE_CHANNEL_DOMAIN := "straywild_PRESENCE_V1:"
-const INVITE_INBOX_DOMAIN := "straywild_INVITE_V1:"
-
-var _records: Dictionary = {}
-var _loaded := false
-var _write_blocked := false
-var _store_path := ""
-var _expected_hash := ""
-var _data_root: PlayerDataRoot
-
-
-func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
- _store_path = path
- _data_root = data_root
- _records.clear()
- _loaded = false
- _write_blocked = false
- _expected_hash = ""
-
-
-func is_muted(fingerprint: String) -> bool:
- _ensure_loaded()
- return bool(_records.get(fingerprint, {}).get("muted", false))
-
-
-func is_blocked(fingerprint: String) -> bool:
- _ensure_loaded()
- return bool(_records.get(fingerprint, {}).get("blocked", false))
-
-
-func is_friend(fingerprint: String) -> bool:
- _ensure_loaded()
- return bool(_records.get(fingerprint, {}).get("friend", false))
-
-
-func set_muted(fingerprint: String, display_name: String, value: bool) -> bool:
- if not _valid_target(fingerprint, display_name):
- return false
- _ensure_loaded()
- var record := _record(fingerprint, display_name)
- record["muted"] = value or bool(record.get("blocked", false))
- return _commit(fingerprint, record)
-
-
-func set_blocked(fingerprint: String, display_name: String, value: bool) -> bool:
- if not _valid_target(fingerprint, display_name):
- return false
- _ensure_loaded()
- var record := _record(fingerprint, display_name)
- record["blocked"] = value
- # Blocking owns the accompanying mute. Removing the block establishes a
- # fresh visibility boundary for future messages; already suppressed
- # messages retain their immutable local suppression flag.
- record["muted"] = value
- if value:
- _clear_friend_fields(record)
- return _commit(fingerprint, record)
-
-
-func create_friend_capabilities() -> Dictionary:
- var presence_write_token := NetworkIdentityCrypto.secure_id(32)
- var invite_token := NetworkIdentityCrypto.secure_id(32)
- if not _valid_social_token(presence_write_token) or not _valid_social_token(invite_token):
- return {}
- return {
- "local_presence_write_token": presence_write_token,
- "local_invite_token": invite_token,
- "presence_channel": presence_channel_for_write_token(
- presence_write_token
- ),
- "invite_token": invite_token,
- }
-
-
-func add_friend(
- fingerprint: String,
- display_name: String,
- local_capabilities: Dictionary,
- remote_capabilities: Dictionary,
-) -> bool:
- if (
- not _valid_target(fingerprint, display_name)
- or is_blocked(fingerprint)
- or not _valid_local_capabilities(local_capabilities)
- or not _valid_public_capabilities(remote_capabilities)
- ):
- return false
- _ensure_loaded()
- if not is_friend(fingerprint) and get_friends().size() >= MAX_FRIENDS:
- return false
- var record := _record(fingerprint, display_name)
- var now := int(Time.get_unix_time_from_system())
- record["friend"] = true
- record["friend_since_unix"] = int(record.get("friend_since_unix", now))
- record["local_presence_write_token"] = str(
- local_capabilities["local_presence_write_token"]
- )
- record["local_invite_token"] = str(
- local_capabilities["local_invite_token"]
- )
- record["remote_presence_channel"] = str(
- remote_capabilities["presence_channel"]
- )
- record["remote_invite_token"] = str(
- remote_capabilities["invite_token"]
- )
- return _commit(fingerprint, record)
-
-
-func remove_friend(fingerprint: String, display_name: String) -> bool:
- if not _valid_target(fingerprint, display_name):
- return false
- _ensure_loaded()
- var record := _record(fingerprint, display_name)
- _clear_friend_fields(record)
- return _commit(fingerprint, record)
-
-
-func get_friend_record(fingerprint: String) -> Dictionary:
- _ensure_loaded()
- var record: Dictionary = _records.get(fingerprint, {})
- return record.duplicate(true) if bool(record.get("friend", false)) else {}
-
-
-func get_friends() -> Array[Dictionary]:
- _ensure_loaded()
- var result: Array[Dictionary] = []
- for value: Dictionary in _records.values():
- if bool(value.get("friend", false)):
- result.append(value.duplicate(true))
- _sort_records(result)
- return result
-
-
-static func presence_channel_for_write_token(write_token: String) -> String:
- return (PRESENCE_CHANNEL_DOMAIN + write_token).sha256_text()
-
-
-static func invite_inbox_id(invite_token: String) -> String:
- return (INVITE_INBOX_DOMAIN + invite_token).sha256_text()
-
-
-func get_records() -> Array[Dictionary]:
- _ensure_loaded()
- var result: Array[Dictionary] = []
- for value: Dictionary in _records.values():
- if bool(value.get("muted", false)) or bool(value.get("blocked", false)):
- result.append(value.duplicate(true))
- _sort_records(result)
- return result
-
-
-func _record(fingerprint: String, display_name: String) -> Dictionary:
- var now := int(Time.get_unix_time_from_system())
- var record: Dictionary = _records.get(fingerprint, {
- "fingerprint": fingerprint,
- "created_unix": now,
- "muted": false,
- "blocked": false,
- "friend": false,
- })
- record["last_known_display_name"] = display_name
- record["updated_unix"] = now
- return record
-
-
-func _commit(fingerprint: String, record: Dictionary) -> bool:
- if _write_blocked:
- return false
- var previous: Dictionary = _records.duplicate(true)
- if (
- not bool(record.get("muted", false))
- and not bool(record.get("blocked", false))
- and not bool(record.get("friend", false))
- ):
- _records.erase(fingerprint)
- else:
- _records[fingerprint] = record
- while _records.size() > MAX_RECORDS:
- _records.erase(_records.keys().front())
- if not _save():
- _records = previous
- return false
- relationship_changed.emit(fingerprint)
- return true
-
-
-func _valid_target(fingerprint: String, display_name: String) -> bool:
- return (
- NetworkIdentityCrypto.valid_fingerprint(fingerprint)
- and NetworkProfilePreferences.is_valid_display_name(display_name)
- )
-
-
-func _ensure_loaded() -> void:
- if _loaded:
- return
- _loaded = true
- if _store_path.is_empty() or not FileAccess.file_exists(_store_path):
- return
- var file := FileAccess.open(_store_path, FileAccess.READ)
- if file == null:
- return
- var json := JSON.new()
- if json.parse(file.get_as_text()) != OK or typeof(json.data) != TYPE_DICTIONARY:
- return
- var data: Dictionary = json.data
- var format_version: int = int(data.get("format_version", 0))
- if format_version not in [1, FORMAT_VERSION]:
- _write_blocked = true
- return
- for value: Variant in data.get("records", []):
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var record: Dictionary = value
- var fingerprint := str(record.get("fingerprint", ""))
- if not NetworkIdentityCrypto.valid_fingerprint(fingerprint):
- continue
- record["blocked"] = bool(record.get("blocked", false))
- record["muted"] = bool(record.get("muted", false)) or record["blocked"]
- record["friend"] = (
- bool(record.get("friend", false))
- and not record["blocked"]
- and _valid_stored_friend_capabilities(record)
- )
- if not record["friend"]:
- _clear_friend_fields(record)
- _records[fingerprint] = record.duplicate(true)
- _expected_hash = PortableFileGuard.hash_file(_store_path)
-
-
-func _save() -> bool:
- var bytes := JSON.stringify({
- "format_version": FORMAT_VERSION,
- "records": _records.values(),
- }, "\t").to_utf8_buffer()
- var result := PortableFileGuard.write_guarded(
- _store_path, bytes, _expected_hash, _data_root.conflict_directory(),
- _data_root.device_id,
- )
- if bool(result.get("conflict", false)):
- _data_root.report_conflict(str(result.get("message", "")), str(result.get("conflict_path", "")))
- if bool(result.get("ok", false)):
- _expected_hash = str(result["hash"])
- return bool(result.get("ok", false))
-
-
-func _clear_friend_fields(record: Dictionary) -> void:
- record["friend"] = false
- for key: String in [
- "friend_since_unix",
- "local_presence_write_token",
- "local_invite_token",
- "remote_presence_channel",
- "remote_invite_token",
- ]:
- record.erase(key)
-
-
-func _valid_local_capabilities(value: Dictionary) -> bool:
- var write_token := str(value.get("local_presence_write_token", ""))
- var invite_token := str(value.get("local_invite_token", ""))
- return (
- _valid_social_token(write_token)
- and _valid_social_token(invite_token)
- and str(value.get("presence_channel", ""))
- == presence_channel_for_write_token(write_token)
- and str(value.get("invite_token", "")) == invite_token
- )
-
-
-func _valid_public_capabilities(value: Dictionary) -> bool:
- return (
- _valid_social_token(str(value.get("presence_channel", "")))
- and _valid_social_token(str(value.get("invite_token", "")))
- )
-
-
-func _valid_stored_friend_capabilities(record: Dictionary) -> bool:
- var local: Dictionary = {
- "local_presence_write_token": str(
- record.get("local_presence_write_token", "")
- ),
- "local_invite_token": str(record.get("local_invite_token", "")),
- "presence_channel": presence_channel_for_write_token(str(
- record.get("local_presence_write_token", "")
- )),
- "invite_token": str(record.get("local_invite_token", "")),
- }
- var remote: Dictionary = {
- "presence_channel": str(record.get("remote_presence_channel", "")),
- "invite_token": str(record.get("remote_invite_token", "")),
- }
- return _valid_local_capabilities(local) and _valid_public_capabilities(remote)
-
-
-func _valid_social_token(value: String) -> bool:
- if value.length() != SOCIAL_TOKEN_LENGTH:
- return false
- for character: String in value:
- if character not in "0123456789abcdef":
- return false
- return true
-
-
-func _sort_records(records: Array[Dictionary]) -> void:
- records.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
- return str(a.get("last_known_display_name", "")).naturalnocasecmp_to(
- str(b.get("last_known_display_name", ""))
- ) < 0
- )
diff --git a/network/player_relationship_store.gd.uid b/network/player_relationship_store.gd.uid
deleted file mode 100644
index a63c807..0000000
--- a/network/player_relationship_store.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c71relationships01
diff --git a/network/player_spawn_service.gd b/network/player_spawn_service.gd
deleted file mode 100644
index 1d1c292..0000000
--- a/network/player_spawn_service.gd
+++ /dev/null
@@ -1,116 +0,0 @@
-class_name PlayerSpawnService
-extends Node
-
-const PlayerScene: PackedScene = preload("res://player/player.tscn")
-
-signal avatar_spawned(peer_id: int, avatar: Player)
-signal avatar_removed(peer_id: int)
-
-var _players_root: Node3D
-var _local_player: Player
-var _spawn_transform: Transform3D
-var _avatars: Dictionary[int, Player] = {}
-var _local_peer_id: int = 1
-var _dedicated_runtime: bool = false
-
-
-func setup(
- players_root: Node3D,
- local_player: Player,
- spawn_transform: Transform3D,
- dedicated_runtime: bool = false,
-) -> void:
- _players_root = players_root
- _local_player = local_player
- _spawn_transform = spawn_transform
- _dedicated_runtime = dedicated_runtime
-
-
-func set_spawn_transform(spawn_transform: Transform3D) -> void:
- _spawn_transform = spawn_transform
-
-
-func register_local_player(peer_id: int) -> void:
- if _local_player == null:
- return
- for existing_id: int in _avatars.keys():
- if _avatars[existing_id] == _local_player:
- _avatars.erase(existing_id)
- _local_peer_id = peer_id
- _avatars[peer_id] = _local_player
- _local_player.name = "Player_%d" % peer_id
- _local_player.set_local_control(true)
- _local_player.set_network_peer_id(peer_id)
-
-
-func spawn_remote_player(
- peer_id: int,
- transform: Transform3D,
- authoritative_simulation: bool = false,
-) -> Player:
- if peer_id <= 0 or peer_id == _local_peer_id:
- return null
- var existing: Player = _avatars.get(peer_id)
- if existing != null and is_instance_valid(existing):
- return existing
- var avatar := PlayerScene.instantiate() as Player
- avatar.name = "Player_%d" % peer_id
- avatar.set_local_control(false)
- avatar.set_network_peer_id(peer_id)
- _players_root.add_child(avatar)
- avatar.global_transform = transform
- avatar.configure_network_remote(
- authoritative_simulation,
- _dedicated_runtime and authoritative_simulation,
- )
- _avatars[peer_id] = avatar
- avatar_spawned.emit(peer_id, avatar)
- return avatar
-
-
-func remove_peer(peer_id: int) -> void:
- if peer_id == _local_peer_id:
- return
- var avatar: Player = _avatars.get(peer_id)
- _avatars.erase(peer_id)
- if avatar != null and is_instance_valid(avatar):
- avatar.queue_free()
- avatar_removed.emit(peer_id)
-
-
-func clear_remote_players() -> void:
- for peer_id: int in _avatars.keys():
- if peer_id != _local_peer_id:
- remove_peer(peer_id)
-
-
-func get_avatar(peer_id: int) -> Player:
- return _avatars.get(peer_id)
-
-
-func get_local_player() -> Player:
- return _local_player
-
-
-func set_peer_presentation_visible(peer_id: int, should_be_visible: bool) -> void:
- var avatar: Player = _avatars.get(peer_id)
- if avatar != null and is_instance_valid(avatar):
- avatar.set_remote_presentation_visible(should_be_visible)
-
-
-func get_peer_ids() -> Array[int]:
- var result: Array[int] = []
- for peer_id: int in _avatars:
- result.append(peer_id)
- result.sort()
- return result
-
-
-func get_spawn_transform_for_index(index: int) -> Transform3D:
- if index <= 0:
- return _spawn_transform
- var angle: float = float(index) * 2.3999632297
- var ring: float = 1.4 + floor(float(index - 1) / 8.0) * 1.2
- var result: Transform3D = _spawn_transform
- result.origin += Vector3(cos(angle) * ring, 0.0, sin(angle) * ring)
- return result
diff --git a/network/player_spawn_service.gd.uid b/network/player_spawn_service.gd.uid
deleted file mode 100644
index 93d44f8..0000000
--- a/network/player_spawn_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://8w70d0nrf4a7
diff --git a/network/portable_data_migration.gd b/network/portable_data_migration.gd
deleted file mode 100644
index 2de0acb..0000000
--- a/network/portable_data_migration.gd
+++ /dev/null
@@ -1,406 +0,0 @@
-class_name PortableDataMigration
-extends RefCounted
-
-const LEGACY_FILES := {
- "player_save.json": "player/player_save.json",
- "network_profile.json": "player/network_profile.json",
- "player_appearance.json": "player/player_appearance.json",
- "saved_servers.json": "social/saved_servers.json",
- "known_players.json": "social/known_players.json",
- "player_relationships.json": "social/player_relationships.json",
- "server_trust.json": "social/server_trust.json",
- "host_bans.json": "social/host_bans.json",
-}
-
-
-static func legacy_files_present() -> bool:
- for filename: String in LEGACY_FILES:
- if FileAccess.file_exists("user://".path_join(filename)):
- return true
- return false
-
-
-static func migrate_legacy_to(
- data_root: PlayerDataRoot,
- destination: String,
-) -> Dictionary:
- var normalized: String = destination.simplify_path().trim_suffix("/")
- if DirAccess.dir_exists_absolute(normalized):
- if data_root.has_data_manifest(normalized):
- return {
- "ok": false,
- "requires_existing_root_decision": true,
- "message": "The selected folder already contains straywild data.",
- }
- if not _directory_empty(normalized):
- return {
- "ok": false,
- "message": "Choose an empty folder or create a straywild subfolder.",
- }
- var staging: String = "%s.migration-%s" % [
- normalized, Crypto.new().generate_random_bytes(8).hex_encode()
- ]
- if DirAccess.make_dir_recursive_absolute(staging) != OK:
- return {"ok": false, "message": "Migration staging could not be created."}
- var created: Dictionary = data_root.create_unbound_root(staging)
- if not bool(created.get("ok", false)):
- _remove_tree(staging)
- return {"ok": false, "message": str(created.get("message", ""))}
- var copied: Array[String] = []
- for source_name: String in LEGACY_FILES:
- var source: String = ProjectSettings.globalize_path(
- "user://".path_join(source_name)
- )
- if not FileAccess.file_exists(source):
- continue
- var target: String = staging.path_join(str(LEGACY_FILES[source_name]))
- var result: Dictionary = _copy_verified_json(source, target)
- if not bool(result.get("ok", false)):
- _remove_tree(staging)
- return {
- "ok": false,
- "message": "Migration failed while validating %s." % source_name,
- }
- copied.append(source_name)
- if DirAccess.dir_exists_absolute(normalized):
- DirAccess.remove_absolute(normalized)
- if DirAccess.rename_absolute(staging, normalized) != OK:
- _remove_tree(staging)
- return {"ok": false, "message": "Migration could not activate its destination."}
- var manifest: Dictionary = _read_json(
- normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)
- )
- var migrated_root_id: String = str(manifest.get("root_id", ""))
- if migrated_root_id.is_empty() or not data_root.use_existing_root(normalized):
- return {"ok": false, "message": "Migration completed but could not switch roots."}
- var recovery: String = ProjectSettings.globalize_path(
- "user://migration-recovery"
- ).path_join(
- Time.get_datetime_string_from_system().replace(":", "-")
- )
- DirAccess.make_dir_recursive_absolute(recovery)
- for source_name: String in copied:
- _copy_bytes(
- ProjectSettings.globalize_path("user://".path_join(source_name)),
- recovery.path_join(source_name),
- )
- return {
- "ok": true,
- "message": "Player data moved successfully.",
- "recovery_path": recovery,
- }
-
-
-static func migrate_active_to(
- data_root: PlayerDataRoot,
- destination: String,
-) -> Dictionary:
- var normalized: String = destination.simplify_path().trim_suffix("/")
- if normalized == data_root.root_path:
- return {
- "ok": true,
- "changed": false,
- "message": "This data folder is already active.",
- }
- if DirAccess.dir_exists_absolute(normalized) and not _directory_empty(normalized):
- if data_root.has_data_manifest(normalized):
- return {
- "ok": false,
- "requires_existing_root_decision": true,
- "message": (
- "The selected folder already contains straywild data. "
- + "Choose it through the existing-data recovery flow."
- ),
- }
- return {"ok": false, "message": "Choose an empty folder or a straywild subfolder."}
- var staging: String = "%s.migration-%s" % [
- normalized, Crypto.new().generate_random_bytes(8).hex_encode()
- ]
- if DirAccess.make_dir_recursive_absolute(staging) != OK:
- return {"ok": false, "message": "Migration staging could not be created."}
- var created: Dictionary = data_root.create_unbound_root(staging)
- if not bool(created.get("ok", false)):
- _remove_tree(staging)
- return {"ok": false, "message": str(created.get("message", ""))}
- for relative: String in [
- "player/player_save.nfsave",
- "player/player_save.json",
- "player/save_slots.json",
- "player/network_profile.json",
- "player/player_appearance.json",
- "social/saved_servers.json",
- "social/known_players.json",
- "social/player_relationships.json",
- "social/server_trust.json",
- "social/host_bans.json",
- ]:
- var source: String = data_root.root_path.path_join(relative)
- if not FileAccess.file_exists(source):
- continue
- if not bool(
- _copy_verified_owned_file(
- source, staging.path_join(relative)
- ).get("ok", false)
- ):
- _remove_tree(staging)
- return {"ok": false, "message": "Migration validation failed for %s." % relative}
- var source_slots: String = data_root.root_path.path_join("player/saves")
- if (
- DirAccess.dir_exists_absolute(source_slots)
- and not _copy_progression_slots(
- source_slots,
- staging.path_join("player/saves"),
- )
- ):
- _remove_tree(staging)
- return {"ok": false, "message": "Migration validation failed for save slots."}
- if DirAccess.dir_exists_absolute(normalized):
- DirAccess.remove_absolute(normalized)
- if DirAccess.rename_absolute(staging, normalized) != OK:
- _remove_tree(staging)
- return {"ok": false, "message": "Migration could not activate its destination."}
- var old_root: String = data_root.root_path
- if not data_root.use_existing_root(normalized):
- return {"ok": false, "message": "Migration copied data but did not change the pointer."}
- return {
- "ok": true,
- "changed": true,
- "message": "Data folder changed. Previous data was preserved.",
- "previous_root": old_root,
- }
-
-
-static func adopt_legacy_app_data(data_root: PlayerDataRoot) -> Dictionary:
- var source_root: String = ProjectSettings.globalize_path("user://").trim_suffix("/")
- var root: String = ProjectSettings.globalize_path(
- PlayerDataRoot.APP_DATA_PORTABLE_PATH
- )
- if DirAccess.make_dir_recursive_absolute(root) != OK:
- return {"ok": false, "message": "The app-data folder could not be created."}
- var created: Dictionary = data_root.create_app_data_layout_for_migration(root)
- if not bool(created.get("ok", false)):
- return created
- for source_name: String in LEGACY_FILES:
- var source: String = source_root.path_join(source_name)
- if not FileAccess.file_exists(source):
- continue
- var target: String = root.path_join(str(LEGACY_FILES[source_name]))
- if not bool(_copy_verified_json(source, target).get("ok", false)):
- return {
- "ok": false,
- "message": "Could not validate %s in app-data mode." % source_name,
- }
- if not data_root.use_existing_root(root):
- return {"ok": false, "message": data_root.error_message}
- return {
- "ok": true,
- "message": "Existing player data remains in app data.",
- "recovery_path": source_root,
- }
-
-
-static func replace_existing_with_legacy(
- data_root: PlayerDataRoot,
- destination: String,
-) -> Dictionary:
- var normalized: String = destination.simplify_path().trim_suffix("/")
- if not data_root.has_data_manifest(normalized):
- return {"ok": false, "message": "The selected folder is not a straywild data root."}
- var recovery: String = ProjectSettings.globalize_path(
- "user://migration-recovery"
- ).path_join(
- "replaced-root-" + Time.get_datetime_string_from_system().replace(":", "-")
- )
- if not _copy_tree(normalized, recovery):
- return {"ok": false, "message": "The selected data could not be backed up."}
- _remove_tree(normalized)
- var result: Dictionary = migrate_legacy_to(data_root, normalized)
- if bool(result.get("ok", false)):
- result["replaced_root_backup"] = recovery
- return result
-
-
-static func replace_existing_with_active(
- data_root: PlayerDataRoot,
- destination: String,
-) -> Dictionary:
- var normalized: String = destination.simplify_path().trim_suffix("/")
- if not data_root.has_data_manifest(normalized):
- return {"ok": false, "message": "The selected folder is not a straywild data root."}
- var recovery: String = ProjectSettings.globalize_path(
- "user://migration-recovery"
- ).path_join(
- "replaced-root-" + Time.get_datetime_string_from_system().replace(":", "-")
- )
- if not _copy_tree(normalized, recovery):
- return {"ok": false, "message": "The selected data could not be backed up."}
- _remove_tree(normalized)
- var result: Dictionary = migrate_active_to(data_root, normalized)
- if bool(result.get("ok", false)):
- result["replaced_root_backup"] = recovery
- return result
-
-
-static func _copy_verified_json(source: String, destination: String) -> Dictionary:
- var bytes: PackedByteArray = PortableFileGuard.read_bytes(source)
- if bytes.is_empty() and FileAccess.get_open_error() != OK:
- return {"ok": false}
- var json: JSON = JSON.new()
- if (
- json.parse(bytes.get_string_from_utf8()) != OK
- or typeof(json.data) != TYPE_DICTIONARY
- or not _valid_owned_data(source.get_file(), json.data)
- ):
- return {"ok": false}
- if DirAccess.make_dir_recursive_absolute(destination.get_base_dir()) != OK:
- return {"ok": false}
- if not _write_bytes(destination, bytes):
- return {"ok": false}
- var copied: PackedByteArray = PortableFileGuard.read_bytes(destination)
- return {
- "ok": (
- PortableFileGuard.hash_bytes(copied)
- == PortableFileGuard.hash_bytes(bytes)
- )
- }
-
-
-static func _copy_verified_owned_file(
- source: String,
- destination: String,
-) -> Dictionary:
- if source.get_extension().to_lower() != "nfsave":
- return _copy_verified_json(source, destination)
- var decoded: Dictionary = ProgressionSaveCodec.read_local_save(source)
- if not bool(decoded.get("ok", false)):
- return {"ok": false}
- var save_data: Dictionary = decoded["data"]
- var version: int = int(save_data.get("save_version", -1))
- if version < 1 or version > PlayerSaveManager.SAVE_VERSION:
- return {"ok": false}
- var bytes: PackedByteArray = PortableFileGuard.read_bytes(source)
- if bytes.is_empty() or not _write_bytes(destination, bytes):
- return {"ok": false}
- return {
- "ok": (
- PortableFileGuard.hash_file(destination)
- == PortableFileGuard.hash_bytes(bytes)
- )
- }
-
-
-static func _valid_owned_data(filename: String, data: Dictionary) -> bool:
- if filename == "player_save.json":
- var version: int = int(data.get("save_version", -1))
- return version >= 1 and version <= PlayerSaveManager.SAVE_VERSION
- if filename == "save_slots.json":
- return (
- int(data.get("format_version", -1)) == PlayerSaveSlotCatalog.FORMAT_VERSION
- and typeof(data.get("active_slot_id")) == TYPE_STRING
- and typeof(data.get("slots")) == TYPE_ARRAY
- )
- return (
- filename not in LEGACY_FILES
- or int(data.get("format_version", -1)) == 1
- )
-
-
-static func _copy_progression_slots(source: String, destination: String) -> bool:
- if DirAccess.make_dir_recursive_absolute(destination) != OK:
- return false
- var access := DirAccess.open(source)
- if access == null:
- return false
- access.list_dir_begin()
- var filename: String = access.get_next()
- while not filename.is_empty():
- if (
- not access.current_is_dir()
- and filename.get_extension().to_lower() == "nfsave"
- and not bool(_copy_verified_owned_file(
- source.path_join(filename),
- destination.path_join(filename),
- ).get("ok", false))
- ):
- access.list_dir_end()
- return false
- filename = access.get_next()
- access.list_dir_end()
- return true
-
-
-static func _copy_bytes(source: String, destination: String) -> bool:
- return _write_bytes(destination, PortableFileGuard.read_bytes(source))
-
-
-static func _copy_tree(source: String, destination: String) -> bool:
- if DirAccess.make_dir_recursive_absolute(destination) != OK:
- return false
- var access: DirAccess = DirAccess.open(source)
- if access == null:
- return false
- access.list_dir_begin()
- var name: String = access.get_next()
- while not name.is_empty():
- var from: String = source.path_join(name)
- var to: String = destination.path_join(name)
- if access.current_is_dir():
- if not _copy_tree(from, to):
- access.list_dir_end()
- return false
- elif not _copy_bytes(from, to):
- access.list_dir_end()
- return false
- name = access.get_next()
- access.list_dir_end()
- return true
-
-
-static func _write_bytes(path: String, bytes: PackedByteArray) -> bool:
- DirAccess.make_dir_recursive_absolute(path.get_base_dir())
- var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
- if file == null:
- return false
- file.store_buffer(bytes)
- file.flush()
- var ok: bool = file.get_error() == OK
- file.close()
- return ok
-
-
-static func _read_json(path: String) -> Dictionary:
- var bytes: PackedByteArray = PortableFileGuard.read_bytes(path)
- var json: JSON = JSON.new()
- return (
- json.data
- if json.parse(bytes.get_string_from_utf8()) == OK
- and typeof(json.data) == TYPE_DICTIONARY
- else {}
- )
-
-
-static func _directory_empty(path: String) -> bool:
- var access: DirAccess = DirAccess.open(path)
- if access == null:
- return true
- access.list_dir_begin()
- var name: String = access.get_next()
- access.list_dir_end()
- return name.is_empty()
-
-
-static func _remove_tree(path: String) -> void:
- var access: DirAccess = DirAccess.open(path)
- if access == null:
- return
- access.list_dir_begin()
- var name: String = access.get_next()
- while not name.is_empty():
- var child: String = path.path_join(name)
- if access.current_is_dir():
- _remove_tree(child)
- else:
- DirAccess.remove_absolute(child)
- name = access.get_next()
- access.list_dir_end()
- DirAccess.remove_absolute(path)
diff --git a/network/portable_data_migration.gd.uid b/network/portable_data_migration.gd.uid
deleted file mode 100644
index 1eb9103..0000000
--- a/network/portable_data_migration.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bjkaght4qyylm
diff --git a/network/portable_file_guard.gd b/network/portable_file_guard.gd
deleted file mode 100644
index cb99ae1..0000000
--- a/network/portable_file_guard.gd
+++ /dev/null
@@ -1,148 +0,0 @@
-class_name PortableFileGuard
-extends RefCounted
-
-const MAX_PORTABLE_FILE_BYTES := 16 * 1024 * 1024
-
-
-static func hash_file(path: String) -> String:
- if not FileAccess.file_exists(path):
- return ""
- var file: FileAccess = FileAccess.open(path, FileAccess.READ)
- if file == null or file.get_length() > MAX_PORTABLE_FILE_BYTES:
- return ""
- var bytes: PackedByteArray = file.get_buffer(file.get_length())
- file.close()
- return hash_bytes(bytes)
-
-
-static func hash_bytes(bytes: PackedByteArray) -> String:
- var context: HashingContext = HashingContext.new()
- var start_error: Error = context.start(HashingContext.HASH_SHA256)
- if start_error != OK:
- return ""
- var update_error: Error = context.update(bytes)
- if update_error != OK:
- return ""
- return context.finish().hex_encode()
-
-
-static func read_bytes(path: String, maximum_bytes: int = MAX_PORTABLE_FILE_BYTES) -> PackedByteArray:
- if not FileAccess.file_exists(path):
- return PackedByteArray()
- var file: FileAccess = FileAccess.open(path, FileAccess.READ)
- if file == null or file.get_length() > maximum_bytes:
- return PackedByteArray()
- var bytes: PackedByteArray = file.get_buffer(file.get_length())
- file.close()
- return bytes
-
-
-static func write_guarded(
- path: String,
- bytes: PackedByteArray,
- expected_hash: String,
- conflict_directory: String,
- device_id: String,
-) -> Dictionary:
- var current_exists: bool = FileAccess.file_exists(path)
- var current_hash: String = hash_file(path) if current_exists else ""
- if current_hash != expected_hash:
- var conflict_path: String = _write_conflict_copy(
- path, bytes, conflict_directory, device_id
- )
- return {
- "ok": false,
- "conflict": true,
- "hash": expected_hash,
- "conflict_path": conflict_path,
- "message": (
- "This file changed on another device.\n"
- + "Your current data was preserved as a conflict copy."
- ),
- }
- if not _ensure_parent(path):
- return {"ok": false, "conflict": false, "hash": expected_hash}
- var temporary: String = path + ".tmp"
- var backup: String = path + ".backup"
- var file: FileAccess = FileAccess.open(temporary, FileAccess.WRITE)
- if file == null:
- return {"ok": false, "conflict": false, "hash": expected_hash}
- file.store_buffer(bytes)
- file.flush()
- var write_error: Error = file.get_error()
- file.close()
- if write_error != OK:
- _remove(temporary)
- return {"ok": false, "conflict": false, "hash": expected_hash}
- _remove(backup)
- if current_exists and not _rename(path, backup):
- _remove(temporary)
- return {"ok": false, "conflict": false, "hash": expected_hash}
- if not _rename(temporary, path):
- if current_exists:
- _rename(backup, path)
- return {"ok": false, "conflict": false, "hash": expected_hash}
- _remove(backup)
- return {
- "ok": true,
- "conflict": false,
- "hash": hash_bytes(bytes),
- "conflict_path": "",
- }
-
-
-static func has_syncthing_conflict(directory: String) -> bool:
- var access: DirAccess = DirAccess.open(directory)
- if access == null:
- return false
- access.list_dir_begin()
- var name: String = access.get_next()
- while not name.is_empty():
- var lower: String = name.to_lower()
- if "sync-conflict" in lower or ".syncthing." in lower:
- access.list_dir_end()
- return true
- name = access.get_next()
- access.list_dir_end()
- return false
-
-
-static func _write_conflict_copy(
- canonical_path: String,
- bytes: PackedByteArray,
- conflict_directory: String,
- device_id: String,
-) -> String:
- if DirAccess.make_dir_recursive_absolute(conflict_directory) != OK:
- return ""
- var filename: String = canonical_path.get_file()
- var safe_device: String = device_id.left(16)
- var timestamp: String = Time.get_datetime_string_from_system().replace(":", "-")
- var destination: String = conflict_directory.path_join(
- "%s.local-%s-%s" % [filename, safe_device, timestamp]
- )
- var file: FileAccess = FileAccess.open(destination, FileAccess.WRITE)
- if file == null:
- return ""
- file.store_buffer(bytes)
- file.flush()
- var ok: bool = file.get_error() == OK
- file.close()
- return destination if ok else ""
-
-
-static func _ensure_parent(path: String) -> bool:
- var parent: String = path.get_base_dir()
- return (
- DirAccess.dir_exists_absolute(parent)
- or DirAccess.make_dir_recursive_absolute(parent) == OK
- )
-
-
-static func _rename(from_path: String, to_path: String) -> bool:
- return DirAccess.rename_absolute(from_path, to_path) == OK
-
-
-static func _remove(path: String) -> void:
- if FileAccess.file_exists(path):
- DirAccess.remove_absolute(path)
diff --git a/network/portable_file_guard.gd.uid b/network/portable_file_guard.gd.uid
deleted file mode 100644
index ea9a4b8..0000000
--- a/network/portable_file_guard.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://biw72bdyctluv
diff --git a/network/saved_server_entry.gd b/network/saved_server_entry.gd
deleted file mode 100644
index 00052bc..0000000
--- a/network/saved_server_entry.gd
+++ /dev/null
@@ -1,88 +0,0 @@
-class_name SavedServerEntry
-extends RefCounted
-
-enum Kind {
- SAVED,
- RECENT,
-}
-
-var kind: Kind = Kind.SAVED
-var entry_id: String = ""
-var display_name: String = ""
-var route_kind: String = "DIRECT"
-var host: String = ""
-var normalized_host: String = ""
-var port: int = EndpointParser.DEFAULT_PORT
-var normalized_endpoint: String = ""
-var favorite: bool = false
-var created_at_unix: int = 0
-var updated_at_unix: int = 0
-var last_success_at_unix: int = 0
-var last_result_code: String = ""
-var last_observed_server_name: String = ""
-var last_observed_protocol_version: int = 0
-var last_observed_player_count: int = 0
-var last_observed_max_players: int = 0
-
-
-static func from_dictionary(
- data: Dictionary,
- entry_kind: Kind,
-) -> SavedServerEntry:
- var entry := SavedServerEntry.new()
- entry.kind = entry_kind
- entry.entry_id = str(data.get("entry_id", ""))
- entry.display_name = str(data.get("display_name", ""))
- entry.route_kind = str(data.get("route_kind", "DIRECT"))
- entry.host = str(data.get("host", ""))
- entry.normalized_host = str(data.get("normalized_host", entry.host))
- entry.port = int(data.get("port", EndpointParser.DEFAULT_PORT))
- entry.normalized_endpoint = str(data.get("normalized_endpoint", ""))
- entry.favorite = bool(data.get("favorite", false))
- entry.created_at_unix = int(data.get("created_at_unix", 0))
- entry.updated_at_unix = int(data.get("updated_at_unix", 0))
- entry.last_success_at_unix = int(data.get("last_success_at_unix", 0))
- entry.last_result_code = str(data.get("last_result_code", ""))
- entry.last_observed_server_name = str(
- data.get("last_observed_server_name", "")
- )
- entry.last_observed_protocol_version = int(
- data.get("last_observed_protocol_version", 0)
- )
- entry.last_observed_player_count = int(
- data.get("last_observed_player_count", 0)
- )
- entry.last_observed_max_players = int(
- data.get("last_observed_max_players", 0)
- )
- return entry
-
-
-func to_dictionary() -> Dictionary:
- return {
- "entry_id": entry_id,
- "display_name": display_name,
- "route_kind": route_kind,
- "host": host,
- "normalized_host": normalized_host,
- "port": port,
- "normalized_endpoint": normalized_endpoint,
- "favorite": favorite,
- "created_at_unix": created_at_unix,
- "updated_at_unix": updated_at_unix,
- "last_success_at_unix": last_success_at_unix,
- "last_result_code": last_result_code,
- "last_observed_server_name": last_observed_server_name,
- "last_observed_protocol_version": last_observed_protocol_version,
- "last_observed_player_count": last_observed_player_count,
- "last_observed_max_players": last_observed_max_players,
- }
-
-
-func get_endpoint() -> ConnectionEndpoint:
- var endpoint_text: String = (
- "[%s]:%d" % [host, port]
- if host.contains(":")
- else "%s:%d" % [host, port]
- )
- return EndpointParser.parse(endpoint_text)
diff --git a/network/saved_server_entry.gd.uid b/network/saved_server_entry.gd.uid
deleted file mode 100644
index 97f6372..0000000
--- a/network/saved_server_entry.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b3guw5fet7lv0
diff --git a/network/saved_server_store.gd b/network/saved_server_store.gd
deleted file mode 100644
index 4e00259..0000000
--- a/network/saved_server_store.gd
+++ /dev/null
@@ -1,468 +0,0 @@
-class_name SavedServerStore
-extends Node
-
-const FORMAT_VERSION: int = 1
-const MAX_SAVED_ENTRIES: int = 100
-const MAX_RECENT_ENTRIES: int = 20
-const MAX_DISPLAY_NAME_LENGTH: int = 80
-
-signal data_changed
-signal recovery_warning_changed(message: String)
-
-var _saved_entries: Array[Dictionary] = []
-var _recent_entries: Array[Dictionary] = []
-var _loaded: bool = false
-var _recovery_warning: String = ""
-var _write_blocked: bool = false
-var _store_path := ""
-var _expected_hash := ""
-var _data_root: PlayerDataRoot
-
-
-func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
- _store_path = path
- _data_root = data_root
- _saved_entries.clear()
- _recent_entries.clear()
- _loaded = false
- _recovery_warning = ""
- _write_blocked = false
- _expected_hash = ""
-
-
-func _temp_path() -> String:
- return _store_path + ".tmp"
-
-
-func _backup_path() -> String:
- return _store_path + ".backup"
-
-
-func get_saved_entries() -> Array[SavedServerEntry]:
- _ensure_loaded()
- var result: Array[SavedServerEntry] = []
- for data: Dictionary in _saved_entries:
- result.append(SavedServerEntry.from_dictionary(
- data, SavedServerEntry.Kind.SAVED
- ))
- result.sort_custom(_sort_saved_entries)
- return result
-
-
-func get_recent_entries() -> Array[SavedServerEntry]:
- _ensure_loaded()
- var result: Array[SavedServerEntry] = []
- for data: Dictionary in _recent_entries:
- result.append(SavedServerEntry.from_dictionary(
- data, SavedServerEntry.Kind.RECENT
- ))
- result.sort_custom(func(a: SavedServerEntry, b: SavedServerEntry) -> bool:
- return a.last_success_at_unix > b.last_success_at_unix
- )
- return result
-
-
-func list_entries() -> Array[Dictionary]:
- _ensure_loaded()
- return _saved_entries.duplicate(true)
-
-
-func get_recovery_warning() -> String:
- _ensure_loaded()
- return _recovery_warning
-
-
-func find_saved_by_endpoint(
- endpoint: ConnectionEndpoint,
-) -> SavedServerEntry:
- _ensure_loaded()
- if endpoint == null or not endpoint.is_valid():
- return null
- for data: Dictionary in _saved_entries:
- if data.get("normalized_endpoint", "") == endpoint.normalized_display:
- return SavedServerEntry.from_dictionary(
- data, SavedServerEntry.Kind.SAVED
- )
- return null
-
-
-func find_by_normalized_endpoint(value: String) -> Dictionary:
- var endpoint := EndpointParser.parse(value)
- var entry: SavedServerEntry = find_saved_by_endpoint(endpoint)
- return entry.to_dictionary() if entry != null else {}
-
-
-func save_or_update_entry(
- display_name: String,
- endpoint: ConnectionEndpoint,
- entry_id: String = "",
-) -> SavedServerEntry:
- _ensure_loaded()
- var clean_name: String = display_name.strip_edges()
- if (
- endpoint == null
- or not endpoint.is_valid()
- or clean_name.is_empty()
- or clean_name.length() > MAX_DISPLAY_NAME_LENGTH
- ):
- return null
- var now: int = int(Time.get_unix_time_from_system())
- var target: Dictionary = {}
- for data: Dictionary in _saved_entries:
- if not entry_id.is_empty() and data.get("entry_id", "") == entry_id:
- target = data
- break
- for data: Dictionary in _saved_entries:
- if (
- data.get("normalized_endpoint", "") == endpoint.normalized_display
- and data != target
- ):
- return null
- if target.is_empty():
- if _saved_entries.size() >= MAX_SAVED_ENTRIES:
- return null
- target = _make_entry(endpoint, SavedServerEntry.Kind.SAVED)
- _saved_entries.append(target)
- target["display_name"] = clean_name
- target["host"] = endpoint.host
- target["normalized_host"] = endpoint.host
- target["port"] = endpoint.port
- target["normalized_endpoint"] = endpoint.normalized_display
- target["updated_at_unix"] = now
- if not _save_atomic():
- return null
- data_changed.emit()
- return SavedServerEntry.from_dictionary(
- target, SavedServerEntry.Kind.SAVED
- )
-
-
-func save_entry(display_name: String, endpoint: ConnectionEndpoint) -> bool:
- return save_or_update_entry(display_name, endpoint) != null
-
-
-func remove_saved_entry(entry_id: String) -> bool:
- _ensure_loaded()
- for index: int in range(_saved_entries.size()):
- if _saved_entries[index].get("entry_id", "") == entry_id:
- _saved_entries.remove_at(index)
- if not _save_atomic():
- return false
- data_changed.emit()
- return true
- return false
-
-
-func remove_entry(entry_id: String) -> bool:
- return remove_saved_entry(entry_id)
-
-
-func set_favorite(entry_id: String, favorite: bool) -> bool:
- _ensure_loaded()
- for data: Dictionary in _saved_entries:
- if data.get("entry_id", "") == entry_id:
- data["favorite"] = favorite
- data["updated_at_unix"] = int(Time.get_unix_time_from_system())
- if not _save_atomic():
- return false
- data_changed.emit()
- return true
- return false
-
-
-func record_successful_connection(
- endpoint: ConnectionEndpoint,
- observed_max_players: int,
- server_name: String = "",
- protocol_version: int = 0,
- player_count: int = 0,
-) -> bool:
- _ensure_loaded()
- if endpoint == null or not endpoint.is_valid():
- return false
- var now: int = int(Time.get_unix_time_from_system())
- for data: Dictionary in _saved_entries:
- if data.get("normalized_endpoint", "") == endpoint.normalized_display:
- _apply_result_metadata(
- data, now, "SUCCESS", server_name, protocol_version,
- player_count, observed_max_players
- )
- var recent: Dictionary = {}
- for data: Dictionary in _recent_entries:
- if data.get("normalized_endpoint", "") == endpoint.normalized_display:
- recent = data
- break
- if recent.is_empty():
- recent = _make_entry(endpoint, SavedServerEntry.Kind.RECENT)
- _recent_entries.append(recent)
- recent["display_name"] = (
- server_name if not server_name.is_empty() else endpoint.normalized_display
- )
- _apply_result_metadata(
- recent, now, "SUCCESS", server_name, protocol_version,
- player_count, observed_max_players
- )
- _recent_entries.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
- return int(a.get("last_success_at_unix", 0)) > int(
- b.get("last_success_at_unix", 0)
- )
- )
- if _recent_entries.size() > MAX_RECENT_ENTRIES:
- _recent_entries.resize(MAX_RECENT_ENTRIES)
- if not _save_atomic():
- return false
- data_changed.emit()
- return true
-
-
-func record_connection_failure(
- endpoint: ConnectionEndpoint,
- result_code: String,
-) -> bool:
- _ensure_loaded()
- if endpoint == null or not endpoint.is_valid():
- return false
- for data: Dictionary in _saved_entries:
- if data.get("normalized_endpoint", "") == endpoint.normalized_display:
- data["last_result_code"] = result_code.left(48)
- data["updated_at_unix"] = int(Time.get_unix_time_from_system())
- if not _save_atomic():
- return false
- data_changed.emit()
- break
- return true
-
-
-func remove_recent_entry(entry_id: String) -> bool:
- _ensure_loaded()
- for index: int in range(_recent_entries.size()):
- if _recent_entries[index].get("entry_id", "") == entry_id:
- _recent_entries.remove_at(index)
- if not _save_atomic():
- return false
- data_changed.emit()
- return true
- return false
-
-
-func clear_recent_entries() -> bool:
- _ensure_loaded()
- if _recent_entries.is_empty():
- return true
- _recent_entries.clear()
- if not _save_atomic():
- return false
- data_changed.emit()
- return true
-
-
-func _ensure_loaded() -> void:
- if _loaded:
- return
- _loaded = true
- _recover_interrupted_write()
- if _store_path.is_empty() or not FileAccess.file_exists(_store_path):
- return
- var data: Dictionary = _read_store(_store_path)
- if data.is_empty() and FileAccess.file_exists(_backup_path()):
- data = _read_store(_backup_path())
- if not data.is_empty():
- _set_warning("Recovered saved servers from the local backup.")
- if data.is_empty():
- if FileAccess.file_exists(_store_path):
- _set_warning(
- "Saved servers could not be read. Direct connection is still available."
- )
- return
- if data.get("format_version") != FORMAT_VERSION:
- _write_blocked = true
- _set_warning(
- "Saved servers use a newer format. Direct connection is still available."
- )
- return
- _load_collection(data.get("saved_entries", data.get("entries", [])), true)
- _load_collection(data.get("recent_entries", []), false)
- _expected_hash = PortableFileGuard.hash_file(_store_path)
-
-
-func _load_collection(raw: Variant, is_saved: bool) -> void:
- if typeof(raw) != TYPE_ARRAY:
- _set_warning("Some saved-server data was unavailable.")
- return
- var target: Array[Dictionary] = (
- _saved_entries if is_saved else _recent_entries
- )
- var seen: Dictionary[String, bool] = {}
- for value: Variant in raw:
- if typeof(value) != TYPE_DICTIONARY:
- _set_warning("Some malformed server entries were skipped.")
- continue
- var validated: Dictionary = _validate_entry(value, is_saved)
- if validated.is_empty():
- _set_warning("Some malformed server entries were skipped.")
- continue
- var identity: String = validated["normalized_endpoint"]
- if seen.has(identity):
- continue
- seen[identity] = true
- target.append(validated)
- if target.size() >= (
- MAX_SAVED_ENTRIES if is_saved else MAX_RECENT_ENTRIES
- ):
- break
-
-
-func _validate_entry(value: Dictionary, is_saved: bool) -> Dictionary:
- if (
- typeof(value.get("entry_id")) != TYPE_STRING
- or str(value.get("entry_id", "")).is_empty()
- or typeof(value.get("host")) != TYPE_STRING
- ):
- return {}
- var stored_host: String = str(value.get("host", ""))
- var stored_port: int = int(value.get("port", 0))
- var endpoint_text: String = (
- "[%s]:%d" % [stored_host, stored_port]
- if stored_host.contains(":")
- else "%s:%d" % [stored_host, stored_port]
- )
- var endpoint := EndpointParser.parse(endpoint_text)
- if not endpoint.is_valid():
- return {}
- var display_name: String = str(
- value.get("display_name", "")
- ).strip_edges()
- if is_saved and (
- display_name.is_empty()
- or display_name.length() > MAX_DISPLAY_NAME_LENGTH
- ):
- return {}
- var result: Dictionary = _make_entry(
- endpoint,
- SavedServerEntry.Kind.SAVED if is_saved else SavedServerEntry.Kind.RECENT
- )
- for key: String in result:
- if value.has(key) and typeof(value[key]) == typeof(result[key]):
- result[key] = value[key]
- result["host"] = endpoint.host
- result["normalized_host"] = endpoint.host
- result["port"] = endpoint.port
- result["normalized_endpoint"] = endpoint.normalized_display
- return result
-
-
-func _make_entry(
- endpoint: ConnectionEndpoint,
- kind: SavedServerEntry.Kind,
-) -> Dictionary:
- var now: int = int(Time.get_unix_time_from_system())
- return {
- "entry_id": Crypto.new().generate_random_bytes(16).hex_encode(),
- "display_name": endpoint.host,
- "route_kind": "DIRECT",
- "host": endpoint.host,
- "normalized_host": endpoint.host,
- "port": endpoint.port,
- "normalized_endpoint": endpoint.normalized_display,
- "favorite": false,
- "created_at_unix": now,
- "updated_at_unix": now,
- "last_success_at_unix": 0,
- "last_result_code": "",
- "last_observed_server_name": "",
- "last_observed_protocol_version": 0,
- "last_observed_player_count": 0,
- "last_observed_max_players": 0,
- "entry_kind": int(kind),
- }
-
-
-func _apply_result_metadata(
- data: Dictionary,
- now: int,
- result_code: String,
- server_name: String,
- protocol_version: int,
- player_count: int,
- max_players: int,
-) -> void:
- data["last_success_at_unix"] = now
- data["updated_at_unix"] = now
- data["last_result_code"] = result_code
- data["last_observed_server_name"] = server_name.left(80)
- data["last_observed_protocol_version"] = maxi(protocol_version, 0)
- data["last_observed_player_count"] = maxi(player_count, 0)
- data["last_observed_max_players"] = maxi(max_players, 0)
-
-
-func _sort_saved_entries(
- a: SavedServerEntry,
- b: SavedServerEntry,
-) -> bool:
- if a.favorite != b.favorite:
- return a.favorite
- if a.last_success_at_unix != b.last_success_at_unix:
- return a.last_success_at_unix > b.last_success_at_unix
- var name_compare: int = a.display_name.naturalnocasecmp_to(b.display_name)
- if name_compare != 0:
- return name_compare < 0
- return a.entry_id < b.entry_id
-
-
-func _save_atomic() -> bool:
- if _write_blocked:
- return false
- var bytes := JSON.stringify({
- "format_version": FORMAT_VERSION,
- "saved_entries": _saved_entries,
- "recent_entries": _recent_entries,
- }, "\t").to_utf8_buffer()
- var result := PortableFileGuard.write_guarded(
- _store_path, bytes, _expected_hash, _data_root.conflict_directory(),
- _data_root.device_id,
- )
- if bool(result.get("conflict", false)):
- _data_root.report_conflict(str(result.get("message", "")), str(result.get("conflict_path", "")))
- if bool(result.get("ok", false)):
- _expected_hash = str(result["hash"])
- return bool(result.get("ok", false))
-
-
-func _read_store(path: String) -> Dictionary:
- var file := FileAccess.open(path, FileAccess.READ)
- if file == null:
- return {}
- var json := JSON.new()
- var error: Error = json.parse(file.get_as_text())
- file.close()
- return json.data if error == OK and typeof(json.data) == TYPE_DICTIONARY else {}
-
-
-func _recover_interrupted_write() -> void:
- if FileAccess.file_exists(_store_path):
- _remove_if_present(_temp_path())
- return
- if FileAccess.file_exists(_backup_path()):
- _rename(_backup_path(), _store_path)
- _set_warning("Recovered saved servers after an interrupted write.")
- _remove_if_present(_temp_path())
-
-
-func _set_warning(message: String) -> void:
- if _recovery_warning.is_empty():
- _recovery_warning = message
- recovery_warning_changed.emit(_recovery_warning)
-
-
-func _rename(from_path: String, to_path: String) -> bool:
- return DirAccess.rename_absolute(
- ProjectSettings.globalize_path(from_path),
- ProjectSettings.globalize_path(to_path)
- ) == OK
-
-
-func _remove_if_present(path: String) -> bool:
- return (
- not FileAccess.file_exists(path)
- or DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) == OK
- )
diff --git a/network/saved_server_store.gd.uid b/network/saved_server_store.gd.uid
deleted file mode 100644
index b07aac2..0000000
--- a/network/saved_server_store.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b7j0er0u1nwy7
diff --git a/network/server_trust_store.gd b/network/server_trust_store.gd
deleted file mode 100644
index 1d4dd29..0000000
--- a/network/server_trust_store.gd
+++ /dev/null
@@ -1,132 +0,0 @@
-class_name ServerTrustStore
-extends Node
-
-const FORMAT_VERSION: int = 1
-enum Verification {
- FIRST_SEEN,
- MATCH,
- CHANGED,
-}
-
-var _records: Dictionary = {}
-var _loaded: bool = false
-var _write_blocked: bool = false
-var _store_path := ""
-var _expected_hash := ""
-var _data_root: PlayerDataRoot
-
-
-func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
- _store_path = path
- _data_root = data_root
- _records.clear()
- _loaded = false
- _write_blocked = false
- _expected_hash = ""
-
-
-func verify(endpoint: ConnectionEndpoint, fingerprint: String) -> Verification:
- _ensure_loaded()
- if endpoint == null or not endpoint.is_valid():
- return Verification.CHANGED
- var record: Dictionary = _records.get(endpoint.normalized_display, {})
- if record.is_empty():
- return Verification.FIRST_SEEN
- return (
- Verification.MATCH
- if record.get("fingerprint") == fingerprint
- else Verification.CHANGED
- )
-
-
-func get_record(endpoint: ConnectionEndpoint) -> Dictionary:
- _ensure_loaded()
- if endpoint == null:
- return {}
- return Dictionary(_records.get(endpoint.normalized_display, {})).duplicate(true)
-
-
-func trust(
- endpoint: ConnectionEndpoint,
- fingerprint: String,
- server_name: String = "",
-) -> bool:
- _ensure_loaded()
- if (
- endpoint == null
- or not endpoint.is_valid()
- or not NetworkIdentityCrypto.valid_fingerprint(fingerprint)
- ):
- return false
- var now := int(Time.get_unix_time_from_system())
- var previous: Dictionary = _records.get(endpoint.normalized_display, {})
- _records[endpoint.normalized_display] = {
- "route_kind": "direct",
- "normalized_host": endpoint.host,
- "port": endpoint.port,
- "normalized_endpoint": endpoint.normalized_display,
- "fingerprint": fingerprint,
- "first_seen_unix": int(previous.get("first_seen_unix", now)),
- "last_seen_unix": now,
- "last_observed_server_name": server_name.left(80),
- "trust_state": "pinned",
- }
- return _save()
-
-
-func touch(endpoint: ConnectionEndpoint) -> void:
- _ensure_loaded()
- if endpoint == null:
- return
- var record: Dictionary = _records.get(endpoint.normalized_display, {})
- if record.is_empty():
- return
- record["last_seen_unix"] = int(Time.get_unix_time_from_system())
- _save()
-
-
-func _ensure_loaded() -> void:
- if _loaded:
- return
- _loaded = true
- if _store_path.is_empty() or not FileAccess.file_exists(_store_path):
- return
- var file := FileAccess.open(_store_path, FileAccess.READ)
- if file == null:
- return
- var json := JSON.new()
- if json.parse(file.get_as_text()) != OK or typeof(json.data) != TYPE_DICTIONARY:
- return
- var data: Dictionary = json.data
- if data.get("format_version") != FORMAT_VERSION:
- _write_blocked = true
- return
- if typeof(data.get("records")) != TYPE_ARRAY:
- return
- for value: Variant in data["records"]:
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var record: Dictionary = value
- var endpoint := str(record.get("normalized_endpoint", ""))
- var fingerprint := str(record.get("fingerprint", ""))
- if not endpoint.is_empty() and NetworkIdentityCrypto.valid_fingerprint(fingerprint):
- _records[endpoint] = record.duplicate(true)
- _expected_hash = PortableFileGuard.hash_file(_store_path)
-
-
-func _save() -> bool:
- if _write_blocked:
- return false
- var bytes := JSON.stringify({
- "format_version": FORMAT_VERSION,
- "records": _records.values(),
- }, "\t").to_utf8_buffer()
- var result := PortableFileGuard.write_guarded(
- _store_path, bytes, _expected_hash, _data_root.conflict_directory(),
- _data_root.device_id,
- )
- if bool(result.get("conflict", false)):
- _data_root.report_conflict(str(result.get("message", "")), str(result.get("conflict_path", "")))
- if bool(result.get("ok", false)):
- _expected_hash = str(result["hash"])
- return bool(result.get("ok", false))
diff --git a/network/server_trust_store.gd.uid b/network/server_trust_store.gd.uid
deleted file mode 100644
index 71ce1df..0000000
--- a/network/server_trust_store.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cp2kam2btjkb0
diff --git a/player/animalese_voice_profiles.gd b/player/animalese_voice_profiles.gd
deleted file mode 100644
index ded0898..0000000
--- a/player/animalese_voice_profiles.gd
+++ /dev/null
@@ -1,124 +0,0 @@
-class_name AnimaleseVoiceProfiles
-extends RefCounted
-
-const DEFAULT_ID: String = "natural"
-const DEFAULT_SAMPLE_SET_ID: String = "kat"
-const DEFAULT_SPEED_ID: String = "normal"
-const DEFAULT_CALL_ID: String = "meow"
-const CALL_AUDIO_DIRECTORY: String = "res://sound/dialogue/calls"
-const SAMPLE_SET_OPTIONS: Array[Dictionary] = [
- {
- "id": "kat",
- "label": "kat",
- "directory": "res://sound/dialogue/animalese/alphanumeric",
- },
- {
- "id": "robot",
- "label": "robot",
- "directory": "res://sound/dialogue/animalese/robot",
- },
- {
- "id": "kim",
- "label": "kim",
- "directory": "res://sound/dialogue/animalese/kim",
- },
-]
-const OPTIONS: Array[Dictionary] = [
- {"id": "tiny", "label": "tiny", "pitch": 1.24},
- {"id": "bright", "label": "bright", "pitch": 1.10},
- {"id": "natural", "label": "natural", "pitch": 1.0},
- {"id": "mellow", "label": "mellow", "pitch": 0.89},
- {"id": "deep", "label": "deep", "pitch": 0.77},
-]
-
-
-static func is_valid_sample_set(sample_set_id: String) -> bool:
- for option: Dictionary in SAMPLE_SET_OPTIONS:
- if str(option.get("id", "")) == sample_set_id:
- return true
- return false
-
-
-static func sanitized_sample_set_id(sample_set_id: String) -> String:
- return (
- sample_set_id
- if is_valid_sample_set(sample_set_id)
- else DEFAULT_SAMPLE_SET_ID
- )
-
-
-static func sample_directory_for(sample_set_id: String) -> String:
- var resolved_id := sanitized_sample_set_id(sample_set_id)
- for option: Dictionary in SAMPLE_SET_OPTIONS:
- if str(option.get("id", "")) == resolved_id:
- return str(option.get("directory", ""))
- return ""
-
-
-const SPEED_OPTIONS: Array[Dictionary] = [
- {"id": "slow", "label": "slow", "characters_per_second": 20.0},
- {"id": "relaxed", "label": "relaxed", "characters_per_second": 24.0},
- {"id": "normal", "label": "normal", "characters_per_second": 28.0},
- {"id": "quick", "label": "quick", "characters_per_second": 34.0},
- {"id": "rapid", "label": "rapid", "characters_per_second": 40.0},
-]
-const CALL_OPTIONS: Array[Dictionary] = [
- {"id": "meow", "label": "meow"},
- {"id": "bark", "label": "bark"},
-]
-
-
-static func is_valid(voice_id: String) -> bool:
- for option: Dictionary in OPTIONS:
- if str(option.get("id", "")) == voice_id:
- return true
- return false
-
-
-static func sanitized_id(voice_id: String) -> String:
- return voice_id if is_valid(voice_id) else DEFAULT_ID
-
-
-static func pitch_for(voice_id: String) -> float:
- var resolved_id := sanitized_id(voice_id)
- for option: Dictionary in OPTIONS:
- if str(option.get("id", "")) == resolved_id:
- return float(option.get("pitch", 1.0))
- return 1.0
-
-
-static func is_valid_speed(speed_id: String) -> bool:
- for option: Dictionary in SPEED_OPTIONS:
- if str(option.get("id", "")) == speed_id:
- return true
- return false
-
-
-static func sanitized_speed_id(speed_id: String) -> String:
- return speed_id if is_valid_speed(speed_id) else DEFAULT_SPEED_ID
-
-
-static func speed_for(speed_id: String) -> float:
- var resolved_id := sanitized_speed_id(speed_id)
- for option: Dictionary in SPEED_OPTIONS:
- if str(option.get("id", "")) == resolved_id:
- return float(option.get("characters_per_second", 28.0))
- return 28.0
-
-
-static func is_valid_call(call_id: String) -> bool:
- for option: Dictionary in CALL_OPTIONS:
- if str(option.get("id", "")) == call_id:
- return true
- return false
-
-
-static func sanitized_call_id(call_id: String) -> String:
- return call_id if is_valid_call(call_id) else DEFAULT_CALL_ID
-
-
-static func call_audio_path(call_id: String) -> String:
- return "%s/%s.wav" % [
- CALL_AUDIO_DIRECTORY,
- sanitized_call_id(call_id),
- ]
diff --git a/player/animalese_voice_profiles.gd.uid b/player/animalese_voice_profiles.gd.uid
deleted file mode 100644
index 1817007..0000000
--- a/player/animalese_voice_profiles.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dlut32tq2v66j
diff --git a/player/fishing_rod_attachment.tscn b/player/fishing_rod_attachment.tscn
deleted file mode 100644
index d803e28..0000000
--- a/player/fishing_rod_attachment.tscn
+++ /dev/null
@@ -1,29 +0,0 @@
-[gd_scene load_steps=4 format=3]
-
-[ext_resource type="Script" path="res://player/tool_socket_attachment.gd" id="1_socket"]
-
-[sub_resource type="CylinderMesh" id="FishingRodMesh"]
-top_radius = 0.025
-bottom_radius = 0.045
-height = 1.2
-
-[sub_resource type="StandardMaterial3D" id="FishingRodMaterial"]
-albedo_color = Color(0.22, 0.12, 0.06, 1)
-roughness = 0.8
-
-[node name="FishingRodAttachment" type="BoneAttachment3D"]
-bone_name = "rod_socket"
-script = ExtResource("1_socket")
-aligned_mount_path = NodePath("FishingRod")
-
-[node name="FishingRod" type="Node3D" parent="."]
-
-[node name="FallbackRodMesh" type="MeshInstance3D" parent="FishingRod"]
-position = Vector3(0, 0.5, 0)
-mesh = SubResource("FishingRodMesh")
-material_override = SubResource("FishingRodMaterial")
-
-[node name="ModelMount" type="Node3D" parent="FishingRod"]
-
-[node name="FishingRodTip" type="Marker3D" parent="FishingRod"]
-position = Vector3(0, 1.1, 0)
diff --git a/player/fur_pattern.gdshader b/player/fur_pattern.gdshader
deleted file mode 100644
index aad013f..0000000
--- a/player/fur_pattern.gdshader
+++ /dev/null
@@ -1,27 +0,0 @@
-shader_type spatial;
-render_mode unshaded, cull_back;
-
-// Pattern PNGs are data maps rather than colored artwork. Black selects the
-// base color and the red, green, and blue channels select the three authored
-// accent colors. Alpha is intentionally ignored so artists may leave unused
-// UV space transparent.
-uniform sampler2D pattern_texture : filter_nearest, repeat_disable;
-uniform vec4 base_color : source_color = vec4(1.0);
-uniform vec4 red_zone_color : source_color = vec4(0.9, 0.8, 0.6, 1.0);
-uniform vec4 green_zone_color : source_color = vec4(0.4, 0.2, 0.1, 1.0);
-uniform vec4 blue_zone_color : source_color = vec4(0.7, 0.2, 0.15, 1.0);
-
-void fragment() {
- vec3 zone_weights = max(texture(pattern_texture, UV).rgb, vec3(0.0));
- float zone_total = zone_weights.r + zone_weights.g + zone_weights.b;
- // Filtering between two authored zone colors can produce a sum near one.
- // Normalize only an overfull sample, retaining smooth black-to-zone edges.
- zone_weights /= max(zone_total, 1.0);
- float base_weight = 1.0 - min(zone_total, 1.0);
- ALBEDO = (
- base_color.rgb * base_weight
- + red_zone_color.rgb * zone_weights.r
- + green_zone_color.rgb * zone_weights.g
- + blue_zone_color.rgb * zone_weights.b
- );
-}
diff --git a/player/fur_pattern.gdshader.uid b/player/fur_pattern.gdshader.uid
deleted file mode 100644
index e16692e..0000000
--- a/player/fur_pattern.gdshader.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://eoeegcj00qy8
diff --git a/player/held_item_attachment.tscn b/player/held_item_attachment.tscn
deleted file mode 100644
index 66b3b7c..0000000
--- a/player/held_item_attachment.tscn
+++ /dev/null
@@ -1,44 +0,0 @@
-[gd_scene format=3]
-
-[node name="HeldItemAttachment" type="BoneAttachment3D"]
-bone_name = "hand.R"
-
-[node name="HeldFishDisplay" type="Node3D" parent="."]
-unique_name_in_owner = true
-position = Vector3(0, 0.08, 0.04)
-top_level = true
-visible = false
-
-[node name="HeldFishSprite" type="Sprite3D" parent="HeldFishDisplay"]
-unique_name_in_owner = true
-pixel_size = 0.012
-shaded = false
-double_sided = true
-texture_filter = 0
-
-[node name="HeldArtKitDisplay" type="Node3D" parent="."]
-unique_name_in_owner = true
-position = Vector3(0, 0.08, 0.04)
-visible = false
-
-[node name="HeldArtKitSprite" type="Sprite3D" parent="HeldArtKitDisplay"]
-unique_name_in_owner = true
-rotation = Vector3(0, 0, 4.71239)
-pixel_size = 0.008
-flip_h = true
-shaded = false
-double_sided = true
-texture_filter = 0
-
-[node name="CatchDisplay" type="Node3D" parent="."]
-unique_name_in_owner = true
-position = Vector3(0, 0.08, 0.04)
-top_level = true
-visible = false
-
-[node name="CatchSprite" type="Sprite3D" parent="CatchDisplay"]
-unique_name_in_owner = true
-pixel_size = 0.012
-shaded = false
-double_sided = true
-texture_filter = 0
diff --git a/player/materials/player_blob_shadow.gdshader b/player/materials/player_blob_shadow.gdshader
deleted file mode 100644
index c1a5089..0000000
--- a/player/materials/player_blob_shadow.gdshader
+++ /dev/null
@@ -1,13 +0,0 @@
-shader_type spatial;
-render_mode blend_mix, depth_draw_never, cull_disabled, unshaded;
-
-uniform vec4 shadow_color : source_color = vec4(0.035, 0.105, 0.125, 0.18);
-uniform float inner_softness : hint_range(0.0, 0.8, 0.01) = 0.05;
-
-void fragment() {
- vec2 centered_uv = (UV - vec2(0.5)) * 2.0;
- float radial_distance = dot(centered_uv, centered_uv);
- float falloff = 1.0 - smoothstep(inner_softness, 1.0, radial_distance);
- ALBEDO = shadow_color.rgb;
- ALPHA = shadow_color.a * falloff;
-}
diff --git a/player/materials/player_blob_shadow.gdshader.uid b/player/materials/player_blob_shadow.gdshader.uid
deleted file mode 100644
index d7cd853..0000000
--- a/player/materials/player_blob_shadow.gdshader.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cd16fw0yye48e
diff --git a/player/materials/player_blob_shadow.tres b/player/materials/player_blob_shadow.tres
deleted file mode 100644
index 249429f..0000000
--- a/player/materials/player_blob_shadow.tres
+++ /dev/null
@@ -1,7 +0,0 @@
-[gd_resource type="ShaderMaterial" load_steps=2 format=3]
-
-[ext_resource type="Shader" path="res://player/materials/player_blob_shadow.gdshader" id="1_shader"]
-
-[resource]
-render_priority = 1
-shader = ExtResource("1_shader")
diff --git a/player/net_attachment.gd b/player/net_attachment.gd
deleted file mode 100644
index 3c7843a..0000000
--- a/player/net_attachment.gd
+++ /dev/null
@@ -1,88 +0,0 @@
-extends "res://player/tool_socket_attachment.gd"
-
-const TextureSamplingPolicyScript := preload(
- "res://main/texture_sampling_policy.gd"
-)
-const SPRING_ROOT_BONE: StringName = &"net_mid"
-const SPRING_END_BONE: StringName = &"net_tip"
-const IMPACT_BONE: StringName = &"net_rim"
-
-@export_range(0.0, 10.0, 0.05) var spring_stiffness: float = 1.8
-@export_range(0.0, 1.0, 0.01) var spring_drag: float = 0.68
-@export_range(0.0, 10.0, 0.05) var spring_gravity: float = 0.3
-@export_range(0.01, 0.5, 0.01) var spring_end_length: float = 0.1
-
-@onready var _catching_net: Node3D = %CatchingNet
-@onready var _net_model: Node3D = %NetModel
-
-var _spring: SpringBoneSimulator3D
-var _spring_was_visible: bool = false
-var _net_skeleton: Skeleton3D
-
-
-func _ready() -> void:
- super._ready()
- TextureSamplingPolicyScript.enforce_subtree(_net_model)
- _configure_spring()
- _sync_spring_activity()
-
-
-func _process(_delta: float) -> void:
- _sync_spring_activity()
-
-
-func _configure_spring() -> void:
- _net_skeleton = _net_model.find_child(
- "Skeleton3D", true, false
- ) as Skeleton3D
- if _net_skeleton == null:
- push_error("Imported net model is missing its Skeleton3D.")
- return
- if _net_skeleton.find_bone(SPRING_ROOT_BONE) < 0:
- push_error("Imported net model is missing bone %s." % SPRING_ROOT_BONE)
- return
- if _net_skeleton.find_bone(SPRING_END_BONE) < 0:
- push_error("Imported net model is missing bone %s." % SPRING_END_BONE)
- return
- if _net_skeleton.find_bone(IMPACT_BONE) < 0:
- push_error("Imported net model is missing bone %s." % IMPACT_BONE)
- return
-
- _spring = SpringBoneSimulator3D.new()
- _spring.name = "NetSpring"
- _spring.set_setting_count(1)
- _spring.set_root_bone_name(0, SPRING_ROOT_BONE)
- _spring.set_end_bone_name(0, SPRING_END_BONE)
- _spring.set_extend_end_bone(0, true)
- _spring.set_end_bone_direction(
- 0, SkeletonModifier3D.BONE_DIRECTION_FROM_PARENT
- )
- _spring.set_end_bone_length(0, spring_end_length)
- _spring.set_stiffness(0, spring_stiffness)
- _spring.set_drag(0, spring_drag)
- _spring.set_gravity(0, spring_gravity)
- _spring.set_gravity_direction(0, Vector3.DOWN)
- _net_skeleton.add_child(_spring)
-
-
-func get_impact_world_position() -> Vector3:
- if _net_skeleton == null:
- return _catching_net.global_position
- var impact_bone_index := _net_skeleton.find_bone(IMPACT_BONE)
- if impact_bone_index < 0:
- return _catching_net.global_position
- return _net_skeleton.global_transform * (
- _net_skeleton.get_bone_global_pose(impact_bone_index).origin
- )
-
-
-func _sync_spring_activity() -> void:
- if _spring == null:
- return
- var is_visible := _catching_net.is_visible_in_tree()
- if is_visible and not _spring_was_visible:
- _spring.active = true
- _spring.reset()
- elif not is_visible:
- _spring.active = false
- _spring_was_visible = is_visible
diff --git a/player/net_attachment.gd.uid b/player/net_attachment.gd.uid
deleted file mode 100644
index 9bf2fd6..0000000
--- a/player/net_attachment.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cw5vc02hoelgm
diff --git a/player/net_attachment.tscn b/player/net_attachment.tscn
deleted file mode 100644
index da49a88..0000000
--- a/player/net_attachment.tscn
+++ /dev/null
@@ -1,20 +0,0 @@
-[gd_scene load_steps=3 format=3]
-
-[ext_resource type="Script" path="res://player/net_attachment.gd" id="1_script"]
-[ext_resource type="PackedScene" path="res://art/exported/items/equipment/net.glb" id="2_model"]
-
-[node name="NetAttachment" type="BoneAttachment3D"]
-bone_name = "rod_socket"
-script = ExtResource("1_script")
-aligned_mount_path = NodePath("CatchingNet")
-
-[node name="CatchingNet" type="Node3D" parent="." unique_id=947004727]
-unique_name_in_owner = true
-visible = false
-
-[node name="ModelMount" type="Node3D" parent="CatchingNet"]
-position = Vector3(-0.000217, -0.098636, -0.020218)
-rotation = Vector3(0, 3.1415927, 0)
-
-[node name="NetModel" parent="CatchingNet/ModelMount" unique_id=1627767240 instance=ExtResource("2_model")]
-unique_name_in_owner = true
diff --git a/player/player.gd b/player/player.gd
index e36f92b..705cb1c 100644
--- a/player/player.gd
+++ b/player/player.gd
@@ -4,11 +4,9 @@ extends CharacterBody3D
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
const CollectionLogType = preload("res://collection/collection_log.gd")
const FishCatchType = preload("res://fish/fish_catch.gd")
-const FishDataType = preload("res://fish/fish_data.gd")
const FishSaleServiceType = preload("res://economy/fish_sale_service.gd")
const PlayerWalletType = preload("res://economy/player_wallet.gd")
const PlayerBagType = preload("res://inventory/player_bag.gd")
-const ItemDataType = preload("res://items/item_data.gd")
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
const PlayerFishingUpgradesType = preload(
"res://progression/player_fishing_upgrades.gd"
@@ -19,362 +17,6 @@ const PlayerItemEffectsType = preload(
const PlayerCoolerCapacityType = preload(
"res://progression/player_cooler_capacity.gd"
)
-const PlayerInventoryLayoutType = preload(
- "res://inventory/player_inventory_layout.gd"
-)
-const PlayerArtUnlocksType = preload(
- "res://progression/player_art_unlocks.gd"
-)
-const PlayerExperienceType = preload(
- "res://progression/player_experience.gd"
-)
-const ControllerMappingManagerType = preload(
- "res://settings/controller_mapping_manager.gd"
-)
-const FishingRodAttachmentScene = preload(
- "res://player/fishing_rod_attachment.tscn"
-)
-const NetAttachmentScene = preload("res://player/net_attachment.tscn")
-const NetAttachmentType = preload("res://player/net_attachment.gd")
-const ShovelAttachmentScene = preload("res://player/shovel_attachment.tscn")
-const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
-const HeldItemAttachmentScene = preload(
- "res://player/held_item_attachment.tscn"
-)
-const SprintDustTrailType = preload("res://player/sprint_dust_trail.gd")
-const VoiceProfilesType = preload(
- "res://player/animalese_voice_profiles.gd"
-)
-
-signal retract_visual_finished
-
-const CHARACTER_IDLE_ANIMATION: StringName = &"idle"
-const CHARACTER_IDLE_SHOW_ANIMATION: StringName = &"idle_show"
-const CHARACTER_IDLE_SIT_ANIMATION: StringName = &"idle_sit"
-const CHARACTER_IDLE_SIT_SHOW_ANIMATION: StringName = &"idle_sit_show"
-const CHARACTER_WALKING_ANIMATION: StringName = &"walking"
-const CHARACTER_WALKING_SHOW_ANIMATION: StringName = &"walking_show"
-const CHARACTER_RUNNING_ANIMATION: StringName = &"running"
-const CHARACTER_RUNNING_SHOW_ANIMATION: StringName = &"running_show"
-const CHARACTER_IDLE_SNEAK_ANIMATION: StringName = &"idle_sneak"
-const CHARACTER_SNEAKING_ANIMATION: StringName = &"sneaking"
-const CHARACTER_CASTING_ANIMATION: StringName = &"casting"
-const CHARACTER_CASTING_SIT_ANIMATION: StringName = &"casting_sit"
-const CHARACTER_RELEASE_ANIMATION: StringName = &"release"
-const CHARACTER_RELEASE_SIT_ANIMATION: StringName = &"release_sit"
-const CHARACTER_RETRACT_ANIMATION: StringName = &"retract"
-const CHARACTER_RETRACT_SIT_ANIMATION: StringName = &"retract_sit"
-const CHARACTER_POCKET_IDLE_IDLE_ANIMATION: StringName = &"pocket_idle_idle"
-const CHARACTER_POCKET_IDLE_SHOW_ANIMATION: StringName = &"pocket_idle_show"
-const CHARACTER_POCKET_SHOW_SHOW_ANIMATION: StringName = &"pocket_show_show"
-const CHARACTER_POCKET_SHOW_IDLE_ANIMATION: StringName = &"pocket_show_idle"
-const CHARACTER_POCKET_SIT_IDLE_IDLE_ANIMATION: StringName = &"pocket_sit_idle_idle"
-const CHARACTER_POCKET_SIT_IDLE_SHOW_ANIMATION: StringName = &"pocket_sit_idle_show"
-const CHARACTER_POCKET_SIT_SHOW_SHOW_ANIMATION: StringName = &"pocket_sit_show_show"
-const CHARACTER_POCKET_SIT_SHOW_IDLE_ANIMATION: StringName = &"pocket_sit_show_idle"
-const CHARACTER_POCKET_WALKING_IDLE_IDLE_ANIMATION: StringName = &"pocket_walking_idle_idle"
-const CHARACTER_POCKET_WALKING_IDLE_SHOW_ANIMATION: StringName = &"pocket_walking_idle_show"
-const CHARACTER_POCKET_WALKING_SHOW_SHOW_ANIMATION: StringName = &"pocket_walking_show_show"
-const CHARACTER_POCKET_WALKING_SHOW_IDLE_ANIMATION: StringName = &"pocket_walking_show_idle"
-const CHARACTER_FISHING_ANIMATION: StringName = &"fishing"
-const CHARACTER_FISHING_SIT_ANIMATION: StringName = &"fishing_sit"
-const CHARACTER_FIGHTING_ANIMATION: StringName = &"fighting"
-const CHARACTER_FIGHTING_SIT_ANIMATION: StringName = &"fighting_sit"
-const CHARACTER_NET_DRAW_ANIMATION: StringName = &"draw"
-const CHARACTER_NET_STRIKE_ANIMATION: StringName = &"strike"
-const CATCH_PRESENTATION_REFERENCE_LONG_SIDE_PX: float = 1280.0
-const CATCH_PRESENTATION_MIN_TEXTURE_SCALE: float = 0.1
-const CATCH_PRESENTATION_MAX_TEXTURE_SCALE: float = 20.0
-const CATCH_SHOWCASE_INPUT_OWNER: StringName = &"catch_showcase"
-# Add future networked emote animation IDs here. The protocol accepts unknown
-# safe IDs so newer clients can extend it, but Player only presents actions
-# explicitly approved by this catalog.
-const NETWORK_ANIMATION_ACTION_IDS: Array[StringName] = [
- CHARACTER_CASTING_ANIMATION,
- CHARACTER_NET_DRAW_ANIMATION,
- CHARACTER_NET_STRIKE_ANIMATION,
-]
-
-enum FishingVisualPhase {
- NONE,
- CASTING,
- RELEASE,
- RETRACT,
- FISHING,
-}
-
-enum PocketVisualTarget {
- NONE,
- IDLE_ITEM,
- HELD_FISH,
- ART_KIT,
- CATCH_SHOWCASE,
-}
-
-enum LocomotionState {
- IDLE,
- WALKING,
- RUNNING,
- SNEAKING,
-}
-const FIGHTING_EYES_ID: String = "alligator_eyes"
-const BLINK_EYES_ID: String = "closed"
-const BLINK_INTERVAL_SECONDS := Vector2(2.8, 7.5)
-const BLINK_DURATION_SECONDS: float = 0.11
-const CHARACTER_CALL_MOUTH_ID: String = "open_ah"
-const CHARACTER_CALL_MOUTH_DURATION_SECONDS: float = 0.16
-const SPEECH_MOUTH_IDS: Array[String] = [
- "open_ah",
- "open_oh",
- "open_small",
- "open_smile",
-]
-const SPEECH_MOUTH_FRAME_SECONDS: float = 0.09
-const BASE_REEL_SPEED: float = 0.16
-const LANDING_DUST_MIN_FALL_SPEED: float = 2.5
-const NETWORK_EXTRAPOLATION_LIMIT_SECONDS: float = 0.25
-const NETWORK_EXPECTED_SNAPSHOT_INTERVAL_SECONDS: float = 1.0 / 30.0
-const NETWORK_SNAPSHOT_JITTER_LIMIT_SECONDS: float = 0.1
-const NETWORK_SNAPSHOT_JITTER_WEIGHT: float = 0.15
-const NETWORK_REMOTE_SMOOTHING_RATE: float = 12.0
-const NETWORK_REMOTE_JITTER_SMOOTHING_RATE: float = 6.0
-const NETWORK_INPUT_STALE_TIMEOUT_SECONDS: float = 0.25
-const NETWORK_INPUT_STALE_TIMEOUT_MAX_SECONDS: float = 1.25
-const NETWORK_INPUT_STALE_RTT_MULTIPLIER: float = 1.5
-const NETWORK_MOVEMENT_HISTORY_SECONDS: float = 1.25
-const NETWORK_MAX_LAG_COMPENSATION_SECONDS: float = 0.75
-const LOCAL_PREDICTION_EXTRAPOLATION_LIMIT_SECONDS: float = 0.25
-const LOCAL_PREDICTION_FALLBACK_TRANSIT_RATIO: float = 0.5
-const LOCAL_PREDICTION_CORRECTION_THRESHOLD: float = 1.5
-const LOCAL_PREDICTION_SNAP_DISTANCE: float = 6.0
-const LOCAL_PREDICTION_CORRECTION_DELAY_SECONDS: float = 0.35
-const LOCAL_PREDICTION_MIN_CORRECTION_AUDITS: int = 3
-const LOCAL_PREDICTION_SOFT_CORRECTION_RATE: float = 2.5
-const LOCAL_PREDICTION_MAX_SOFT_CORRECTION_STEP: float = 0.15
-const LOCAL_PREDICTION_MAX_AUDIT_DELTA_SECONDS: float = 0.15
-const LOCAL_RECONCILIATION_PRESENTATION_RECENTER_RATE: float = 5.0
-# The target Android handheld exposes its physical right trigger through
-# Godot's left-trigger axis. Keep the role named here so the platform mapping
-# remains isolated from camera behavior.
-const CONTROLLER_ZOOM_TRIGGER_AXIS: JoyAxis = JOY_AXIS_TRIGGER_LEFT
-
-var appearance_snapshot: Dictionary = (
- CharacterCustomizationCatalog.default_snapshot()
-)
-var animalese_voice_id: String = VoiceProfilesType.DEFAULT_ID
-var animalese_sample_set_id: String = VoiceProfilesType.DEFAULT_SAMPLE_SET_ID
-var active_bait_id: StringName = StringName()
-var active_lure_id: StringName = StringName()
-signal active_bait_changed(item_id: StringName)
-signal active_lure_changed(item_id: StringName)
-
-
-func equip_bait(item: ItemDataType) -> bool:
- if item == null or not item.is_bait() or bag == null:
- return false
- if not bag.owns_item(item.item_id):
- return false
- active_bait_id = item.item_id
- active_bait_changed.emit(active_bait_id)
- return true
-
-
-func unequip_bait() -> void:
- if active_bait_id.is_empty():
- return
- active_bait_id = StringName()
- active_bait_changed.emit(active_bait_id)
-
-
-func equip_lure(item: ItemDataType) -> bool:
- if item == null or not item.is_lure() or bag == null:
- return false
- if not bag.owns_item(item.item_id):
- return false
- active_lure_id = item.item_id
- active_lure_changed.emit(active_lure_id)
- return true
-
-
-func unequip_lure() -> void:
- if active_lure_id.is_empty():
- return
- active_lure_id = StringName()
- active_lure_changed.emit(active_lure_id)
-
-
-func apply_appearance_snapshot(snapshot: Dictionary) -> void:
- if CharacterCustomizationCatalog.validate_snapshot(snapshot):
- appearance_snapshot = snapshot.duplicate(true)
- _apply_presented_appearance()
-
-
-func get_character_visual_scale() -> float:
- return CharacterCustomizationCatalog.character_scale(
- appearance_snapshot.get(
- CharacterCustomizationCatalog.SCALE_CATEGORY_ID,
- CharacterCustomizationCatalog.DEFAULT_CHARACTER_SCALE,
- )
- )
-
-
-func set_fighting_visual(active: bool) -> void:
- if _fighting_visual_active == active:
- return
- _fighting_visual_active = active
- if active:
- _blink_visual_active = false
- _schedule_next_blink()
- _apply_presented_appearance()
- _character_animation_name = &""
- _update_character_animation()
-
-
-func play_character_call_visual() -> void:
- _character_call_visual_generation += 1
- var generation: int = _character_call_visual_generation
- _character_call_mouth_active = true
- _apply_presented_appearance()
- await get_tree().create_timer(
- CHARACTER_CALL_MOUTH_DURATION_SECONDS
- ).timeout
- if generation != _character_call_visual_generation:
- return
- _character_call_mouth_active = false
- _apply_presented_appearance()
-
-
-func play_speech_visual(duration_seconds: float) -> void:
- stop_speech_visual()
- if duration_seconds <= 0.0 or SPEECH_MOUTH_IDS.is_empty():
- return
- var generation: int = _speech_visual_generation
- var elapsed: float = 0.0
- var frame_index: int = 0
- _speech_mouth_active = true
- while elapsed < duration_seconds:
- _speech_mouth_id = SPEECH_MOUTH_IDS[
- frame_index % SPEECH_MOUTH_IDS.size()
- ]
- _apply_presented_appearance()
- var frame_duration := minf(
- SPEECH_MOUTH_FRAME_SECONDS,
- duration_seconds - elapsed,
- )
- await get_tree().create_timer(frame_duration).timeout
- if generation != _speech_visual_generation:
- return
- elapsed += frame_duration
- frame_index += 1
- _speech_mouth_active = false
- _speech_mouth_id = ""
- _apply_presented_appearance()
-
-
-func stop_speech_visual() -> void:
- _speech_visual_generation += 1
- if not _speech_mouth_active and _speech_mouth_id.is_empty():
- return
- _speech_mouth_active = false
- _speech_mouth_id = ""
- _apply_presented_appearance()
-
-
-func set_fishing_visual(active: bool) -> void:
- if (
- active
- and _fishing_visual_phase == FishingVisualPhase.RELEASE
- and _character_animation_name in [
- CHARACTER_RELEASE_ANIMATION,
- CHARACTER_RELEASE_SIT_ANIMATION,
- ]
- and _character_animation_player != null
- and _character_animation_player.is_playing()
- ):
- _fishing_after_release_pending = true
- return
- _set_fishing_visual_phase(
- FishingVisualPhase.FISHING if active else FishingVisualPhase.NONE
- )
-
-
-func set_casting_visual() -> void:
- _set_fishing_visual_phase(FishingVisualPhase.CASTING)
-
-
-func set_release_visual() -> void:
- _set_fishing_visual_phase(FishingVisualPhase.RELEASE)
-
-
-func set_retract_visual() -> void:
- _retract_animation_completed = false
- _set_fishing_visual_phase(FishingVisualPhase.RETRACT)
- if (
- _character_animation_player == null
- or _character_animation_name not in [
- CHARACTER_RETRACT_ANIMATION,
- CHARACTER_RETRACT_SIT_ANIMATION,
- ]
- or not _character_animation_player.is_playing()
- ):
- _complete_retract_animation()
-
-
-func _set_fishing_visual_phase(phase: FishingVisualPhase) -> void:
- if phase != FishingVisualPhase.NONE and _pocket_visual_active:
- _complete_pocket_visual()
- if _fishing_visual_phase == phase:
- return
- if (
- _fishing_visual_phase == FishingVisualPhase.RETRACT
- and phase != FishingVisualPhase.RETRACT
- ):
- _complete_retract_animation()
- if phase != FishingVisualPhase.RELEASE:
- _fishing_after_release_pending = false
- _fishing_visual_phase = phase
- _fishing_visual_active = phase != FishingVisualPhase.NONE
- _character_animation_name = &""
- _update_character_animation()
-
-
-func _apply_presented_appearance() -> void:
- if not is_node_ready() or _visuals == null:
- return
- var presented_appearance := appearance_snapshot
- if _blink_visual_active and not _fighting_visual_active:
- presented_appearance = appearance_snapshot.duplicate(true)
- presented_appearance["eyes"] = BLINK_EYES_ID
- if _fighting_visual_active:
- presented_appearance = appearance_snapshot.duplicate(true)
- presented_appearance["eyes"] = FIGHTING_EYES_ID
- if _speech_mouth_active and not _speech_mouth_id.is_empty():
- if presented_appearance == appearance_snapshot:
- presented_appearance = appearance_snapshot.duplicate(true)
- presented_appearance["mouth"] = _speech_mouth_id
- if _character_call_mouth_active:
- if presented_appearance == appearance_snapshot:
- presented_appearance = appearance_snapshot.duplicate(true)
- presented_appearance["mouth"] = CHARACTER_CALL_MOUTH_ID
- PlayerVisualPresenter.apply_appearance(_visuals, presented_appearance)
-
-
-func apply_animalese_voice_id(voice_id: String) -> void:
- animalese_voice_id = VoiceProfilesType.sanitized_id(voice_id)
-
-
-func get_animalese_voice_id() -> String:
- return animalese_voice_id
-
-
-func apply_animalese_sample_set_id(sample_set_id: String) -> void:
- animalese_sample_set_id = VoiceProfilesType.sanitized_sample_set_id(
- sample_set_id
- )
-
-
-func get_animalese_sample_set_id() -> String:
- return animalese_sample_set_id
class ShowcaseCameraSnapshot:
extends RefCounted
@@ -384,17 +26,18 @@ class ShowcaseCameraSnapshot:
var pitch_rotation: Vector3
var spring_length: float
var target_zoom: float
+ var camera_input_enabled: bool
var camera_dragging: bool
@export_category("Movement")
-@export var walk_speed: float = 2.25
-@export var sprint_speed: float = 4.5
-@export var sneak_speed: float = 1.125
-@export var slow_walk_speed: float = 2.9
+@export var walk_speed: float = 5.0
+@export var sprint_speed: float = 8.0
+@export var sneak_speed: float = 2.0
+@export var slow_walk_speed: float = 3.25
@export_range(0.1, 20.0, 0.1) var jump_velocity: float = 4.6
@export_range(0.1, 5.0, 0.05) var upward_gravity_multiplier: float = 1.35
-@export_range(0.1, 5.0, 0.05) var fall_gravity_multiplier: float = 2.35
+@export_range(0.1, 5.0, 0.05) var fall_gravity_multiplier: float = 2.1
@export_range(0.1, 3.0, 0.05) var body_center_height: float = 0.9
@export var body_rotation_speed: float = 12.0
@@ -402,7 +45,7 @@ class ShowcaseCameraSnapshot:
@export var local_control_enabled: bool = true
@export_category("Fishing Stats")
-@export_range(0.01, 2.0, 0.01) var reel_speed: float = BASE_REEL_SPEED
+@export_range(0.01, 2.0, 0.01) var reel_speed: float = 0.32
@export_range(1, 100, 1) var click_power: int = 1
@export_category("Showcase")
@@ -410,49 +53,28 @@ class ShowcaseCameraSnapshot:
@export_range(0.05, 1.0, 0.01) var showcase_restore_duration: float = 0.24
@export_range(0.05, 1.5, 0.01) var showcase_camera_transition_duration: float = 0.42
@export_range(-180.0, 180.0, 1.0) var showcase_camera_yaw_offset: float = 180.0
-@export_range(-180.0, 180.0, 1.0) var net_showcase_camera_yaw_offset: float = 0.0
@export_range(-80.0, 80.0, 1.0) var showcase_camera_pitch: float = -8.0
@export_range(1.0, 10.0, 0.1) var showcase_camera_zoom_distance: float = 3.6
@export_range(0.5, 3.0, 0.05) var showcase_camera_target_height: float = 1.45
@export_range(0.01, 1.0, 0.01) var catch_presentation_base_scale: float = 0.10
-@export_category("Gathering")
-@export_flags_3d_physics var net_impact_collision_mask: int = 1
-@export_range(0.02, 0.5, 0.01) var net_impact_radius: float = 0.16
-@export_range(0.0, 1.0, 0.01) var net_success_contact_pause_duration: float = 0.45
-
@export_category("Camera")
@export var mouse_sensitivity: float = 0.005
@export var controller_camera_speed: float = 2.5
@export_range(0.0, 1.0, 0.01) var controller_camera_deadzone: float = 0.2
@export var invert_camera_y: bool = false
-var _swap_hotbar_camera_scroll: bool = false
-var _rv_interior_camera_mode: bool = false
-var _rv_interior_environment: Environment
-var _rv_previous_camera_environment: Environment
@export_range(-89.0, 0.0, 0.5) var minimum_pitch_degrees: float = -65.0
@export_range(0.0, 89.0, 0.5) var maximum_pitch_degrees: float = 45.0
@export var minimum_zoom: float = 2.0
@export var maximum_zoom: float = 8.0
@export var zoom_step: float = 0.75
@export var zoom_smoothing: float = 12.0
-@export_range(0.1, 12.0, 0.1) var controller_zoom_speed: float = 4.0
-@export_range(0.0, 1.0, 0.01) var controller_trigger_threshold: float = 0.55
-@export_range(1.0, 40.0, 0.5) var free_camera_speed: float = 10.0
-@export_range(1.0, 10.0, 0.5) var free_camera_sprint_multiplier: float = 3.0
@onready var _visuals: Node3D = %Visuals
-@onready var _character_rig: Node3D = get_node_or_null(
- "Visuals/CharacterRig/CharacterRig"
-) as Node3D
-@onready var _character_animation_player: AnimationPlayer = (
- get_node_or_null("Visuals/CharacterRig/AnimationPlayer") as AnimationPlayer
-)
@onready var _camera_yaw: Node3D = %CameraYaw
@onready var _camera_pitch: Node3D = %CameraPitch
@onready var _spring_arm: SpringArm3D = %SpringArm3D
@onready var _camera: Camera3D = %Camera3D
-@onready var _player_collision_shape: CollisionShape3D = $CollisionShape3D
@onready var inventory: FishInventoryType = %Inventory
@onready var collection_log: CollectionLogType = %CollectionLog
@onready var wallet: PlayerWalletType = %Wallet
@@ -462,45 +84,19 @@ var _rv_previous_camera_environment: Environment
@onready var fishing_upgrades: PlayerFishingUpgradesType = %FishingUpgrades
@onready var item_effects: PlayerItemEffectsType = %ItemEffects
@onready var cooler_capacity: PlayerCoolerCapacityType = %CoolerCapacity
-@onready var inventory_layout: PlayerInventoryLayoutType = %InventoryLayout
-@onready var art_unlocks: PlayerArtUnlocksType = %ArtUnlocks
-@onready var experience: PlayerExperienceType = %Experience
@onready var _cast_origin: Marker3D = %CastOrigin
-var _catch_display: Node3D
-var _catch_sprite: Sprite3D
-var _held_item_attachment: BoneAttachment3D
-var _held_fish_display: Node3D
-var _held_fish_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04)
-var _held_fish_sprite: Sprite3D
-var _held_art_kit_display: Node3D
-var _held_art_kit_sprite: Sprite3D
-var _held_decor_package_display: Node3D
-var _held_decor_package_visible: bool = false
-var _catch_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04)
+@onready var _fishing_rod: Node3D = %FishingRod
+@onready var _fishing_rod_tip: Marker3D = %FishingRodTip
+@onready var _catch_display: Node3D = %CatchDisplay
+@onready var _catch_sprite: Sprite3D = %CatchSprite
var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity"))
var _camera_dragging: bool = false
var _camera_input_enabled: bool = true
-var _camera_drag_prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
-var _camera_drag_pointer_origin: Vector2 = Vector2.ZERO
-var _camera_drag_restore_pointer: bool = false
-var _free_camera_active: bool = false
-var _free_camera_body: CharacterBody3D
-var _free_camera: Camera3D
-var _free_camera_yaw: float = 0.0
-var _free_camera_pitch: float = 0.0
var _movement_enabled: bool = true
-var _local_input_suppressors: Dictionary[StringName, bool] = {}
-var _camera_input_suppressors: Dictionary[StringName, bool] = {}
var _water_recovery_active: bool = false
-var _remote_recovery_presentation_active: bool = false
-var _remote_recovery_visual_origin: Vector3
-var _remote_recovery_elapsed: float = 0.0
var _target_zoom: float = 5.0
var _showcase_rod_visibility: bool = true
-var _showcase_net_visibility: bool = false
-var _showcase_shovel_visibility: bool = false
-var _remote_presentation_visible := true
var _showcase_rod_state_stored: bool = false
var _showcase_visual_rotation: Vector3
var _showcase_visual_rotation_stored: bool = false
@@ -509,560 +105,17 @@ var _showcase_restore_tween: Tween
var _showcase_camera_tween: Tween
var _showcase_camera_snapshot: ShowcaseCameraSnapshot
var _showcase_restore_generation: int = 0
-var _network_peer_id: int = 0
-var _network_authoritative_simulation: bool = false
-var _network_interpolation_enabled: bool = false
-var _network_axis: Vector2 = Vector2.ZERO
-var _network_camera_yaw: float = 0.0
-var _network_jump_pending: bool = false
-var _network_sprint: bool = false
-var _network_sneak: bool = false
-var _network_slow_walk: bool = false
-var _last_network_input_sequence: int = 0
-var _network_input_age: float = 0.0
-var _network_input_stale: bool = false
-var _network_input_stale_timeout_seconds: float = (
- NETWORK_INPUT_STALE_TIMEOUT_SECONDS
-)
-var _network_jump_intent_active: bool = false
-var _network_target_position: Vector3
-var _network_target_velocity: Vector3
-var _network_target_visual_yaw: float = 0.0
-var _network_target_grounded: bool = false
-var _network_target_locomotion_state: LocomotionState = LocomotionState.IDLE
-var _network_target_animation_action_id: StringName = &""
-var _network_target_animation_action_sequence: int = 0
-var _network_target_animation_action_elapsed: float = 0.0
-var _network_target_animation_action_paused: bool = false
-var _network_snapshot_ready: bool = false
-var _network_snapshot_age: float = 0.0
-var _network_snapshot_jitter: float = 0.0
-var _network_simulation_only: bool = false
-var _local_reconciliation_visual_offset: Vector3 = Vector3.ZERO
-var _local_reconciliation_camera_offset: Vector3 = Vector3.ZERO
-var _local_prediction_error_seconds: float = 0.0
-var _local_prediction_error_audits: int = 0
-var _local_prediction_error_direction: Vector3 = Vector3.ZERO
-var _local_prediction_soft_corrections: int = 0
-var _local_prediction_hard_corrections: int = 0
-var _local_prediction_largest_error: float = 0.0
-var _authoritative_movement_history: Array[Dictionary] = []
-var _local_network_jump_intent_pending: bool = false
-var _local_network_jump_intent_sequence: int = -1
-var _animation_action_id: StringName = &""
-var _animation_action_sequence: int = 0
-var _animation_action_elapsed: float = 0.0
-var _animation_action_paused: bool = false
-var _presented_animation_action_id: StringName = &""
-var _presented_animation_action_sequence: int = -1
-var _presented_animation_action_paused: bool = false
-var _character_animation_name: StringName = &""
-var _sitting: bool = false
-var _sit_after_landing: bool = false
-var _sitting_intent_pending: bool = false
-var _sitting_intent_sequence: int = -1
-var _held_fish_visible: bool = false
-var _pending_held_fish_texture: Texture2D
-var _pending_held_fish_scale: Vector3 = Vector3.ONE
-var _held_art_kit_visible: bool = false
-var _showcase_animation_active: bool = false
-var _pocket_visual_active: bool = false
-var _pocket_visual_target: PocketVisualTarget = PocketVisualTarget.NONE
-var _pocket_visual_animation: StringName = &""
-var _pocket_visual_finished_callback: Callable
-var _pocket_visual_callback_runs_on_interrupt: bool = true
-var _pocket_visual_midpoint_callback: Callable
-var _pocket_visual_midpoint_called: bool = false
-var _pocket_visual_generation: int = 0
-var _fighting_visual_active: bool = false
-var _blink_visual_active: bool = false
-var _blink_seconds_remaining: float = 0.0
-var _blink_rng := RandomNumberGenerator.new()
-var _character_call_mouth_active: bool = false
-var _character_call_visual_generation: int = 0
-var _speech_mouth_active: bool = false
-var _speech_mouth_id: String = ""
-var _speech_visual_generation: int = 0
-var _fishing_visual_active: bool = false
-var _fishing_visual_phase: FishingVisualPhase = FishingVisualPhase.NONE
-var _fishing_after_release_pending: bool = false
-var _retract_animation_completed: bool = true
-var _fishing_rod: Node3D
-var _catching_net: Node3D
-var _catching_net_attachment: NetAttachmentType
-var _shovel: Node3D
-var _net_strike_collision_armed: bool = false
-var _net_strike_has_previous_sample: bool = false
-var _net_strike_previous_position: Vector3
-var _net_strike_previous_animation_position: float = 0.0
-var _net_strike_contact_held: bool = false
-var _net_strike_result_known: bool = false
-var _net_strike_successful: bool = false
-var _net_strike_showcase_pause_tween: Tween
-var _net_impact_shape := SphereShape3D.new()
-var _pending_net_showcase_catch: FishCatchType
-var _pending_net_showcase_remote: bool = false
-var _fishing_rod_tip: Marker3D
-var _fishing_rod_model_mount: Node3D
-var _fishing_rod_fallback_visual: GeometryInstance3D
-var _custom_fishing_rod_visual: Node3D
-var _active_fishing_rod_id: StringName
-var _active_item_is_rod: bool = false
-var _active_item_is_net: bool = false
-var _active_item_is_shovel: bool = false
-var _controller_mapping_manager: ControllerMappingManagerType
-var _sprint_dust_landing_ready: bool = false
-var _sprint_dust_airborne: bool = false
-var _sprint_dust_fall_speed: float = 0.0
-
-@onready var _sprint_dust: SprintDustTrailType = %SprintDust
func _ready() -> void:
- _blink_rng.randomize()
- _schedule_next_blink()
- if (
- _character_animation_player != null
- and not _character_animation_player.animation_finished.is_connected(
- _on_character_animation_finished
- )
- ):
- _character_animation_player.animation_finished.connect(
- _on_character_animation_finished
- )
- if not bag.contents_changed.is_connected(_on_bag_contents_changed):
- bag.contents_changed.connect(_on_bag_contents_changed)
- _apply_presented_appearance()
- _initialize_fishing_rod()
- _initialize_catching_net()
- _initialize_shovel()
_target_zoom = clampf(_spring_arm.spring_length, minimum_zoom, maximum_zoom)
_spring_arm.spring_length = _target_zoom
_camera.current = local_control_enabled
- _initialize_held_item_attachment()
-
-
-func _on_bag_contents_changed() -> void:
- if (
- not active_bait_id.is_empty()
- and bag.get_quantity(active_bait_id) <= 0
- ):
- unequip_bait()
- if (
- not active_lure_id.is_empty()
- and not bag.owns_item(active_lure_id)
- ):
- unequip_lure()
-
-
-func set_controller_mapping_manager(
- mapping_manager: ControllerMappingManagerType,
-) -> void:
- _controller_mapping_manager = mapping_manager
-
-
-func begin_animation_action(
- action_id: StringName,
- elapsed_seconds: float = 0.0,
-) -> bool:
- if (
- action_id.is_empty()
- or not supports_network_animation_action(action_id)
- or _character_animation_player == null
- or not _character_animation_player.has_animation(action_id)
- ):
- return false
- var next_sequence: int = (
- 1
- if _animation_action_sequence
- >= NetworkPlayerAnimationProtocol.MAX_ACTION_SEQUENCE
- else _animation_action_sequence + 1
- )
- var action_state: Dictionary = (
- NetworkPlayerAnimationProtocol.make_action_state(
- action_id, next_sequence, elapsed_seconds
- )
- )
- if not NetworkPlayerAnimationProtocol.validate_action_state(action_state):
- return false
- _apply_animation_action_state(action_state)
- return true
-
-
-func end_animation_action() -> void:
- if _animation_action_id.is_empty():
- return
- var next_sequence: int = (
- 1
- if _animation_action_sequence
- >= NetworkPlayerAnimationProtocol.MAX_ACTION_SEQUENCE
- else _animation_action_sequence + 1
- )
- _apply_animation_action_state(
- NetworkPlayerAnimationProtocol.make_action_state(
- &"", next_sequence, 0.0
- )
- )
-
-
-func begin_net_draw_visual() -> bool:
- return begin_animation_action(
- CHARACTER_NET_DRAW_ANIMATION
- if is_sneaking()
- else CHARACTER_CASTING_ANIMATION
- )
-
-
-func play_net_strike_visual() -> bool:
- if begin_animation_action(CHARACTER_NET_STRIKE_ANIMATION):
- _net_strike_collision_armed = local_control_enabled
- _net_strike_has_previous_sample = false
- _net_strike_contact_held = false
- _net_strike_result_known = false
- _net_strike_successful = false
- return true
- cancel_net_action_visual()
- return false
-
-
-func resolve_net_strike_visual(successful: bool) -> void:
- if _animation_action_id != CHARACTER_NET_STRIKE_ANIMATION:
- return
- _net_strike_result_known = true
- _net_strike_successful = successful
- if successful:
- _net_strike_collision_armed = false
- if successful and _animation_action_paused:
- _net_strike_contact_held = false
- _set_animation_action_paused(false)
-
-
-func is_net_strike_held() -> bool:
- return (
- _animation_action_id == CHARACTER_NET_STRIKE_ANIMATION
- and _net_strike_contact_held
- )
-
-
-func release_net_strike_hold() -> bool:
- if not is_net_strike_held():
- return false
- cancel_net_action_visual()
- return true
-
-
-func cancel_net_action_visual() -> void:
- _clear_net_strike_state()
- if _animation_action_id in [
- CHARACTER_CASTING_ANIMATION,
- CHARACTER_NET_DRAW_ANIMATION,
- CHARACTER_NET_STRIKE_ANIMATION,
- ]:
- end_animation_action()
-
-
-func _clear_net_strike_state() -> void:
- _kill_net_strike_showcase_pause()
- _net_strike_collision_armed = false
- _net_strike_has_previous_sample = false
- _net_strike_contact_held = false
- _net_strike_result_known = false
- _net_strike_successful = false
- if _animation_action_paused:
- _set_animation_action_paused(false)
-
-
-func _kill_net_strike_showcase_pause() -> void:
- if (
- _net_strike_showcase_pause_tween != null
- and _net_strike_showcase_pause_tween.is_valid()
- ):
- _net_strike_showcase_pause_tween.kill()
- _net_strike_showcase_pause_tween = null
-
-
-func _hold_successful_net_strike_before_showcase() -> void:
- _kill_net_strike_showcase_pause()
- var strike_animation := _character_animation_player.get_animation(
- CHARACTER_NET_STRIKE_ANIMATION
- )
- if strike_animation != null:
- _animation_action_elapsed = maxf(
- strike_animation.length - 0.001,
- 0.0,
- )
- _net_strike_collision_armed = false
- _net_strike_contact_held = false
- _set_animation_action_paused(true)
- if net_success_contact_pause_duration <= 0.0:
- _finish_successful_net_strike_pause()
- return
- _net_strike_showcase_pause_tween = create_tween()
- _net_strike_showcase_pause_tween.tween_interval(
- net_success_contact_pause_duration
- )
- _net_strike_showcase_pause_tween.finished.connect(
- _finish_successful_net_strike_pause
- )
-
-
-func _finish_successful_net_strike_pause() -> void:
- _net_strike_showcase_pause_tween = null
- if local_control_enabled:
- if not _net_strike_result_known or not _net_strike_successful:
- return
- _clear_net_strike_state()
- end_animation_action()
- _begin_pending_net_showcase()
-
-
-func _set_animation_action_paused(paused: bool) -> void:
- if _animation_action_id.is_empty() or _animation_action_paused == paused:
- return
- var next_sequence: int = (
- 1
- if _animation_action_sequence
- >= NetworkPlayerAnimationProtocol.MAX_ACTION_SEQUENCE
- else _animation_action_sequence + 1
- )
- _apply_animation_action_state(
- NetworkPlayerAnimationProtocol.make_action_state(
- _animation_action_id,
- next_sequence,
- _animation_action_elapsed,
- paused,
- )
- )
-
-
-func _update_net_strike_contact() -> void:
- if (
- not local_control_enabled
- or not _net_strike_collision_armed
- or _animation_action_paused
- or _animation_action_id != CHARACTER_NET_STRIKE_ANIMATION
- or _catching_net_attachment == null
- or _character_animation_player == null
- or get_world_3d() == null
- ):
- return
- var current_position: Vector3 = (
- _catching_net_attachment.get_impact_world_position()
- )
- var current_animation_position: float = (
- _character_animation_player.current_animation_position
- )
- if not _net_strike_has_previous_sample:
- _net_strike_previous_position = current_position
- _net_strike_previous_animation_position = current_animation_position
- _net_strike_has_previous_sample = true
- return
- var motion := current_position - _net_strike_previous_position
- if motion.length_squared() <= 0.000001:
- _net_strike_previous_animation_position = current_animation_position
- return
- _net_impact_shape.radius = net_impact_radius
- var query := PhysicsShapeQueryParameters3D.new()
- query.shape = _net_impact_shape
- query.transform = Transform3D(
- Basis.IDENTITY,
- _net_strike_previous_position,
- )
- query.motion = motion
- query.collision_mask = net_impact_collision_mask
- query.collide_with_areas = false
- query.collide_with_bodies = true
- query.exclude = [get_rid()]
- var fractions: PackedFloat32Array = (
- get_world_3d().direct_space_state.cast_motion(query)
- )
- if fractions.size() >= 2 and float(fractions[1]) < 1.0:
- var impact_fraction := clampf(float(fractions[0]), 0.0, 1.0)
- _animation_action_elapsed = lerpf(
- _net_strike_previous_animation_position,
- current_animation_position,
- impact_fraction,
- )
- _net_strike_collision_armed = false
- _net_strike_contact_held = true
- _set_animation_action_paused(true)
- return
- _net_strike_previous_position = current_position
- _net_strike_previous_animation_position = current_animation_position
-
-
-func _apply_animation_action_state(action_state: Dictionary) -> void:
- if not NetworkPlayerAnimationProtocol.validate_action_state(action_state):
- return
- var action_id := StringName(str(action_state["id"]))
- if (
- not action_id.is_empty()
- and not supports_network_animation_action(action_id)
- ):
- action_id = &""
- var action_sequence: int = int(action_state["sequence"])
- var action_changed: bool = (
- action_id != _animation_action_id
- or action_sequence != _animation_action_sequence
- or bool(action_state.get("paused", false))
- != _animation_action_paused
- )
- _animation_action_id = action_id
- _animation_action_sequence = action_sequence
- _animation_action_elapsed = float(action_state["elapsed"])
- _animation_action_paused = bool(action_state.get("paused", false))
- if action_changed:
- _character_animation_name = &""
- _update_character_animation()
-
-
-func _initialize_fishing_rod() -> void:
- var skeleton := get_node_or_null(
- "Visuals/CharacterRig/CharacterRig/Skeleton3D"
- ) as Skeleton3D
- if skeleton == null:
- push_error("Player character skeleton is unavailable.")
- return
- var attachment := FishingRodAttachmentScene.instantiate() as BoneAttachment3D
- if attachment == null:
- push_error("Fishing rod attachment could not be instantiated.")
- return
- skeleton.add_child(attachment)
- _fishing_rod = attachment.get_node("FishingRod") as Node3D
- _fishing_rod_tip = attachment.get_node(
- "FishingRod/FishingRodTip"
- ) as Marker3D
- _fishing_rod_model_mount = attachment.get_node(
- "FishingRod/ModelMount"
- ) as Node3D
- _fishing_rod_fallback_visual = attachment.get_node(
- "FishingRod/FallbackRodMesh"
- ) as GeometryInstance3D
-
-
-func _initialize_catching_net() -> void:
- var skeleton := get_node_or_null(
- "Visuals/CharacterRig/CharacterRig/Skeleton3D"
- ) as Skeleton3D
- if skeleton == null:
- push_error("Player character skeleton is unavailable for the net.")
- return
- var attachment := NetAttachmentScene.instantiate() as NetAttachmentType
- if attachment == null:
- push_error("Catching net attachment could not be instantiated.")
- return
- skeleton.add_child(attachment)
- _catching_net_attachment = attachment
- _catching_net = attachment.get_node("CatchingNet") as Node3D
- if _catching_net != null:
- _catching_net.visible = false
-
-
-func _initialize_shovel() -> void:
- var skeleton := get_node_or_null(
- "Visuals/CharacterRig/CharacterRig/Skeleton3D"
- ) as Skeleton3D
- if skeleton == null:
- push_error("Player character skeleton is unavailable for the shovel.")
- return
- var attachment := ShovelAttachmentScene.instantiate() as BoneAttachment3D
- if attachment == null:
- push_error("Shovel attachment could not be instantiated.")
- return
- skeleton.add_child(attachment)
- _shovel = attachment.get_node("Shovel") as Node3D
- if _shovel != null:
- _shovel.visible = false
-
-
-func _initialize_held_item_attachment() -> void:
- var skeleton := get_node_or_null(
- "Visuals/CharacterRig/CharacterRig/Skeleton3D"
- ) as Skeleton3D
- if skeleton == null:
- push_error("Player character skeleton is unavailable for held items.")
- return
- var attachment := HeldItemAttachmentScene.instantiate() as BoneAttachment3D
- if attachment == null:
- push_error("Held item attachment could not be instantiated.")
- return
- skeleton.add_child(attachment)
- _held_item_attachment = attachment
- _held_fish_display = attachment.get_node("HeldFishDisplay") as Node3D
- _held_fish_attachment_offset = _held_fish_display.position
- _held_fish_sprite = attachment.get_node(
- "HeldFishDisplay/HeldFishSprite"
- ) as Sprite3D
- _held_art_kit_display = attachment.get_node("HeldArtKitDisplay") as Node3D
- _held_art_kit_sprite = attachment.get_node(
- "HeldArtKitDisplay/HeldArtKitSprite"
- ) as Sprite3D
- _held_decor_package_display = _build_decor_package_display(attachment)
- _catch_display = attachment.get_node("CatchDisplay") as Node3D
- _catch_attachment_offset = _catch_display.position
- _catch_sprite = attachment.get_node("CatchDisplay/CatchSprite") as Sprite3D
func _physics_process(delta: float) -> void:
- if _network_interpolation_enabled:
- _update_network_interpolation(delta)
- return
- _simulate_movement_physics(delta)
- if _network_authoritative_simulation:
- _record_authoritative_movement_state()
-
-
-func _simulate_movement_physics(delta: float) -> void:
- if _network_authoritative_simulation:
- _update_network_input_freshness(delta)
- if local_control_enabled and _free_camera_active:
- _update_free_camera_physics()
- velocity.x = 0.0
- velocity.z = 0.0
- if not is_on_floor():
- velocity.y -= _gravity * fall_gravity_multiplier * delta
- move_and_slide()
- _network_jump_pending = false
- return
- var local_jump_pressed: bool = (
- local_control_enabled
- and Input.is_action_just_pressed("jump")
- )
- if (
- local_jump_pressed
- and _is_movement_input_enabled()
- and not _free_camera_active
- ):
- _queue_local_network_jump_intent()
- var jump_requested: bool = (
- _is_movement_input_enabled()
- and not _free_camera_active
- and (
- local_jump_pressed
- or (_network_authoritative_simulation and _network_jump_pending)
- )
- )
- if jump_requested:
- _cancel_sitting_for_jump()
- # Sitting deliberately stops movement processing, so never allow a stale
- # grounded flag or a reconciled network state to leave an airborne avatar in
- # that early-return path. Preserve the intent and sit once landing is real.
- if _sitting and not _can_begin_sitting():
- _sit_after_landing = true
- _set_sitting(false)
- if _sit_after_landing and not jump_requested and _can_begin_sitting():
- _sit_after_landing = false
- _set_sitting(true, local_control_enabled)
- if _sitting:
- if jump_requested:
- _set_sitting(false, local_control_enabled)
- else:
- velocity = Vector3.ZERO
- _network_jump_pending = false
- return
if _water_recovery_active:
velocity = Vector3.ZERO
- _network_jump_pending = false
return
if not is_on_floor():
@@ -1072,43 +125,34 @@ func _simulate_movement_physics(delta: float) -> void:
else fall_gravity_multiplier
)
velocity.y -= _gravity * gravity_multiplier * delta
- elif jump_requested:
+ elif (
+ local_control_enabled
+ and _movement_enabled
+ and Input.is_action_just_pressed("jump")
+ ):
velocity.y = jump_velocity
- _network_jump_pending = false
- if not _is_movement_input_enabled():
+ if not local_control_enabled or not _movement_enabled:
velocity.x = 0.0
velocity.z = 0.0
move_and_slide()
return
- var input_vector: Vector2
- var camera_basis: Basis
- if local_control_enabled:
- input_vector = Input.get_vector(
- "move_left",
- "move_right",
- "move_forward",
- "move_backward"
- )
- camera_basis = _camera_yaw.global_basis
- elif _network_authoritative_simulation:
- input_vector = _network_axis
- camera_basis = Basis(Vector3.UP, _network_camera_yaw)
- else:
- velocity.x = 0.0
- velocity.z = 0.0
- return
+ var input_vector: Vector2 = Input.get_vector(
+ "move_left",
+ "move_right",
+ "move_forward",
+ "move_backward"
+ )
+ var camera_basis: Basis = _camera_yaw.global_basis
var move_direction: Vector3 = camera_basis.x * input_vector.x + camera_basis.z * input_vector.y
move_direction.y = 0.0
var input_strength: float = minf(input_vector.length(), 1.0)
move_direction = move_direction.normalized()
- if input_strength > 0.05:
- release_net_strike_hold()
- var speed: float = _get_network_aware_speed()
- if item_effects != null:
- speed *= item_effects.get_movement_multiplier()
+ var speed: float = (
+ _get_current_speed() * item_effects.get_movement_multiplier()
+ )
velocity.x = move_direction.x * speed * input_strength
velocity.z = move_direction.z * speed * input_strength
@@ -1124,70 +168,32 @@ func _simulate_movement_physics(delta: float) -> void:
func _process(delta: float) -> void:
- if _network_simulation_only:
- return
- _update_local_reconciliation_visuals(delta)
- if not _animation_action_id.is_empty() and not _animation_action_paused:
- _animation_action_elapsed = minf(
- _animation_action_elapsed + delta,
- NetworkPlayerAnimationProtocol.MAX_ACTION_ELAPSED_SECONDS,
- )
- _update_blink(delta)
- _update_character_animation()
- _update_net_strike_contact()
- _update_sprint_dust(delta)
- # The hand bone supplies the attachment position, but its animated wrist
- # rotation should not turn the flat fish/catch artwork edge-on. Keep each
- # display aligned with the character's facing while it follows the hand.
- if _character_rig != null and _held_item_attachment != null:
- var facing_rotation: Vector3 = _character_rig.global_rotation
- for display: Node3D in [_held_fish_display, _catch_display]:
- if display != null:
- var attachment_offset: Vector3 = (
- _held_fish_attachment_offset
- if display == _held_fish_display
- else _catch_attachment_offset
- )
- display.global_position = (
- _held_item_attachment.global_transform * attachment_offset
- )
- display.global_rotation = facing_rotation
- if _remote_recovery_presentation_active:
- _remote_recovery_elapsed += delta
- _visuals.position = (
- _remote_recovery_visual_origin
- + Vector3.UP
- * sin(_remote_recovery_elapsed * 2.0 * TAU)
- * 0.08
- )
if not local_control_enabled:
return
- if _free_camera_active:
- if _is_camera_input_enabled():
- _rotate_free_camera(
- _scale_controller_camera_input(
- _get_controller_camera_stick(),
- delta,
- )
- )
- return
- if _is_camera_input_enabled():
- var stick: Vector2 = _get_controller_camera_stick()
+ if (
+ _camera_input_enabled
+ and _camera_dragging
+ and not Input.is_action_pressed("camera_drag")
+ ):
+ _camera_dragging = false
+
+ if _camera_input_enabled:
+ var stick: Vector2 = Vector2(
+ Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
+ Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y)
+ )
if stick.length() > controller_camera_deadzone:
- if (
- _get_controller_zoom_strength()
- >= controller_trigger_threshold
- ):
- var vertical_zoom_input: float = _apply_axis_deadzone(stick.y)
- _set_target_zoom(
- _target_zoom
- + vertical_zoom_input * controller_zoom_speed * delta
- )
- else:
- _rotate_camera(
- _scale_controller_camera_input(stick, delta)
- )
+ var adjusted_strength: float = (
+ (stick.length() - controller_camera_deadzone)
+ / (1.0 - controller_camera_deadzone)
+ )
+ _rotate_camera(
+ stick.normalized()
+ * adjusted_strength
+ * controller_camera_speed
+ * delta
+ )
var zoom_weight: float = 1.0 - exp(-zoom_smoothing * delta)
_spring_arm.spring_length = lerpf(
@@ -1197,607 +203,32 @@ func _process(delta: float) -> void:
)
-func _update_blink(delta: float) -> void:
- if _fighting_visual_active:
- return
- _blink_seconds_remaining -= delta
- if _blink_seconds_remaining > 0.0:
- return
- _blink_visual_active = not _blink_visual_active
- if _blink_visual_active:
- _blink_seconds_remaining = BLINK_DURATION_SECONDS
- else:
- _schedule_next_blink()
- _apply_presented_appearance()
-
-
-func _schedule_next_blink() -> void:
- _blink_seconds_remaining = _blink_rng.randf_range(
- BLINK_INTERVAL_SECONDS.x,
- BLINK_INTERVAL_SECONDS.y,
- )
-
-
-func _update_sprint_dust(delta: float) -> void:
- if _sprint_dust == null:
- return
- var horizontal_velocity := Vector3(velocity.x, 0.0, velocity.z)
- var grounded: bool = _get_presented_grounded()
- if _water_recovery_active:
- _sprint_dust_airborne = false
- _sprint_dust_fall_speed = 0.0
- elif not _sprint_dust_landing_ready:
- # Establish a real ground contact before arming landing effects. This
- # prevents the player's initial placement from looking like a fall.
- _sprint_dust_landing_ready = grounded
- elif not grounded:
- _sprint_dust_airborne = true
- _sprint_dust_fall_speed = maxf(
- _sprint_dust_fall_speed,
- maxf(-velocity.y, 0.0),
- )
- elif _sprint_dust_airborne:
- if _sprint_dust_fall_speed >= LANDING_DUST_MIN_FALL_SPEED:
- var landing_facing: Vector3 = horizontal_velocity.normalized()
- if landing_facing.is_zero_approx() and _visuals != null:
- landing_facing = -_visuals.global_basis.z
- _sprint_dust.emit_landing_burst(
- global_position,
- landing_facing,
- )
- _sprint_dust_airborne = false
- _sprint_dust_fall_speed = 0.0
- var should_emit: bool = (
- grounded
- and not _sitting
- and not _water_recovery_active
- and _get_presented_locomotion_state() == LocomotionState.RUNNING
- )
- _sprint_dust.update_trail(
- delta,
- global_position,
- horizontal_velocity,
- should_emit,
- )
-
-
-func _get_controller_camera_stick() -> Vector2:
- if _controller_mapping_manager != null:
- return Vector2(
- _controller_mapping_manager.get_role_axis(
- ControllerMappingManagerType.ROLE_RIGHT_STICK_X
- ),
- _controller_mapping_manager.get_role_axis(
- ControllerMappingManagerType.ROLE_RIGHT_STICK_Y
- ),
- )
- return Vector2(
- Input.get_joy_axis(0, JOY_AXIS_RIGHT_X),
- Input.get_joy_axis(0, JOY_AXIS_RIGHT_Y),
- )
-
-
-func _scale_controller_camera_input(
- stick: Vector2,
- delta: float,
-) -> Vector2:
- var strength: float = stick.length()
- if strength <= controller_camera_deadzone:
- return Vector2.ZERO
- var adjusted_strength: float = (
- (strength - controller_camera_deadzone)
- / (1.0 - controller_camera_deadzone)
- )
- return (
- stick.normalized()
- * adjusted_strength
- * controller_camera_speed
- * delta
- )
-
-
-func _get_controller_zoom_strength() -> float:
- if _controller_mapping_manager != null:
- return _controller_mapping_manager.get_role_strength(
- ControllerMappingManagerType.ROLE_CAMERA_ZOOM
- )
- return Input.get_joy_axis(0, CONTROLLER_ZOOM_TRIGGER_AXIS)
-
-
-func _update_character_animation() -> void:
- if _character_animation_player == null:
- return
- var locomotion_state: LocomotionState = (
- _get_presented_locomotion_state()
- )
- var is_walking: bool = locomotion_state != LocomotionState.IDLE
- var is_running: bool = locomotion_state == LocomotionState.RUNNING
- var is_sneaking_pose: bool = (
- locomotion_state == LocomotionState.SNEAKING
- )
- var is_moving_horizontally: bool = (
- _is_presented_moving_horizontally()
- )
- var animation_action: Dictionary = _get_presented_animation_action()
- var animation_action_id := StringName(str(animation_action.get("id", "")))
- var animation_action_paused: bool = bool(
- animation_action.get("paused", false)
- )
- var animation_action_available: bool = (
- not animation_action_id.is_empty()
- and supports_network_animation_action(animation_action_id)
- and _character_animation_player.has_animation(animation_action_id)
- )
- var held_show_item_visible: bool = _has_held_show_item()
- var requested_animation: Array[StringName] = []
- if _pocket_visual_active:
- requested_animation = [_pocket_visual_animation]
- elif _showcase_animation_active:
- if _sitting:
- requested_animation = [
- CHARACTER_IDLE_SIT_SHOW_ANIMATION,
- &"idle_sit_show_loop",
- CHARACTER_IDLE_SIT_ANIMATION,
- &"idle_sit_loop",
- &"idle_loop_sit",
- ]
- else:
- requested_animation = [
- CHARACTER_IDLE_SHOW_ANIMATION,
- &"idle_show_loop",
- &"show",
- &"show_loop",
- ]
- elif _fighting_visual_active:
- if _sitting:
- requested_animation = [
- CHARACTER_FIGHTING_SIT_ANIMATION,
- CHARACTER_IDLE_SIT_ANIMATION,
- &"idle_sit_loop",
- ]
- else:
- requested_animation = [
- CHARACTER_FIGHTING_ANIMATION,
- CHARACTER_IDLE_ANIMATION,
- ]
- elif _fishing_visual_active:
- if _sitting:
- match _fishing_visual_phase:
- FishingVisualPhase.CASTING:
- requested_animation = [
- CHARACTER_CASTING_SIT_ANIMATION,
- CHARACTER_FISHING_SIT_ANIMATION,
- CHARACTER_IDLE_SIT_ANIMATION,
- ]
- FishingVisualPhase.RELEASE:
- requested_animation = [
- CHARACTER_RELEASE_SIT_ANIMATION,
- CHARACTER_FISHING_SIT_ANIMATION,
- CHARACTER_IDLE_SIT_ANIMATION,
- ]
- FishingVisualPhase.RETRACT:
- requested_animation = [
- CHARACTER_RETRACT_SIT_ANIMATION,
- CHARACTER_RETRACT_ANIMATION,
- CHARACTER_FISHING_SIT_ANIMATION,
- CHARACTER_IDLE_SIT_ANIMATION,
- ]
- _:
- requested_animation = [
- CHARACTER_FISHING_SIT_ANIMATION,
- CHARACTER_IDLE_SIT_ANIMATION,
- &"idle_sit_loop",
- ]
- else:
- match _fishing_visual_phase:
- FishingVisualPhase.CASTING:
- requested_animation = [
- CHARACTER_CASTING_ANIMATION,
- CHARACTER_FISHING_ANIMATION,
- CHARACTER_IDLE_ANIMATION,
- ]
- FishingVisualPhase.RELEASE:
- requested_animation = [
- CHARACTER_RELEASE_ANIMATION,
- CHARACTER_FISHING_ANIMATION,
- CHARACTER_IDLE_ANIMATION,
- ]
- FishingVisualPhase.RETRACT:
- requested_animation = [
- CHARACTER_RETRACT_ANIMATION,
- CHARACTER_FISHING_ANIMATION,
- CHARACTER_IDLE_ANIMATION,
- ]
- _:
- requested_animation = [
- CHARACTER_FISHING_ANIMATION,
- CHARACTER_IDLE_ANIMATION,
- ]
- elif animation_action_available:
- requested_animation = [animation_action_id]
- elif _sitting:
- if held_show_item_visible:
- requested_animation = [
- CHARACTER_IDLE_SIT_SHOW_ANIMATION,
- &"idle_sit_show_loop",
- CHARACTER_IDLE_SIT_ANIMATION,
- &"idle_sit_loop",
- &"idle_loop_sit",
- ]
- else:
- requested_animation = [
- CHARACTER_IDLE_SIT_ANIMATION,
- &"idle_sit_loop",
- &"sitting",
- &"idle_loop_sit",
- ]
- elif is_sneaking_pose:
- if is_moving_horizontally:
- requested_animation = [
- CHARACTER_SNEAKING_ANIMATION,
- CHARACTER_WALKING_ANIMATION,
- &"walking_loop",
- ]
- else:
- requested_animation = [
- CHARACTER_IDLE_SNEAK_ANIMATION,
- CHARACTER_IDLE_ANIMATION,
- &"idle_loop",
- ]
- elif is_running and held_show_item_visible:
- requested_animation = [
- CHARACTER_RUNNING_SHOW_ANIMATION,
- &"running_show_loop",
- CHARACTER_WALKING_SHOW_ANIMATION,
- &"walking_show_loop",
- ]
- elif is_running:
- requested_animation = [
- CHARACTER_RUNNING_ANIMATION,
- &"running_loop",
- CHARACTER_WALKING_ANIMATION,
- &"walking_loop",
- ]
- elif is_walking and held_show_item_visible:
- requested_animation = [
- CHARACTER_WALKING_SHOW_ANIMATION,
- &"walking_show_loop",
- ]
- elif held_show_item_visible:
- requested_animation = [
- CHARACTER_IDLE_SHOW_ANIMATION,
- &"idle_show_loop",
- &"show",
- &"show_loop",
- ]
- elif is_walking:
- requested_animation = [
- CHARACTER_WALKING_ANIMATION,
- &"walking_loop",
- ]
- else:
- requested_animation = [CHARACTER_IDLE_ANIMATION, &"idle_loop"]
- var next_animation: StringName = &""
- for candidate: StringName in requested_animation:
- if _character_animation_player.has_animation(candidate):
- next_animation = candidate
- break
- if next_animation.is_empty():
- return
- var action_selected: bool = (
- animation_action_available and next_animation == animation_action_id
- )
- var action_sequence: int = int(animation_action.get("sequence", 0))
- var action_changed: bool = (
- action_selected
- and (
- animation_action_id != _presented_animation_action_id
- or action_sequence != _presented_animation_action_sequence
- or animation_action_paused
- != _presented_animation_action_paused
- )
- )
- if not action_selected:
- _presented_animation_action_id = &""
- _presented_animation_action_sequence = -1
- _presented_animation_action_paused = false
- if (
- _character_animation_playback_matches(
- next_animation,
- action_selected and animation_action_paused,
- )
- and not action_changed
- ):
- return
- # The animation name is only a selection cache. A remote player's
- # AnimationPlayer can occasionally stop or lose its assigned animation while
- # its replicated locomotion state remains valid. Reassert the selected
- # presentation here instead of waiting for another network state transition;
- # movement simulation and replication remain untouched.
- _character_animation_player.active = true
- _character_animation_player.play(next_animation)
- _character_animation_name = next_animation
- if action_selected:
- _presented_animation_action_id = animation_action_id
- _presented_animation_action_sequence = action_sequence
- _presented_animation_action_paused = animation_action_paused
- var elapsed: float = float(animation_action.get("elapsed", 0.0))
- var animation: Animation = _character_animation_player.get_animation(
- next_animation
- )
- if (
- (elapsed > 0.0 or animation_action_paused)
- and animation != null
- and animation.length > 0.0
- ):
- _character_animation_player.seek(
- fposmod(elapsed, animation.length), true
- )
- if animation_action_paused:
- _character_animation_player.pause()
-
-
-func _character_animation_playback_matches(
- animation_name: StringName,
- paused_action_selected: bool,
-) -> bool:
- if (
- _character_animation_name != animation_name
- or _character_animation_player.assigned_animation != animation_name
- or not _character_animation_player.active
- ):
- return false
- if paused_action_selected:
- return true
- var animation := _character_animation_player.get_animation(animation_name)
- if animation == null or animation.loop_mode == Animation.LOOP_NONE:
- # Completed one-shot actions deliberately keep their final pose until the
- # authoritative action state advances; they must not be restarted here.
- return true
- return _character_animation_player.is_playing()
-
-
-func _on_character_animation_finished(animation_name: StringName) -> void:
- if animation_name == CHARACTER_NET_STRIKE_ANIMATION:
- if local_control_enabled:
- if _net_strike_result_known and _net_strike_successful:
- _hold_successful_net_strike_before_showcase()
- else:
- var strike_animation := (
- _character_animation_player.get_animation(
- CHARACTER_NET_STRIKE_ANIMATION
- )
- )
- if strike_animation != null:
- _animation_action_elapsed = maxf(
- strike_animation.length - 0.001,
- 0.0,
- )
- _net_strike_collision_armed = false
- _net_strike_contact_held = true
- _set_animation_action_paused(true)
- return
- if _pending_net_showcase_remote:
- _hold_successful_net_strike_before_showcase()
- return
- if (
- local_control_enabled
- and not _animation_action_id.is_empty()
- and animation_name == _animation_action_id
- ):
- if animation_name in [
- CHARACTER_CASTING_ANIMATION,
- CHARACTER_NET_DRAW_ANIMATION,
- ]:
- return
- end_animation_action()
- return
- if animation_name in [
- CHARACTER_POCKET_IDLE_IDLE_ANIMATION,
- CHARACTER_POCKET_IDLE_SHOW_ANIMATION,
- CHARACTER_POCKET_SHOW_SHOW_ANIMATION,
- CHARACTER_POCKET_SHOW_IDLE_ANIMATION,
- CHARACTER_POCKET_SIT_IDLE_IDLE_ANIMATION,
- CHARACTER_POCKET_SIT_IDLE_SHOW_ANIMATION,
- CHARACTER_POCKET_SIT_SHOW_SHOW_ANIMATION,
- CHARACTER_POCKET_SIT_SHOW_IDLE_ANIMATION,
- CHARACTER_POCKET_WALKING_IDLE_IDLE_ANIMATION,
- CHARACTER_POCKET_WALKING_IDLE_SHOW_ANIMATION,
- CHARACTER_POCKET_WALKING_SHOW_SHOW_ANIMATION,
- CHARACTER_POCKET_WALKING_SHOW_IDLE_ANIMATION,
- ]:
- if _pocket_visual_active:
- _complete_pocket_visual()
- return
- if animation_name in [
- CHARACTER_RETRACT_ANIMATION,
- CHARACTER_RETRACT_SIT_ANIMATION,
- ]:
- _complete_retract_animation()
- return
- if (
- not _fishing_after_release_pending
- or animation_name not in [
- CHARACTER_RELEASE_ANIMATION,
- CHARACTER_RELEASE_SIT_ANIMATION,
- ]
- ):
- return
- _fishing_after_release_pending = false
- _set_fishing_visual_phase(FishingVisualPhase.FISHING)
-
-
-func is_retract_visual_complete() -> bool:
- return _retract_animation_completed
-
-
-func _complete_retract_animation() -> void:
- if _retract_animation_completed:
- return
- _retract_animation_completed = true
- retract_visual_finished.emit()
-
-
-func toggle_sitting() -> void:
- if _sit_after_landing:
- _sit_after_landing = false
- _set_sitting(false, local_control_enabled)
- return
- var should_sit: bool = not _sitting
- if should_sit:
- if (
- _character_animation_player == null
- or not _has_any_character_animation([
- CHARACTER_IDLE_SIT_ANIMATION,
- &"idle_sit_loop",
- &"sitting",
- &"idle_loop_sit",
- ])
- ):
- return
- if not _can_begin_sitting():
- _sit_after_landing = true
- return
- _set_sitting(should_sit, local_control_enabled)
-
-
-func _has_any_character_animation(candidates: Array[StringName]) -> bool:
- if _character_animation_player == null:
- return false
- for candidate: StringName in candidates:
- if _character_animation_player.has_animation(candidate):
- return true
- return false
-
-
-func _set_sitting(
- should_sit: bool,
- is_local_intent: bool = false,
-) -> void:
- if is_local_intent:
- _sitting_intent_pending = true
- _sitting_intent_sequence = -1
- if _sitting == should_sit:
- return
- _sitting = should_sit
- if _sitting:
- velocity = Vector3.ZERO
- _character_animation_name = &""
- _update_character_animation()
-
-
-func is_sitting() -> bool:
- return _sitting
-
-
-func _can_begin_sitting() -> bool:
- return is_on_floor() and velocity.y <= 0.0
-
-
-func _cancel_sitting_for_jump() -> void:
- if not _sitting and not _sit_after_landing:
- return
- _sit_after_landing = false
- _set_sitting(false, local_control_enabled)
-
-
-func get_network_sitting_state() -> bool:
- return _sitting
-
-
-func get_network_sitting_intent() -> bool:
- # Jumping is an explicit cancellation of both a current sit and a deferred
- # sit. Input capture can run before the matching physics frame, so suppress
- # the stale seated bit as soon as the reliable jump intent is queued.
- if _local_network_jump_intent_pending:
- return false
- return _sitting or _sit_after_landing
-
-
-func apply_network_sitting_state(should_sit: bool) -> void:
- _set_sitting(should_sit)
-
-
-func apply_authoritative_network_sitting_state(should_sit: bool) -> void:
- if should_sit and (
- not _can_begin_sitting() or _network_jump_pending
- ):
- _sit_after_landing = true
- _set_sitting(false)
- return
- _sit_after_landing = false
- _set_sitting(should_sit)
-
-
-func _input(event: InputEvent) -> void:
- # Camera dragging must observe the complete mouse gesture before GUI controls
- # get a chance to consume one part of it. Do not claim the event here: the
- # Art Kit still needs the same right-button state to suspend its pointer.
- if not local_control_enabled or not _is_camera_input_enabled():
- return
- var mouse_button := event as InputEventMouseButton
- if mouse_button != null and event.is_action("camera_drag"):
- _set_camera_dragging(mouse_button.pressed)
- return
- var mouse_motion := event as InputEventMouseMotion
- if mouse_motion == null or not _camera_dragging:
- return
- if _free_camera_active:
- _rotate_free_camera(mouse_motion.screen_relative * mouse_sensitivity)
- else:
- _rotate_camera(mouse_motion.screen_relative * mouse_sensitivity)
-
-
-func _notification(what: int) -> void:
- # A captured pointer can miss its release when the application loses focus.
- # Normal camera drags are ended by the matching mouse-button event; polling
- # InputMap here is intentionally avoided because changing mouse mode can
- # transiently clear the action and release an otherwise active drag.
- if what == NOTIFICATION_APPLICATION_FOCUS_OUT and _camera_dragging:
- _set_camera_dragging(false)
-
-
func _unhandled_input(event: InputEvent) -> void:
- if not local_control_enabled or not _is_camera_input_enabled():
+ if not local_control_enabled or not _camera_input_enabled:
return
- if event.is_action_pressed("toggle_free_camera"):
- toggle_free_camera()
+
+ if event.is_action("camera_drag"):
+ _camera_dragging = event.is_pressed()
get_viewport().set_input_as_handled()
return
- if _free_camera_active:
+ if event is InputEventMouseMotion and _camera_dragging:
+ _rotate_camera(event.relative * mouse_sensitivity)
+ get_viewport().set_input_as_handled()
return
- var mouse_zoom_in: bool = (
+ if (
event is InputEventMouseButton
- and (
- (not event.shift_pressed)
- if _rv_interior_camera_mode
- else event.shift_pressed != _swap_hotbar_camera_scroll
- )
+ and event.shift_pressed
and event.is_action_pressed("camera_zoom_in")
- )
- var controller_zoom_in: bool = (
- event is InputEventJoypadButton
- and event.is_action_pressed("camera_zoom_in")
- )
- var mouse_zoom_out: bool = (
- event is InputEventMouseButton
- and (
- (not event.shift_pressed)
- if _rv_interior_camera_mode
- else event.shift_pressed != _swap_hotbar_camera_scroll
- )
- and event.is_action_pressed("camera_zoom_out")
- )
- var controller_zoom_out: bool = (
- event is InputEventJoypadButton
- and event.is_action_pressed("camera_zoom_out")
- )
- if mouse_zoom_in or controller_zoom_in:
+ ):
_set_target_zoom(_target_zoom - zoom_step)
get_viewport().set_input_as_handled()
- elif mouse_zoom_out or controller_zoom_out:
+ elif (
+ event is InputEventMouseButton
+ and event.shift_pressed
+ and event.is_action_pressed("camera_zoom_out")
+ ):
_set_target_zoom(_target_zoom + zoom_step)
get_viewport().set_input_as_handled()
@@ -1812,42 +243,6 @@ func _get_current_speed() -> float:
return walk_speed
-func _set_camera_dragging(active: bool) -> void:
- if _camera_dragging == active:
- return
- _camera_dragging = active
- if active:
- _camera_drag_prior_mouse_mode = Input.mouse_mode
- _camera_drag_restore_pointer = _camera_drag_prior_mouse_mode in [
- Input.MOUSE_MODE_VISIBLE,
- Input.MOUSE_MODE_CONFINED,
- ]
- if _camera_drag_restore_pointer:
- _camera_drag_pointer_origin = get_viewport().get_mouse_position()
- # Capture starts with the physical press, before any motion is applied.
- # Switching modes after the first movement can synthesize a center warp
- # in the middle of the drag and interrupt camera control.
- Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
- else:
- if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
- Input.mouse_mode = _camera_drag_prior_mouse_mode
- if _camera_drag_restore_pointer:
- Input.warp_mouse(_camera_drag_pointer_origin)
- _camera_drag_restore_pointer = false
-
-
-func _get_network_aware_speed() -> float:
- if local_control_enabled:
- return _get_current_speed()
- if _network_sneak:
- return sneak_speed
- if _network_slow_walk:
- return slow_walk_speed
- if _network_sprint:
- return sprint_speed
- return walk_speed
-
-
func _rotate_camera(delta_rotation: Vector2) -> void:
_camera_yaw.rotation.y -= delta_rotation.x
var vertical_direction: float = -1.0 if invert_camera_y else 1.0
@@ -1858,1139 +253,22 @@ func _rotate_camera(delta_rotation: Vector2) -> void:
)
-func _set_free_camera_active(active: bool) -> void:
- if _free_camera_active == active:
- return
- if active:
- var camera_transform: Transform3D = _camera.global_transform
- var duplicated_camera := _camera.duplicate() as Camera3D
- if duplicated_camera == null:
- return
- var camera_body := CharacterBody3D.new()
- camera_body.name = "FreeCameraBody"
- camera_body.collision_layer = 0
- camera_body.collision_mask = collision_mask
- camera_body.motion_mode = CharacterBody3D.MOTION_MODE_FLOATING
- add_child(camera_body)
- camera_body.top_level = true
- camera_body.global_position = camera_transform.origin
- var camera_collision := CollisionShape3D.new()
- camera_collision.name = "FreeCameraCollision"
- camera_collision.shape = _player_collision_shape.shape
- camera_body.add_child(camera_collision)
- duplicated_camera.name = "FreeCamera"
- duplicated_camera.unique_name_in_owner = false
- camera_body.add_child(duplicated_camera)
- duplicated_camera.transform = Transform3D(
- camera_transform.basis,
- Vector3.ZERO,
- )
- _free_camera_body = camera_body
- _free_camera = duplicated_camera
- var camera_rotation: Vector3 = _free_camera.global_rotation
- _free_camera_yaw = camera_rotation.y
- _free_camera_pitch = camera_rotation.x
- _camera.current = false
- _free_camera.current = true
- _free_camera_active = true
- return
- _free_camera_active = false
- _set_camera_dragging(false)
- if _free_camera != null:
- _free_camera.current = false
- _free_camera = null
- if _free_camera_body != null:
- _free_camera_body.queue_free()
- _free_camera_body = null
- _camera.current = local_control_enabled
-
-
-func toggle_free_camera() -> void:
- if not local_control_enabled:
- return
- _set_free_camera_active(not _free_camera_active)
-
-
-func is_free_camera_active() -> bool:
- return _free_camera_active
-
-
-func _update_free_camera_physics() -> void:
- if _free_camera == null or _free_camera_body == null:
- _set_free_camera_active(false)
- return
- # Freecam translation is camera input, not avatar movement. Fishing locks
- # the avatar after a cast, but the detached camera must remain movable.
- # Camera/menu input ownership still stops freecam while an overlay is active.
- if not _is_camera_input_enabled():
- _free_camera_body.velocity = Vector3.ZERO
- _free_camera_body.move_and_slide()
- return
- var input_vector: Vector2 = Input.get_vector(
- "move_left",
- "move_right",
- "move_forward",
- "move_backward",
- )
- var vertical_input: float = (
- Input.get_action_strength("jump")
- - Input.get_action_strength("sneak")
- )
- var yaw_basis := Basis(Vector3.UP, _free_camera_yaw)
- var movement: Vector3 = (
- yaw_basis.x * input_vector.x
- + yaw_basis.z * input_vector.y
- + Vector3.UP * vertical_input
- )
- if movement.length_squared() > 1.0:
- movement = movement.normalized()
- var movement_speed: float = free_camera_speed
- if Input.is_action_pressed("sprint"):
- movement_speed *= free_camera_sprint_multiplier
- _free_camera_body.velocity = movement * movement_speed
- _free_camera_body.move_and_slide()
-
-
-func _rotate_free_camera(delta_rotation: Vector2) -> void:
- if _free_camera == null:
- return
- _free_camera_yaw -= delta_rotation.x
- var vertical_direction: float = -1.0 if invert_camera_y else 1.0
- _free_camera_pitch = clampf(
- _free_camera_pitch - delta_rotation.y * vertical_direction,
- deg_to_rad(-90.0),
- deg_to_rad(90.0),
- )
- _free_camera.global_rotation = Vector3(
- _free_camera_pitch,
- _free_camera_yaw,
- 0.0,
- )
-
-
-func get_active_gameplay_camera() -> Camera3D:
- if _free_camera_active and _free_camera != null:
- return _free_camera
- return _camera
-
-
func _set_target_zoom(value: float) -> void:
- _target_zoom = clampf(
- value,
- 1.25 if _rv_interior_camera_mode else minimum_zoom,
- 16.0 if _rv_interior_camera_mode else maximum_zoom,
- )
-
-
-func _apply_axis_deadzone(value: float) -> float:
- var magnitude: float = absf(value)
- if magnitude <= controller_camera_deadzone:
- return 0.0
- return (
- signf(value)
- * (magnitude - controller_camera_deadzone)
- / (1.0 - controller_camera_deadzone)
- )
+ _target_zoom = clampf(value, minimum_zoom, maximum_zoom)
func set_local_control(enabled: bool) -> void:
- if not enabled and _free_camera_active:
- _set_free_camera_active(false)
local_control_enabled = enabled
if is_node_ready():
_camera.current = enabled
if not enabled:
- _set_camera_dragging(false)
- _clear_local_network_jump_intent()
-
-
-func set_network_peer_id(peer_id: int) -> void:
- _network_peer_id = peer_id
-
-
-func get_network_peer_id() -> int:
- return _network_peer_id
-
-
-func configure_network_remote(
- authoritative_simulation: bool,
- simulation_only: bool = false,
-) -> void:
- set_local_control(false)
- _network_authoritative_simulation = authoritative_simulation
- _network_interpolation_enabled = not authoritative_simulation
- set_network_simulation_only(simulation_only)
- _network_axis = Vector2.ZERO
- _network_jump_pending = false
- _network_sprint = false
- _network_sneak = false
- _network_slow_walk = false
- _network_input_age = 0.0
- _network_input_stale = false
- _network_input_stale_timeout_seconds = NETWORK_INPUT_STALE_TIMEOUT_SECONDS
- _network_jump_intent_active = false
- _network_snapshot_ready = false
- _network_snapshot_age = 0.0
- _network_snapshot_jitter = 0.0
- _network_target_grounded = false
- _network_target_locomotion_state = LocomotionState.IDLE
- _network_target_animation_action_id = &""
- _network_target_animation_action_sequence = 0
- _network_target_animation_action_elapsed = 0.0
- _network_target_animation_action_paused = false
- _camera.current = false
- _authoritative_movement_history.clear()
-
-
-func set_network_simulation_only(enabled: bool) -> void:
- _network_simulation_only = enabled
- if not is_node_ready():
- return
- set_process(not enabled)
- for presentation_root: Node in [
- _visuals,
- get_node_or_null("GroundShadow"),
- get_node_or_null("SprintDust"),
- _camera_yaw,
- ]:
- if presentation_root == null:
- continue
- presentation_root.process_mode = (
- Node.PROCESS_MODE_DISABLED
- if enabled
- else Node.PROCESS_MODE_INHERIT
- )
- if presentation_root is Node3D:
- (presentation_root as Node3D).visible = not enabled
-
-
-func is_network_simulation_only() -> bool:
- return _network_simulation_only
-
-
-func reset_network_movement_state() -> void:
- _clear_local_network_jump_intent()
- if not _animation_action_id.is_empty():
- end_animation_action()
- _sit_after_landing = false
- _sitting_intent_pending = false
- _sitting_intent_sequence = -1
- _set_sitting(false)
- _network_axis = Vector2.ZERO
- _network_jump_pending = false
- _network_sprint = false
- _network_sneak = false
- _network_slow_walk = false
- _network_input_age = 0.0
- _network_input_stale = false
- _network_input_stale_timeout_seconds = NETWORK_INPUT_STALE_TIMEOUT_SECONDS
- _network_jump_intent_active = false
- _last_network_input_sequence = 0
- _reset_local_prediction_error()
- _clear_local_reconciliation_offsets()
- _local_prediction_soft_corrections = 0
- _local_prediction_hard_corrections = 0
- _local_prediction_largest_error = 0.0
-
-
-func apply_network_space_transition(
- target_transform: Transform3D,
- align_local_camera: bool,
-) -> void:
- if not target_transform.origin.is_finite():
- return
- # A room transition is a discontinuity, not ordinary movement. Preserve the
- # host's input acknowledgement while clearing every motion, animation, and
- # reconciliation state that could pull the avatar toward the previous space.
- var acknowledged_input: int = _last_network_input_sequence
- reset_network_movement_state()
- _last_network_input_sequence = acknowledged_input
- _apply_network_casting(false)
- velocity = Vector3.ZERO
- global_transform = Transform3D(Basis.IDENTITY, target_transform.origin)
- var target_yaw: float = target_transform.basis.get_euler().y
- _visuals.rotation.y = target_yaw
- _network_camera_yaw = target_yaw
- _network_target_position = global_position
- _network_target_velocity = Vector3.ZERO
- _network_target_visual_yaw = target_yaw
- _network_snapshot_age = 0.0
- _network_snapshot_ready = true
- _authoritative_movement_history.clear()
- if align_local_camera:
- _set_camera_dragging(false)
- if _free_camera_active:
- _set_free_camera_active(false)
- _camera_yaw.rotation.y = target_yaw
-
-
-func capture_network_input(sequence: int) -> Dictionary:
- if _sitting_intent_pending and _sitting_intent_sequence < 0:
- _sitting_intent_sequence = sequence
- if (
- local_control_enabled
- and _is_movement_input_enabled()
- and not _free_camera_active
- and Input.is_action_just_pressed("jump")
- ):
- _queue_local_network_jump_intent()
- if (
- not _is_movement_input_enabled()
- or _water_recovery_active
- or _free_camera_active
- ):
- _clear_local_network_jump_intent()
- return {
- "sequence": sequence,
- "axis": [0.0, 0.0],
- "camera_yaw": _camera_yaw.global_rotation.y,
- "jump": false,
- "sprint": false,
- "sneak": false,
- "slow_walk": false,
- "sitting": get_network_sitting_intent(),
- "casting": (
- _fishing_visual_phase == FishingVisualPhase.CASTING
- ),
- "animation_action": _make_animation_action_state(),
- }
- var axis: Vector2 = Input.get_vector(
- "move_left",
- "move_right",
- "move_forward",
- "move_backward"
- )
- if (
- _local_network_jump_intent_pending
- and _local_network_jump_intent_sequence < 0
- ):
- _local_network_jump_intent_sequence = sequence
- return {
- "sequence": sequence,
- "axis": [axis.x, axis.y],
- "camera_yaw": _camera_yaw.global_rotation.y,
- "jump": _local_network_jump_intent_pending,
- "sprint": Input.is_action_pressed("sprint"),
- "sneak": Input.is_action_pressed("sneak"),
- "slow_walk": Input.is_action_pressed("slow_walk"),
- "sitting": get_network_sitting_intent(),
- "casting": _fishing_visual_phase == FishingVisualPhase.CASTING,
- "animation_action": _make_animation_action_state(),
- }
-
-
-func has_active_network_input() -> bool:
- if not local_control_enabled:
- return false
- if _local_network_jump_intent_pending or _sitting_intent_pending:
- return true
- if not _is_movement_input_enabled() or _free_camera_active:
- return false
- return (
- not Input.get_vector(
- "move_left",
- "move_right",
- "move_forward",
- "move_backward",
- ).is_zero_approx()
- or Input.is_action_pressed("sprint")
- or Input.is_action_pressed("sneak")
- or Input.is_action_pressed("slow_walk")
- or _fishing_visual_phase == FishingVisualPhase.CASTING
- or not _animation_action_id.is_empty()
- )
-
-
-func get_network_input_state_hash() -> int:
- var axis: Vector2 = Vector2.ZERO
- if (
- local_control_enabled
- and _is_movement_input_enabled()
- and not _free_camera_active
- ):
- axis = Input.get_vector(
- "move_left",
- "move_right",
- "move_forward",
- "move_backward",
- )
- return hash([
- axis,
- _camera_yaw.global_rotation.y,
- _local_network_jump_intent_pending,
- Input.is_action_pressed("sprint"),
- Input.is_action_pressed("sneak"),
- Input.is_action_pressed("slow_walk"),
- get_network_sitting_intent(),
- _fishing_visual_phase == FishingVisualPhase.CASTING,
- _animation_action_id,
- _animation_action_sequence,
- _animation_action_paused,
- ])
-
-
-func apply_authoritative_network_input(
- data: Dictionary,
- stale_timeout_seconds: float = NETWORK_INPUT_STALE_TIMEOUT_SECONDS,
-) -> void:
- var sequence: int = int(data.get("sequence", 0))
- if sequence <= _last_network_input_sequence:
- return
- var axis: Array = data.get("axis", [])
- if axis.size() != 2:
- return
- _last_network_input_sequence = sequence
- _network_input_age = 0.0
- _network_input_stale = false
- _network_input_stale_timeout_seconds = clampf(
- stale_timeout_seconds,
- NETWORK_INPUT_STALE_TIMEOUT_SECONDS,
- NETWORK_INPUT_STALE_TIMEOUT_MAX_SECONDS,
- )
- _network_axis = Vector2(float(axis[0]), float(axis[1])).limit_length(1.0)
- _network_camera_yaw = float(data.get("camera_yaw", 0.0))
- var jump_intent_active: bool = bool(data.get("jump", false))
- if jump_intent_active and not _network_jump_intent_active:
- _network_jump_pending = true
- _network_jump_intent_active = jump_intent_active
- _network_sprint = bool(data.get("sprint", false))
- _network_sneak = bool(data.get("sneak", false))
- _network_slow_walk = bool(data.get("slow_walk", false))
- apply_authoritative_network_animation_action(
- data.get("animation_action", {})
- )
- _apply_network_casting(bool(data.get("casting", false)))
- apply_authoritative_network_sitting_state(
- bool(data.get("sitting", false))
- )
-
-
-func make_network_snapshot(peer_id: int) -> Dictionary:
- return {
- "peer_id": peer_id,
- "acknowledged_input": _last_network_input_sequence,
- "position": [global_position.x, global_position.y, global_position.z],
- "velocity": [velocity.x, velocity.y, velocity.z],
- "visual_yaw": _visuals.rotation.y,
- "animation_state": make_network_animation_state(),
- "grounded": is_on_floor(),
- "sitting": get_network_sitting_state(),
- "casting": _fishing_visual_phase == FishingVisualPhase.CASTING,
- }
-
-
-func make_network_animation_state() -> Dictionary:
- return NetworkPlayerAnimationProtocol.make_state(
- _get_authoritative_locomotion_id(),
- is_on_floor(),
- _animation_action_id,
- _animation_action_sequence,
- _animation_action_elapsed,
- _animation_action_paused,
- )
-
-
-func apply_network_animation_state(state: Dictionary) -> void:
- _apply_network_target_animation_state(state)
-
-
-func apply_authoritative_network_animation_action(
- action_state: Dictionary,
-) -> void:
- if not NetworkPlayerAnimationProtocol.validate_action_state(action_state):
- return
- var incoming_sequence: int = int(action_state["sequence"])
- var wrapped_sequence: bool = (
- _animation_action_sequence
- == NetworkPlayerAnimationProtocol.MAX_ACTION_SEQUENCE
- and incoming_sequence == 1
- )
- if incoming_sequence < _animation_action_sequence and not wrapped_sequence:
- return
- if (
- incoming_sequence == _animation_action_sequence
- and StringName(str(action_state["id"])) != _animation_action_id
- ):
- return
- _apply_animation_action_state(action_state)
-
-
-func push_network_snapshot(
- snapshot: Dictionary,
- estimated_transit_seconds: float = -1.0,
-) -> void:
- var parsed: Dictionary = _parse_network_snapshot(snapshot)
- if parsed.is_empty():
- return
- if _network_snapshot_ready:
- var arrival_error: float = absf(
- _network_snapshot_age
- - NETWORK_EXPECTED_SNAPSHOT_INTERVAL_SECONDS
- )
- _network_snapshot_jitter = lerpf(
- _network_snapshot_jitter,
- minf(arrival_error, NETWORK_SNAPSHOT_JITTER_LIMIT_SECONDS),
- NETWORK_SNAPSHOT_JITTER_WEIGHT,
- )
- var transit_seconds: float = (
- clampf(
- estimated_transit_seconds,
- 0.0,
- NETWORK_EXTRAPOLATION_LIMIT_SECONDS,
- )
- if is_finite(estimated_transit_seconds)
- and estimated_transit_seconds >= 0.0
- else 0.0
- )
- _network_target_position = (
- (parsed["position"] as Vector3)
- + (parsed["velocity"] as Vector3) * transit_seconds
- )
- _network_target_velocity = parsed["velocity"]
- _network_target_visual_yaw = parsed["visual_yaw"]
- if not Dictionary(parsed.get("animation_state", {})).is_empty():
- _apply_network_target_animation_state(parsed["animation_state"])
- _network_snapshot_age = 0.0
- _apply_network_casting(bool(parsed["casting"]))
- _set_sitting(bool(parsed["sitting"]))
- if not _network_snapshot_ready:
- global_position = _network_target_position
- velocity = _network_target_velocity
- _visuals.rotation.y = _network_target_visual_yaw
- _network_snapshot_ready = true
-
-
-func apply_local_prediction_correction(
- snapshot: Dictionary,
- latest_input_sequence: int = 0,
- input_interval_seconds: float = 0.0,
- estimated_transit_seconds: float = -1.0,
- audit_delta_seconds: float = 0.0,
-) -> void:
- var parsed: Dictionary = _parse_network_snapshot(snapshot)
- if parsed.is_empty():
- return
- var acknowledged_input: int = parsed["acknowledged_input"]
- var sitting_intent_acknowledged: bool = (
- _sitting_intent_pending
- and _sitting_intent_sequence >= 0
- and acknowledged_input >= _sitting_intent_sequence
- )
- if sitting_intent_acknowledged:
- _sitting_intent_pending = false
- _sitting_intent_sequence = -1
- if (
- _local_network_jump_intent_pending
- and _local_network_jump_intent_sequence >= 0
- and acknowledged_input >= _local_network_jump_intent_sequence
- ):
- _clear_local_network_jump_intent()
- if not _sitting_intent_pending:
- _set_sitting(bool(parsed["sitting"]))
- var authoritative_position: Vector3 = parsed["position"]
- var transit_seconds: float = resolve_local_prediction_transit_seconds(
- acknowledged_input,
- latest_input_sequence,
- input_interval_seconds,
- estimated_transit_seconds,
- )
- if transit_seconds > 0.0:
- authoritative_position += (
- (parsed["velocity"] as Vector3) * transit_seconds
- )
- var error_offset: Vector3 = authoritative_position - global_position
- var error_distance: float = error_offset.length()
- _local_prediction_largest_error = maxf(
- _local_prediction_largest_error,
- error_distance,
- )
- if error_distance <= LOCAL_PREDICTION_CORRECTION_THRESHOLD:
- _reset_local_prediction_error()
- return
- if error_distance >= LOCAL_PREDICTION_SNAP_DISTANCE:
- _clear_local_reconciliation_offsets()
- global_position = authoritative_position
- velocity = parsed["velocity"]
- _local_prediction_hard_corrections += 1
- _reset_local_prediction_error()
- return
- if _should_defer_local_prediction_correction(parsed):
- # A host and its client do not reach the apex or landing on the same
- # frame. Correcting that expected vertical disagreement while either
- # side is airborne can pull the local body off narrow platforms.
- _reset_local_prediction_error()
- return
- # Both simulations agree the player is standing. Let ordinary floor motion
- # and gravity resolve the vertical component; a stale height sample must not
- # press a locally controlled player down or turn a horizontal correction
- # into an early floor collision.
- error_offset.y = 0.0
- error_distance = error_offset.length()
- if error_distance <= LOCAL_PREDICTION_CORRECTION_THRESHOLD:
- _reset_local_prediction_error()
- return
- var error_direction: Vector3 = error_offset.normalized()
- if (
- not _local_prediction_error_direction.is_zero_approx()
- and _local_prediction_error_direction.dot(error_direction) < 0.5
- ):
- _reset_local_prediction_error()
- _local_prediction_error_direction = error_direction
- _local_prediction_error_audits += 1
- _local_prediction_error_seconds += clampf(
- audit_delta_seconds,
- 0.0,
- LOCAL_PREDICTION_MAX_AUDIT_DELTA_SECONDS,
- )
- if (
- _local_prediction_error_audits
- < LOCAL_PREDICTION_MIN_CORRECTION_AUDITS
- or _local_prediction_error_seconds
- < LOCAL_PREDICTION_CORRECTION_DELAY_SECONDS
- ):
- return
- var correction_weight: float = 1.0 - exp(
- -LOCAL_PREDICTION_SOFT_CORRECTION_RATE
- * clampf(
- audit_delta_seconds,
- 0.0,
- LOCAL_PREDICTION_MAX_AUDIT_DELTA_SECONDS,
- )
- )
- var correction: Vector3 = error_offset * correction_weight
- if correction.length() > LOCAL_PREDICTION_MAX_SOFT_CORRECTION_STEP:
- correction = (
- correction.normalized()
- * LOCAL_PREDICTION_MAX_SOFT_CORRECTION_STEP
- )
- if _apply_camera_safe_local_correction(correction):
- _local_prediction_soft_corrections += 1
-
-
-func _should_defer_local_prediction_correction(parsed: Dictionary) -> bool:
- if not bool(parsed.get("grounded", true)):
- return true
- return _local_network_jump_intent_pending or not is_on_floor()
-
-
-func _apply_camera_safe_local_correction(correction: Vector3) -> bool:
- if correction.is_zero_approx():
- return false
- var previous_visual_position: Vector3 = _visuals.global_position
- var previous_camera_position: Vector3 = _camera_yaw.global_position
- var previous_body_position: Vector3 = global_position
- var base_visual_local_position: Vector3 = (
- _visuals.position - _local_reconciliation_visual_offset
- )
- var base_camera_local_position: Vector3 = (
- _camera_yaw.position - _local_reconciliation_camera_offset
- )
- # Reconciliation is still physical movement. Going through the physics
- # server prevents a delayed host audit from placing the capsule through a
- # tree, cliff, or stepping stone on the way to the authoritative position.
- move_and_collide(correction)
- if global_position.is_equal_approx(previous_body_position):
- return false
- _visuals.global_position = previous_visual_position
- _camera_yaw.global_position = previous_camera_position
- _local_reconciliation_visual_offset = (
- _visuals.position - base_visual_local_position
- )
- _local_reconciliation_camera_offset = (
- _camera_yaw.position - base_camera_local_position
- )
- return true
-
-
-func _reset_local_prediction_error() -> void:
- _local_prediction_error_seconds = 0.0
- _local_prediction_error_audits = 0
- _local_prediction_error_direction = Vector3.ZERO
-
-
-func _clear_local_reconciliation_offsets() -> void:
- if not _local_reconciliation_visual_offset.is_zero_approx():
- _visuals.position -= _local_reconciliation_visual_offset
- if not _local_reconciliation_camera_offset.is_zero_approx():
- _camera_yaw.position -= _local_reconciliation_camera_offset
- _local_reconciliation_visual_offset = Vector3.ZERO
- _local_reconciliation_camera_offset = Vector3.ZERO
-
-
-func _update_local_reconciliation_visuals(delta: float) -> void:
- if (
- _local_reconciliation_visual_offset.is_zero_approx()
- and _local_reconciliation_camera_offset.is_zero_approx()
- ):
- _local_reconciliation_visual_offset = Vector3.ZERO
- _local_reconciliation_camera_offset = Vector3.ZERO
- return
- var retained_ratio: float = exp(
- -LOCAL_RECONCILIATION_PRESENTATION_RECENTER_RATE * delta
- )
- var retained_visual_offset: Vector3 = (
- _local_reconciliation_visual_offset * retained_ratio
- )
- var retained_camera_offset: Vector3 = (
- _local_reconciliation_camera_offset * retained_ratio
- )
- _visuals.position += (
- retained_visual_offset - _local_reconciliation_visual_offset
- )
- _camera_yaw.position += (
- retained_camera_offset - _local_reconciliation_camera_offset
- )
- _local_reconciliation_visual_offset = retained_visual_offset
- _local_reconciliation_camera_offset = retained_camera_offset
-
-
-func get_local_prediction_metrics() -> Dictionary:
- return {
- "soft_corrections": _local_prediction_soft_corrections,
- "hard_corrections": _local_prediction_hard_corrections,
- "largest_error": _local_prediction_largest_error,
- "out_of_bounds_audits": _local_prediction_error_audits,
- "out_of_bounds_seconds": _local_prediction_error_seconds,
- }
-
-
-static func resolve_network_input_stale_timeout_seconds(
- round_trip_msec: int,
-) -> float:
- if round_trip_msec < 0:
- return NETWORK_INPUT_STALE_TIMEOUT_SECONDS
- return clampf(
- NETWORK_INPUT_STALE_TIMEOUT_SECONDS
- + float(round_trip_msec) / 1000.0
- * NETWORK_INPUT_STALE_RTT_MULTIPLIER,
- NETWORK_INPUT_STALE_TIMEOUT_SECONDS,
- NETWORK_INPUT_STALE_TIMEOUT_MAX_SECONDS,
- )
-
-
-func _record_authoritative_movement_state() -> void:
- var now_seconds: float = Time.get_ticks_msec() / 1000.0
- _authoritative_movement_history.append({
- "sequence": _last_network_input_sequence,
- "recorded_at": now_seconds,
- "position": global_position,
- "velocity": velocity,
- "cast_origin": get_cast_origin_position(),
- "facing": get_facing_direction(),
- "sneaking": is_sneaking(),
- })
- var oldest_allowed: float = now_seconds - NETWORK_MOVEMENT_HISTORY_SECONDS
- while (
- not _authoritative_movement_history.is_empty()
- and float(
- _authoritative_movement_history[0].get("recorded_at", 0.0)
- ) < oldest_allowed
- ):
- _authoritative_movement_history.pop_front()
-
-
-func get_lag_compensated_movement_state(
- input_sequence: int,
- max_age_seconds: float = NETWORK_MAX_LAG_COMPENSATION_SECONDS,
-) -> Dictionary:
- var now_seconds: float = Time.get_ticks_msec() / 1000.0
- for index: int in range(
- _authoritative_movement_history.size() - 1,
- -1,
- -1,
- ):
- var candidate: Dictionary = _authoritative_movement_history[index]
- if now_seconds - float(candidate.get("recorded_at", 0.0)) > (
- max_age_seconds
- ):
- break
- if (
- input_sequence <= 0
- or int(candidate.get("sequence", 0)) <= input_sequence
- ):
- return candidate.duplicate(true)
- return {
- "sequence": _last_network_input_sequence,
- "recorded_at": now_seconds,
- "position": global_position,
- "velocity": velocity,
- "cast_origin": get_cast_origin_position(),
- "facing": get_facing_direction(),
- "sneaking": is_sneaking(),
- }
-
-
-static func resolve_local_prediction_transit_seconds(
- acknowledged_input: int,
- latest_input_sequence: int,
- input_interval_seconds: float,
- estimated_transit_seconds: float,
-) -> float:
- if is_finite(estimated_transit_seconds) and estimated_transit_seconds >= 0.0:
- return minf(
- estimated_transit_seconds,
- LOCAL_PREDICTION_EXTRAPOLATION_LIMIT_SECONDS,
- )
- if (
- acknowledged_input <= 0
- or latest_input_sequence <= acknowledged_input
- or input_interval_seconds <= 0.0
- ):
- return 0.0
- # The input gap spans approximately the full round trip: from the input
- # acknowledged by the host to the newest input at snapshot receipt. Only
- # half of that interval lies between the host snapshot and the client now.
- var sequence_gap: int = latest_input_sequence - acknowledged_input
- return minf(
- float(sequence_gap)
- * input_interval_seconds
- * LOCAL_PREDICTION_FALLBACK_TRANSIT_RATIO,
- LOCAL_PREDICTION_EXTRAPOLATION_LIMIT_SECONDS,
- )
-
-
-func apply_network_teleport(snapshot: Dictionary) -> void:
- var parsed: Dictionary = _parse_network_snapshot(snapshot)
- if parsed.is_empty():
- return
- global_position = parsed["position"]
- velocity = parsed["velocity"]
- _visuals.rotation.y = parsed["visual_yaw"]
- _apply_network_casting(bool(parsed["casting"]))
- _set_sitting(bool(parsed["sitting"]))
- _network_target_position = global_position
- _network_target_velocity = velocity
- _network_target_visual_yaw = _visuals.rotation.y
- if not Dictionary(parsed.get("animation_state", {})).is_empty():
- _apply_network_target_animation_state(parsed["animation_state"])
- _network_snapshot_age = 0.0
- _network_snapshot_ready = true
-
-
-func _parse_network_snapshot(snapshot: Dictionary) -> Dictionary:
- var snapshot_position: Variant = snapshot.get("position")
- var network_velocity: Variant = snapshot.get("velocity")
- if (
- typeof(snapshot_position) != TYPE_ARRAY
- or typeof(network_velocity) != TYPE_ARRAY
- or snapshot_position.size() != 3
- or network_velocity.size() != 3
- ):
- return {}
- var parsed_position := Vector3(
- float(snapshot_position[0]),
- float(snapshot_position[1]),
- float(snapshot_position[2])
- )
- var parsed_velocity := Vector3(
- float(network_velocity[0]),
- float(network_velocity[1]),
- float(network_velocity[2])
- )
- var visual_yaw: float = float(snapshot.get("visual_yaw", 0.0))
- var animation_state: Dictionary = {}
- if snapshot.has("animation_state"):
- var animation_state_value: Variant = snapshot.get("animation_state")
- if not NetworkPlayerAnimationProtocol.validate_state(
- animation_state_value
- ):
- return {}
- animation_state = (
- animation_state_value as Dictionary
- ).duplicate(true)
- if (
- snapshot.has("acknowledged_input")
- and (
- typeof(snapshot.get("acknowledged_input")) != TYPE_INT
- or int(snapshot.get("acknowledged_input")) < 0
- )
- ):
- return {}
- var acknowledged_input: int = int(
- snapshot.get("acknowledged_input", 0)
- )
- if snapshot.has("sitting") and typeof(snapshot.get("sitting")) != TYPE_BOOL:
- return {}
- var sitting: bool = bool(snapshot.get("sitting", false))
- if snapshot.has("casting") and typeof(snapshot.get("casting")) != TYPE_BOOL:
- return {}
- var casting: bool = bool(snapshot.get("casting", false))
- if snapshot.has("grounded") and typeof(snapshot.get("grounded")) != TYPE_BOOL:
- return {}
- var grounded: bool = bool(snapshot.get("grounded", true))
- if (
- not parsed_position.is_finite()
- or not parsed_velocity.is_finite()
- or not is_finite(visual_yaw)
- ):
- return {}
- return {
- "position": parsed_position,
- "velocity": parsed_velocity,
- "visual_yaw": visual_yaw,
- "animation_state": animation_state,
- "grounded": grounded,
- "acknowledged_input": acknowledged_input,
- "sitting": sitting,
- "casting": casting,
- }
-
-
-func _make_animation_action_state() -> Dictionary:
- return NetworkPlayerAnimationProtocol.make_action_state(
- _animation_action_id,
- _animation_action_sequence,
- _animation_action_elapsed,
- _animation_action_paused,
- )
-
-
-func _apply_network_target_animation_state(state: Dictionary) -> void:
- if not NetworkPlayerAnimationProtocol.validate_state(state):
- return
- _network_target_grounded = bool(state["grounded"])
- _network_target_locomotion_state = _locomotion_state_from_id(
- StringName(str(state["locomotion_id"]))
- )
- var action: Dictionary = state["action"]
- var incoming_sequence: int = int(action["sequence"])
- var wrapped_sequence: bool = (
- _network_target_animation_action_sequence
- == NetworkPlayerAnimationProtocol.MAX_ACTION_SEQUENCE
- and incoming_sequence == 1
- )
- if (
- incoming_sequence < _network_target_animation_action_sequence
- and not wrapped_sequence
- ):
- return
- if (
- incoming_sequence == _network_target_animation_action_sequence
- and StringName(str(action["id"]))
- != _network_target_animation_action_id
- ):
- return
- _network_target_animation_action_id = StringName(str(action["id"]))
- _network_target_animation_action_sequence = int(action["sequence"])
- _network_target_animation_action_elapsed = float(action["elapsed"])
- _network_target_animation_action_paused = bool(
- action.get("paused", false)
- )
-
-
-func _get_authoritative_locomotion_state() -> LocomotionState:
- if (
- _network_authoritative_simulation
- and _is_movement_input_enabled()
- and not _water_recovery_active
- and not _sitting
- ):
- if _network_sneak:
- return LocomotionState.SNEAKING
- if _network_axis.length_squared() <= 0.0025:
- return LocomotionState.IDLE
- return (
- LocomotionState.RUNNING
- if _network_sprint
- else LocomotionState.WALKING
- )
- return _locomotion_state_from_velocity(is_sneaking())
-
-
-func _get_authoritative_locomotion_id() -> StringName:
- return _locomotion_id_from_state(_get_authoritative_locomotion_state())
-
-
-func _get_presented_locomotion_state() -> LocomotionState:
- if _network_interpolation_enabled and _network_snapshot_ready:
- return _network_target_locomotion_state
- return _locomotion_state_from_velocity(is_sneaking())
-
-
-func _is_presented_moving_horizontally() -> bool:
- var presented_velocity: Vector3 = (
- _network_target_velocity
- if _network_interpolation_enabled and _network_snapshot_ready
- else velocity
- )
- return Vector2(
- presented_velocity.x,
- presented_velocity.z,
- ).length_squared() > 0.0025
-
-
-func _get_presented_animation_action() -> Dictionary:
- if _network_interpolation_enabled and _network_snapshot_ready:
- return NetworkPlayerAnimationProtocol.make_action_state(
- _network_target_animation_action_id,
- _network_target_animation_action_sequence,
- minf(
- _network_target_animation_action_elapsed
- + (
- 0.0
- if _network_target_animation_action_paused
- else _network_snapshot_age
- ),
- NetworkPlayerAnimationProtocol.MAX_ACTION_ELAPSED_SECONDS,
- ),
- _network_target_animation_action_paused,
- )
- return _make_animation_action_state()
-
-
-func _locomotion_id_from_state(state: LocomotionState) -> StringName:
- match state:
- LocomotionState.SNEAKING:
- return NetworkPlayerAnimationProtocol.LOCOMOTION_SNEAKING
- LocomotionState.WALKING:
- return NetworkPlayerAnimationProtocol.LOCOMOTION_WALKING
- LocomotionState.RUNNING:
- return NetworkPlayerAnimationProtocol.LOCOMOTION_RUNNING
- _:
- return NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE
-
-
-func _locomotion_state_from_id(state_id: StringName) -> LocomotionState:
- match state_id:
- NetworkPlayerAnimationProtocol.LOCOMOTION_SNEAKING:
- return LocomotionState.SNEAKING
- NetworkPlayerAnimationProtocol.LOCOMOTION_WALKING:
- return LocomotionState.WALKING
- NetworkPlayerAnimationProtocol.LOCOMOTION_RUNNING:
- return LocomotionState.RUNNING
- _:
- return LocomotionState.IDLE
-
-
-static func supports_network_animation_action(action_id: StringName) -> bool:
- return action_id in NETWORK_ANIMATION_ACTION_IDS
-
-
-func _locomotion_state_from_velocity(
- sneaking: bool = false,
-) -> LocomotionState:
- if sneaking:
- return LocomotionState.SNEAKING
- var horizontal_speed_squared: float = (
- velocity.x * velocity.x + velocity.z * velocity.z
- )
- if horizontal_speed_squared <= 0.0025:
- return LocomotionState.IDLE
- var fastest_non_sprint_speed := maxf(
- walk_speed,
- maxf(sneak_speed, slow_walk_speed)
- )
- var running_threshold := fastest_non_sprint_speed + 0.5
- if horizontal_speed_squared > running_threshold * running_threshold:
- return LocomotionState.RUNNING
- return LocomotionState.WALKING
-
-
-func _get_presented_grounded() -> bool:
- if _network_interpolation_enabled and _network_snapshot_ready:
- return _network_target_grounded
- return is_on_floor()
-
-
-func _apply_network_casting(casting: bool) -> void:
- if casting:
- if _fishing_visual_phase == FishingVisualPhase.NONE:
- set_casting_visual()
- elif _fishing_visual_phase == FishingVisualPhase.CASTING:
- set_fishing_visual(false)
-
-
-func _update_network_interpolation(delta: float) -> void:
- if not _network_snapshot_ready:
- return
- var previous_snapshot_age: float = _network_snapshot_age
- _network_snapshot_age = minf(
- _network_snapshot_age + delta,
- NETWORK_EXTRAPOLATION_LIMIT_SECONDS,
- )
- var extrapolation_delta: float = (
- _network_snapshot_age - previous_snapshot_age
- )
- var predicted_position: Vector3 = (
- _network_target_position
- + _network_target_velocity * _network_snapshot_age
- )
- var projected_position: Vector3 = (
- global_position + _network_target_velocity * extrapolation_delta
- )
- if projected_position.distance_to(predicted_position) > 2.0:
- global_position = predicted_position
- else:
- var jitter_ratio: float = clampf(
- _network_snapshot_jitter
- / NETWORK_SNAPSHOT_JITTER_LIMIT_SECONDS,
- 0.0,
- 1.0,
- )
- var smoothing_rate: float = lerpf(
- NETWORK_REMOTE_SMOOTHING_RATE,
- NETWORK_REMOTE_JITTER_SMOOTHING_RATE,
- jitter_ratio,
- )
- global_position = projected_position.lerp(
- predicted_position,
- 1.0 - exp(-smoothing_rate * delta)
- )
- velocity = _network_target_velocity
- _visuals.rotation.y = lerp_angle(
- _visuals.rotation.y,
- _network_target_visual_yaw,
- 1.0 - exp(-14.0 * delta)
- )
-
-
-func _update_network_input_freshness(delta: float) -> void:
- _network_input_age = minf(
- _network_input_age + delta,
- _network_input_stale_timeout_seconds + 1.0,
- )
- if (
- _network_input_stale
- or _network_input_age <= _network_input_stale_timeout_seconds
- ):
- return
- _network_input_stale = true
- _network_axis = Vector2.ZERO
- _network_jump_pending = false
- _network_sprint = false
- _network_sneak = false
- _network_slow_walk = false
-
-
-func _queue_local_network_jump_intent() -> void:
- if _local_network_jump_intent_pending:
- return
- _local_network_jump_intent_pending = true
- _local_network_jump_intent_sequence = -1
-
-
-func _clear_local_network_jump_intent() -> void:
- _local_network_jump_intent_pending = false
- _local_network_jump_intent_sequence = -1
+ _camera_dragging = false
func is_local_control_enabled() -> bool:
return local_control_enabled
-func is_sneaking() -> bool:
- if local_control_enabled:
- return (
- _is_movement_input_enabled()
- and Input.is_action_pressed("sneak")
- )
- return _network_sneak
-
-
-func is_moving_horizontally() -> bool:
- return Vector2(velocity.x, velocity.z).length_squared() > 0.01
-
-
func set_movement_enabled(enabled: bool) -> void:
_movement_enabled = enabled
if not enabled:
@@ -3002,61 +280,16 @@ func is_movement_enabled() -> bool:
return _movement_enabled
-func set_local_input_suppressed(owner: StringName, suppressed: bool) -> void:
- if owner.is_empty():
- return
- if suppressed:
- _local_input_suppressors[owner] = true
- velocity.x = 0.0
- velocity.z = 0.0
- _set_camera_dragging(false)
- else:
- _local_input_suppressors.erase(owner)
-
-
-func _is_movement_input_enabled() -> bool:
- return _movement_enabled and _local_input_suppressors.is_empty()
-
-
-func _is_camera_input_enabled() -> bool:
- return (
- _camera_input_enabled
- and _local_input_suppressors.is_empty()
- and _camera_input_suppressors.is_empty()
- )
-
-
func set_camera_input_enabled(enabled: bool) -> void:
_camera_input_enabled = enabled
if not enabled:
- _set_camera_dragging(false)
-
-
-func set_camera_input_suppressed(
- owner: StringName,
- suppressed: bool,
-) -> void:
- if owner.is_empty():
- return
- if suppressed:
- _camera_input_suppressors[owner] = true
- _set_camera_dragging(false)
- else:
- _camera_input_suppressors.erase(owner)
-
-
-func set_camera_active(active: bool) -> void:
- if _free_camera_active and _free_camera != null:
- _free_camera.current = active
- else:
- _camera.current = active
+ _camera_dragging = false
func apply_camera_settings(
new_mouse_sensitivity: float,
new_controller_sensitivity: float,
invert_vertical: bool,
- swap_hotbar_camera_scroll: bool = false,
) -> void:
mouse_sensitivity = clampf(new_mouse_sensitivity, 0.001, 0.012)
controller_camera_speed = clampf(
@@ -3065,50 +298,6 @@ func apply_camera_settings(
5.0
)
invert_camera_y = invert_vertical
- _swap_hotbar_camera_scroll = swap_hotbar_camera_scroll
-
-
-func set_rv_interior_camera_mode(
- enabled: bool,
- excluded_wall_rids: Array[RID] = [],
-) -> void:
- if enabled != _rv_interior_camera_mode:
- if enabled:
- _rv_previous_camera_environment = _camera.environment
- _camera.environment = _get_rv_interior_environment()
- if _free_camera != null:
- _free_camera.environment = _camera.environment
- else:
- _camera.environment = _rv_previous_camera_environment
- if _free_camera != null:
- _free_camera.environment = _camera.environment
- _rv_previous_camera_environment = null
- _rv_interior_camera_mode = enabled
- if _spring_arm == null:
- return
- _spring_arm.clear_excluded_objects()
- if enabled:
- for collider_rid: RID in excluded_wall_rids:
- if collider_rid.is_valid():
- _spring_arm.add_excluded_object(collider_rid)
- _set_target_zoom(_target_zoom)
-
-
-func _get_rv_interior_environment() -> Environment:
- if _rv_interior_environment != null:
- return _rv_interior_environment
- _rv_interior_environment = Environment.new()
- _rv_interior_environment.background_mode = Environment.BG_COLOR
- _rv_interior_environment.background_color = Color("020506")
- _rv_interior_environment.ambient_light_source = (
- Environment.AMBIENT_SOURCE_COLOR
- )
- _rv_interior_environment.ambient_light_color = Color("d6caaa")
- _rv_interior_environment.ambient_light_energy = 1.0
- _rv_interior_environment.reflected_light_source = (
- Environment.REFLECTION_SOURCE_DISABLED
- )
- return _rv_interior_environment
func is_camera_input_enabled() -> bool:
@@ -3120,21 +309,6 @@ func set_water_recovery_active(active: bool) -> void:
velocity = Vector3.ZERO
-func is_water_recovery_active() -> bool:
- return _water_recovery_active
-
-
-func set_remote_recovery_presentation(active: bool) -> void:
- if local_control_enabled or active == _remote_recovery_presentation_active:
- return
- _remote_recovery_presentation_active = active
- _remote_recovery_elapsed = 0.0
- if active:
- _remote_recovery_visual_origin = _visuals.position
- else:
- _visuals.position = _remote_recovery_visual_origin
-
-
func prepare_for_water_recovery() -> void:
_restore_gameplay_presentation_for_recovery()
@@ -3157,42 +331,10 @@ func get_body_center_position() -> Vector3:
return global_position + Vector3.UP * body_center_height
-func get_chat_anchor_position() -> Vector3:
- var highest_y := -INF
- for node: Node in _visuals.find_children("*", "MeshInstance3D", true, false):
- var mesh_instance := node as MeshInstance3D
- if mesh_instance == null or not mesh_instance.is_visible_in_tree():
- continue
- var bounds := mesh_instance.get_aabb()
- for endpoint_index: int in range(8):
- var world_point := (
- mesh_instance.global_transform * bounds.get_endpoint(endpoint_index)
- )
- highest_y = maxf(highest_y, world_point.y)
- if not is_finite(highest_y):
- highest_y = get_body_center_position().y + body_center_height
- return Vector3(global_position.x, highest_y + 0.22, global_position.z)
-
-
func get_body_center_height() -> float:
return body_center_height
-func get_gameplay_camera() -> Camera3D:
- return _camera
-
-
-func set_remote_presentation_visible(value: bool) -> void:
- if local_control_enabled:
- return
- _remote_presentation_visible = value
- _visuals.visible = value
-
-
-func is_remote_presentation_visible() -> bool:
- return _remote_presentation_visible
-
-
func get_fishing_rod_tip() -> Marker3D:
return _fishing_rod_tip
@@ -3201,462 +343,11 @@ func get_fishing_rod() -> Node3D:
return _fishing_rod
-func set_active_fishing_rod(
- rod: FishingRodDataType,
- animate_transition: bool = false,
-) -> void:
- _apply_fishing_rod_model(rod)
- set_active_item_is_rod(rod != null and rod.is_available(), animate_transition)
-
-
-func set_active_catching_net(should_show: bool) -> void:
- _active_item_is_net = should_show
- if not should_show:
- cancel_net_action_visual()
- if _catching_net == null:
- return
- if _showcase_rod_state_stored:
- _showcase_net_visibility = should_show
- return
- _catching_net.visible = (
- should_show and not _has_held_show_item() and not _showcase_animation_active
- )
-
-
-func set_active_shovel(should_show: bool) -> void:
- _active_item_is_shovel = should_show
- if not should_show:
- cancel_net_action_visual()
- if _shovel == null:
- return
- if _showcase_rod_state_stored:
- _showcase_shovel_visibility = should_show
- return
- _shovel.visible = (
- should_show and not _has_held_show_item() and not _showcase_animation_active
- )
-
-
-func set_active_decor_package(should_show: bool) -> void:
- var changed: bool = _held_decor_package_visible != should_show
- _held_decor_package_visible = should_show
- if _held_decor_package_display != null:
- _held_decor_package_display.visible = should_show
- if should_show:
- _fishing_rod.visible = false
- if _catching_net != null:
- _catching_net.visible = false
- if _shovel != null:
- _shovel.visible = false
- elif changed:
- _apply_active_rod_visibility()
- if changed:
- _update_character_animation()
-
-
-func _apply_fishing_rod_model(rod: FishingRodDataType) -> void:
- var next_id: StringName = rod.item_id if rod != null else StringName()
- if next_id == _active_fishing_rod_id:
- return
- _active_fishing_rod_id = next_id
- if _custom_fishing_rod_visual != null:
- _custom_fishing_rod_visual.hide()
- _custom_fishing_rod_visual.queue_free()
- _custom_fishing_rod_visual = null
- if _fishing_rod_fallback_visual == null or _fishing_rod_tip == null:
- return
- _fishing_rod_fallback_visual.show()
- _fishing_rod_tip.position = Vector3(0.0, 1.1, 0.0)
- if rod == null:
- return
- _fishing_rod_tip.position = rod.rod_tip_position
- if rod.rod_model_scene == null or _fishing_rod_model_mount == null:
- return
- var instance := rod.rod_model_scene.instantiate() as Node3D
- if instance == null:
- push_warning(
- "Rod %s model root is not Node3D; using fallback."
- % rod.item_id
- )
- return
- _custom_fishing_rod_visual = instance
- _fishing_rod_model_mount.add_child(instance)
- instance.transform = rod.rod_model_transform
- _fishing_rod_fallback_visual.hide()
-
-
-func set_active_item_is_rod(
- active_is_rod: bool,
- animate_transition: bool = false,
-) -> void:
- _active_item_is_rod = active_is_rod
+func set_active_item_is_rod(active_is_rod: bool) -> void:
if _showcase_rod_state_stored:
_showcase_rod_visibility = active_is_rod
- return
- var visibility_changed: bool = _fishing_rod.visible != active_is_rod
- if not animate_transition or not visibility_changed:
+ else:
_fishing_rod.visible = active_is_rod
- return
- if _has_held_show_item() or _showcase_animation_active:
- if not active_is_rod:
- _fishing_rod.visible = false
- return
- if active_is_rod:
- _fishing_rod.visible = false
- _begin_pocket_visual(
- PocketVisualTarget.IDLE_ITEM,
- false,
- false,
- _apply_active_rod_visibility,
- )
-
-
-func _apply_active_rod_visibility() -> void:
- if _showcase_rod_state_stored:
- _showcase_rod_visibility = _active_item_is_rod
- return
- _fishing_rod.visible = _active_item_is_rod
- if _catching_net != null:
- _catching_net.visible = (
- _active_item_is_net
- and not _has_held_show_item()
- and not _showcase_animation_active
- )
- if _shovel != null:
- _shovel.visible = (
- _active_item_is_shovel
- and not _has_held_show_item()
- and not _showcase_animation_active
- )
-
-
-func set_active_art_kit(icon: Texture2D, should_show: bool) -> void:
- var new_visible: bool = should_show and icon != null
- if not new_visible:
- if _held_art_kit_visible:
- _begin_pocket_visual(
- PocketVisualTarget.ART_KIT,
- true,
- _held_fish_visible,
- _clear_active_art_kit,
- true,
- _apply_pending_held_fish,
- )
- else:
- _clear_active_art_kit()
- return
- var previous_show_pose: bool = _has_held_show_item()
- var item_changed: bool = (
- not _held_art_kit_visible
- or _held_art_kit_sprite.texture != icon
- )
- _held_art_kit_visible = true
- _held_art_kit_sprite.texture = icon
- _held_art_kit_sprite.visible = true
- if not previous_show_pose:
- _held_art_kit_display.visible = false
- _fishing_rod.visible = false
- if _catching_net != null:
- _catching_net.visible = false
- if _shovel != null:
- _shovel.visible = false
- if item_changed:
- _begin_pocket_visual(
- PocketVisualTarget.ART_KIT,
- previous_show_pose,
- true,
- Callable(),
- false,
- _apply_active_art_kit_visual,
- )
- else:
- _apply_active_art_kit_visual()
- _update_character_animation()
-
-
-func set_held_fish(
- fish: FishDataType,
- display_scale: float,
- should_show: bool,
- animate_put_away: bool = false,
-) -> void:
- if not should_show or fish == null or fish.display_texture == null:
- if animate_put_away and _held_fish_visible:
- _begin_pocket_visual(
- PocketVisualTarget.HELD_FISH,
- true,
- _held_art_kit_visible,
- _clear_held_fish,
- )
- else:
- if _pocket_visual_target == PocketVisualTarget.HELD_FISH:
- _cancel_pocket_visual()
- _clear_held_fish()
- return
- var previous_show_pose: bool = _has_held_show_item()
- var item_changed: bool = (
- not _held_fish_visible
- or _held_fish_sprite.texture != fish.display_texture
- )
- _held_fish_visible = true
- _pending_held_fish_texture = fish.display_texture
- _pending_held_fish_scale = _get_catch_presentation_scale(
- fish,
- display_scale,
- )
- if not previous_show_pose:
- _held_fish_display.visible = false
- _fishing_rod.visible = false
- if _catching_net != null:
- _catching_net.visible = false
- if _shovel != null:
- _shovel.visible = false
- if item_changed:
- _begin_pocket_visual(
- PocketVisualTarget.HELD_FISH,
- previous_show_pose,
- true,
- Callable(),
- false,
- _apply_pending_held_fish,
- )
- else:
- _apply_pending_held_fish()
- _update_character_animation()
-
-
-func _has_held_show_item() -> bool:
- return (
- _held_fish_visible
- or _held_art_kit_visible
- or _held_decor_package_visible
- )
-
-
-func _build_decor_package_display(parent: Node3D) -> Node3D:
- var display := Node3D.new()
- display.name = "HeldDecorPackageDisplay"
- display.position = Vector3(0.0, 0.1, 0.04)
- display.rotation = Vector3(0.15, 0.0, -0.55)
- display.visible = false
- parent.add_child(display)
-
- var sack := MeshInstance3D.new()
- sack.name = "Sack"
- var sack_mesh := SphereMesh.new()
- sack_mesh.radius = 0.22
- sack_mesh.height = 0.48
- sack_mesh.radial_segments = 8
- sack_mesh.rings = 4
- sack_mesh.material = _decor_package_material(Color("a87843"))
- sack.mesh = sack_mesh
- display.add_child(sack)
-
- var tie := MeshInstance3D.new()
- tie.name = "Tie"
- var tie_mesh := CylinderMesh.new()
- tie_mesh.top_radius = 0.1
- tie_mesh.bottom_radius = 0.12
- tie_mesh.height = 0.08
- tie_mesh.radial_segments = 8
- tie_mesh.material = _decor_package_material(Color("513829"))
- tie.mesh = tie_mesh
- tie.position = Vector3(0.0, 0.22, 0.0)
- display.add_child(tie)
- return display
-
-
-func _decor_package_material(color: Color) -> StandardMaterial3D:
- var material := StandardMaterial3D.new()
- material.albedo_color = color
- material.roughness = 1.0
- material.shading_mode = BaseMaterial3D.SHADING_MODE_PER_VERTEX
- return material
-
-
-func _clear_held_fish() -> void:
- var was_visible: bool = _held_fish_visible
- _held_fish_visible = false
- _pending_held_fish_texture = null
- _pending_held_fish_scale = Vector3.ONE
- _held_fish_display.visible = false
- _held_fish_display.scale = Vector3.ONE
- _held_fish_sprite.texture = null
- if was_visible:
- _apply_active_rod_visibility()
- _update_character_animation()
-
-
-func _apply_pending_held_fish() -> void:
- if not _held_fish_visible or _pending_held_fish_texture == null:
- return
- _held_fish_sprite.texture = _pending_held_fish_texture
- _held_fish_display.scale = _pending_held_fish_scale
- _held_fish_display.visible = true
-
-
-func _clear_active_art_kit() -> void:
- var was_visible: bool = _held_art_kit_visible
- _held_art_kit_visible = false
- _held_art_kit_display.visible = false
- _held_art_kit_sprite.texture = null
- _held_art_kit_sprite.visible = false
- if was_visible:
- _apply_active_rod_visibility()
- if _held_fish_visible:
- _apply_pending_held_fish()
- _update_character_animation()
-
-
-func _apply_active_art_kit_visual() -> void:
- if not _held_art_kit_visible or _held_art_kit_sprite.texture == null:
- return
- _held_art_kit_display.visible = true
-
-
-func _begin_pocket_visual(
- target: PocketVisualTarget,
- starts_in_show_pose: bool,
- ends_in_show_pose: bool,
- finished_callback: Callable,
- callback_runs_on_interrupt: bool = true,
- midpoint_callback: Callable = Callable(),
-) -> void:
- var animation_name: StringName
- if _sitting:
- if starts_in_show_pose:
- animation_name = (
- CHARACTER_POCKET_SIT_SHOW_SHOW_ANIMATION
- if ends_in_show_pose
- else CHARACTER_POCKET_SIT_SHOW_IDLE_ANIMATION
- )
- else:
- animation_name = (
- CHARACTER_POCKET_SIT_IDLE_SHOW_ANIMATION
- if ends_in_show_pose
- else CHARACTER_POCKET_SIT_IDLE_IDLE_ANIMATION
- )
- elif _is_moving_for_pocket_visual():
- if starts_in_show_pose:
- animation_name = (
- CHARACTER_POCKET_WALKING_SHOW_SHOW_ANIMATION
- if ends_in_show_pose
- else CHARACTER_POCKET_WALKING_SHOW_IDLE_ANIMATION
- )
- else:
- animation_name = (
- CHARACTER_POCKET_WALKING_IDLE_SHOW_ANIMATION
- if ends_in_show_pose
- else CHARACTER_POCKET_WALKING_IDLE_IDLE_ANIMATION
- )
- elif starts_in_show_pose:
- animation_name = (
- CHARACTER_POCKET_SHOW_SHOW_ANIMATION
- if ends_in_show_pose
- else CHARACTER_POCKET_SHOW_IDLE_ANIMATION
- )
- else:
- animation_name = (
- CHARACTER_POCKET_IDLE_SHOW_ANIMATION
- if ends_in_show_pose
- else CHARACTER_POCKET_IDLE_IDLE_ANIMATION
- )
- if _pocket_visual_active:
- if (
- _pocket_visual_target == target
- and _pocket_visual_animation == animation_name
- ):
- return
- if _pocket_visual_callback_runs_on_interrupt:
- _complete_pocket_visual()
- else:
- _cancel_pocket_visual()
- if (
- animation_name.is_empty()
- or _character_animation_player == null
- or not _character_animation_player.has_animation(animation_name)
- ):
- if midpoint_callback.is_valid():
- midpoint_callback.call()
- if finished_callback.is_valid():
- finished_callback.call()
- _update_character_animation()
- return
- _pocket_visual_active = true
- _pocket_visual_target = target
- _pocket_visual_animation = animation_name
- _pocket_visual_finished_callback = finished_callback
- _pocket_visual_callback_runs_on_interrupt = callback_runs_on_interrupt
- _pocket_visual_midpoint_callback = midpoint_callback
- _pocket_visual_midpoint_called = false
- _pocket_visual_generation += 1
- var midpoint_generation: int = _pocket_visual_generation
- _update_character_animation()
- _schedule_pocket_visual_midpoint(midpoint_generation, animation_name)
-
-
-func _is_moving_for_pocket_visual() -> bool:
- var horizontal_velocity := Vector2(velocity.x, velocity.z)
- return horizontal_velocity.length_squared() > 0.0025
-
-
-func _schedule_pocket_visual_midpoint(
- generation: int,
- animation_name: StringName,
-) -> void:
- if not _pocket_visual_midpoint_callback.is_valid():
- return
- var animation: Animation = _character_animation_player.get_animation(
- animation_name
- )
- var midpoint_seconds: float = maxf(animation.length * 0.5, 0.0)
- if midpoint_seconds > 0.0:
- await get_tree().create_timer(midpoint_seconds, false).timeout
- if (
- generation != _pocket_visual_generation
- or not _pocket_visual_active
- or _pocket_visual_animation != animation_name
- ):
- return
- _apply_pocket_visual_midpoint()
-
-
-func _apply_pocket_visual_midpoint() -> void:
- if _pocket_visual_midpoint_called:
- return
- _pocket_visual_midpoint_called = true
- var midpoint_callback: Callable = _pocket_visual_midpoint_callback
- _pocket_visual_midpoint_callback = Callable()
- if midpoint_callback.is_valid():
- midpoint_callback.call()
-
-
-func _complete_pocket_visual() -> void:
- if not _pocket_visual_active:
- return
- var finished_callback: Callable = _pocket_visual_finished_callback
- _pocket_visual_active = false
- _pocket_visual_target = PocketVisualTarget.NONE
- _pocket_visual_animation = &""
- _pocket_visual_finished_callback = Callable()
- _pocket_visual_callback_runs_on_interrupt = true
- _pocket_visual_midpoint_callback = Callable()
- _pocket_visual_midpoint_called = false
- _pocket_visual_generation += 1
- if finished_callback.is_valid():
- finished_callback.call()
- _update_character_animation()
-
-
-func _cancel_pocket_visual() -> void:
- _pocket_visual_active = false
- _pocket_visual_target = PocketVisualTarget.NONE
- _pocket_visual_animation = &""
- _pocket_visual_finished_callback = Callable()
- _pocket_visual_callback_runs_on_interrupt = true
- _pocket_visual_midpoint_callback = Callable()
- _pocket_visual_midpoint_called = false
- _pocket_visual_generation += 1
func get_cast_origin_position() -> Vector3:
@@ -3666,169 +357,31 @@ func get_cast_origin_position() -> Vector3:
func begin_catch_showcase(fish_catch: FishCatchType) -> void:
if fish_catch == null or not fish_catch.is_valid():
return
- if _is_net_strike_visual_active():
- _pending_net_showcase_catch = fish_catch
- _pending_net_showcase_remote = false
- return
- _begin_catch_showcase_now(fish_catch, showcase_camera_yaw_offset)
-
-
-func _begin_catch_showcase_now(
- fish_catch: FishCatchType,
- camera_yaw_offset_degrees: float,
-) -> void:
- if _pocket_visual_target == PocketVisualTarget.CATCH_SHOWCASE:
- _cancel_pocket_visual()
- if local_control_enabled:
- set_local_input_suppressed(CATCH_SHOWCASE_INPUT_OWNER, true)
- _showcase_animation_active = true
- set_fishing_visual(false)
_kill_showcase_camera_tween()
_kill_showcase_restore_tween()
_capture_showcase_camera_snapshot()
if not _showcase_visual_rotation_stored:
_showcase_visual_rotation = _visuals.rotation
_showcase_visual_rotation_stored = true
- var showcase_camera_position: Vector3 = _begin_showcase_camera_transition(
- camera_yaw_offset_degrees
- )
+ var showcase_camera_position: Vector3 = _begin_showcase_camera_transition()
_turn_showcase_toward_position(showcase_camera_position)
if not _showcase_rod_state_stored:
_showcase_rod_visibility = _fishing_rod.visible
- _showcase_net_visibility = (
- _catching_net.visible if _catching_net != null else false
- )
- _showcase_shovel_visibility = (
- _shovel.visible if _shovel != null else false
- )
_showcase_rod_state_stored = true
_fishing_rod.visible = false
- if _catching_net != null:
- _catching_net.visible = false
- if _shovel != null:
- _shovel.visible = false
_catch_sprite.texture = fish_catch.fish.display_texture
- _catch_display.scale = _get_catch_presentation_scale(
- fish_catch.fish,
- fish_catch.display_scale,
- )
- _catch_display.visible = _catch_sprite.texture != null
- _update_character_animation()
-
-
-func begin_remote_catch_showcase(fish_catch: FishCatchType) -> void:
- if local_control_enabled or fish_catch == null or not fish_catch.is_valid():
- return
- if _is_net_strike_visual_active():
- _pending_net_showcase_catch = fish_catch
- _pending_net_showcase_remote = true
- return
- _begin_remote_catch_showcase_now(fish_catch)
-
-
-func _begin_remote_catch_showcase_now(fish_catch: FishCatchType) -> void:
- end_catch_showcase(Callable(), true)
- _showcase_animation_active = true
- set_fishing_visual(false)
- _showcase_rod_visibility = _fishing_rod.visible
- _showcase_net_visibility = (
- _catching_net.visible if _catching_net != null else false
- )
- _showcase_shovel_visibility = (
- _shovel.visible if _shovel != null else false
- )
- _showcase_rod_state_stored = true
- _fishing_rod.visible = false
- if _catching_net != null:
- _catching_net.visible = false
- if _shovel != null:
- _shovel.visible = false
- _catch_sprite.texture = fish_catch.fish.display_texture
- _catch_display.scale = _get_catch_presentation_scale(
- fish_catch.fish,
- fish_catch.display_scale,
- )
- _catch_display.visible = _catch_sprite.texture != null
- _update_character_animation()
-
-
-func _get_catch_presentation_scale(
- fish: FishDataType,
- display_scale: float,
-) -> Vector3:
- var texture: Texture2D = fish.display_texture if fish != null else null
- return (
+ _catch_display.scale = (
Vector3.ONE
- * maxf(display_scale, 0.01)
+ * fish_catch.display_scale
* catch_presentation_base_scale
- * get_catch_texture_resolution_scale(texture)
)
-
-
-static func get_catch_texture_resolution_scale(texture: Texture2D) -> float:
- if texture == null:
- return 1.0
- var texture_size: Vector2 = texture.get_size()
- var longest_side: float = maxf(texture_size.x, texture_size.y)
- if not is_finite(longest_side) or longest_side <= 0.0:
- return 1.0
- return clampf(
- CATCH_PRESENTATION_REFERENCE_LONG_SIDE_PX / longest_side,
- CATCH_PRESENTATION_MIN_TEXTURE_SCALE,
- CATCH_PRESENTATION_MAX_TEXTURE_SCALE,
- )
-
-
-func _is_net_strike_visual_active() -> bool:
- return (
- _animation_action_id == CHARACTER_NET_STRIKE_ANIMATION
- or _network_target_animation_action_id
- == CHARACTER_NET_STRIKE_ANIMATION
- or (
- _character_animation_player != null
- and _character_animation_player.current_animation
- == CHARACTER_NET_STRIKE_ANIMATION
- )
- )
-
-
-func _begin_pending_net_showcase() -> void:
- var fish_catch := _pending_net_showcase_catch
- var remote: bool = _pending_net_showcase_remote
- _pending_net_showcase_catch = null
- _pending_net_showcase_remote = false
- if fish_catch == null or not fish_catch.is_valid():
- return
- if remote:
- _begin_remote_catch_showcase_now(fish_catch)
- else:
- _begin_catch_showcase_now(
- fish_catch,
- net_showcase_camera_yaw_offset,
- )
+ _catch_display.visible = _catch_sprite.texture != null
func end_catch_showcase(
restored_callback: Callable = Callable(),
immediate: bool = false,
- pocket_completed: bool = false,
) -> void:
- if immediate and _pocket_visual_target == PocketVisualTarget.CATCH_SHOWCASE:
- _cancel_pocket_visual()
- elif (
- not pocket_completed
- and _showcase_animation_active
- and _catch_display.visible
- ):
- _begin_pocket_visual(
- PocketVisualTarget.CATCH_SHOWCASE,
- true,
- false,
- end_catch_showcase.bind(restored_callback, false, true),
- )
- return
- _showcase_animation_active = false
- _update_character_animation()
_kill_showcase_turn_tween()
_kill_showcase_camera_tween()
_kill_showcase_restore_tween()
@@ -3837,20 +390,12 @@ func end_catch_showcase(
_catch_sprite.texture = null
if _showcase_rod_state_stored:
_fishing_rod.visible = _showcase_rod_visibility
- if _catching_net != null:
- _catching_net.visible = _showcase_net_visibility
- if _shovel != null:
- _shovel.visible = _showcase_shovel_visibility
_showcase_rod_visibility = true
- _showcase_net_visibility = false
- _showcase_shovel_visibility = false
_showcase_rod_state_stored = false
if (
not _showcase_visual_rotation_stored
and _showcase_camera_snapshot == null
):
- if local_control_enabled:
- set_local_input_suppressed(CATCH_SHOWCASE_INPUT_OWNER, false)
if restored_callback.is_valid():
restored_callback.call()
return
@@ -4004,13 +549,11 @@ func _complete_showcase_restore(
_camera_pitch.rotation = _showcase_camera_snapshot.pitch_rotation
_spring_arm.spring_length = _showcase_camera_snapshot.spring_length
_target_zoom = _showcase_camera_snapshot.target_zoom
- _set_camera_dragging(
- _showcase_camera_snapshot.camera_dragging
- and Input.is_action_pressed("camera_drag")
+ _camera_input_enabled = (
+ _showcase_camera_snapshot.camera_input_enabled
)
+ _camera_dragging = _showcase_camera_snapshot.camera_dragging
_showcase_camera_snapshot = null
- if local_control_enabled:
- set_local_input_suppressed(CATCH_SHOWCASE_INPUT_OWNER, false)
if restored_callback.is_valid():
restored_callback.call()
@@ -4024,16 +567,16 @@ func _capture_showcase_camera_snapshot() -> void:
_showcase_camera_snapshot.pitch_rotation = _camera_pitch.rotation
_showcase_camera_snapshot.spring_length = _spring_arm.spring_length
_showcase_camera_snapshot.target_zoom = _target_zoom
+ _showcase_camera_snapshot.camera_input_enabled = _camera_input_enabled
_showcase_camera_snapshot.camera_dragging = _camera_dragging
- _set_camera_dragging(false)
+ _camera_input_enabled = false
+ _camera_dragging = false
-func _begin_showcase_camera_transition(
- camera_yaw_offset_degrees: float,
-) -> Vector3:
+func _begin_showcase_camera_transition() -> Vector3:
var target_world_yaw: float = (
_visuals.global_rotation.y
- + deg_to_rad(camera_yaw_offset_degrees)
+ + deg_to_rad(showcase_camera_yaw_offset)
)
var target_local_yaw: float = target_world_yaw - global_rotation.y
var shortest_target_yaw: float = (
diff --git a/player/player.tscn b/player/player.tscn
index 3288d2d..977040d 100644
--- a/player/player.tscn
+++ b/player/player.tscn
@@ -1,4 +1,4 @@
-[gd_scene load_steps=23 format=3]
+[gd_scene load_steps=18 format=3]
[ext_resource type="Script" path="res://player/player.gd" id="1_script"]
[ext_resource type="Script" path="res://inventory/fish_inventory.gd" id="2_inventory"]
@@ -10,32 +10,32 @@
[ext_resource type="Script" path="res://progression/player_fishing_upgrades.gd" id="8_upgrades"]
[ext_resource type="Script" path="res://progression/player_item_effects.gd" id="9_effects"]
[ext_resource type="Script" path="res://progression/player_cooler_capacity.gd" id="10_capacity"]
-[ext_resource type="Script" path="res://player/player_ground_shadow.gd" id="11_ground_shadow"]
-[ext_resource type="Material" path="res://player/materials/player_blob_shadow.tres" id="12_blob_shadow"]
-[ext_resource type="PackedScene" path="res://art/exported/characters/base/straywild_base_character.glb" id="13_character"]
-[ext_resource type="Script" path="res://progression/player_art_unlocks.gd" id="14_art_unlocks"]
-[ext_resource type="Script" path="res://progression/player_experience.gd" id="15_experience"]
-[ext_resource type="Script" path="res://player/sprint_dust_trail.gd" id="16_sprint_dust"]
-[ext_resource type="Script" path="res://inventory/player_inventory_layout.gd" id="17_inventory_layout"]
[sub_resource type="CapsuleShape3D" id="PlayerShape"]
radius = 0.45
height = 1.8
-[sub_resource type="QuadMesh" id="GroundShadowMesh"]
-material = ExtResource("12_blob_shadow")
-size = Vector2(1.15, 0.72)
+[sub_resource type="CapsuleMesh" id="PlayerMesh"]
+radius = 0.45
+height = 1.8
-[sub_resource type="Shader" id="SprintDustShader"]
-code = "shader_type spatial;\nrender_mode blend_mix, depth_draw_never, cull_disabled, unshaded, shadows_disabled;\n\nuniform vec3 dust_color : source_color = vec3(0.61, 0.57, 0.53);\nvarying flat float puff_phase;\nvarying flat float puff_alpha;\n\nvoid vertex() {\n\tpuff_phase = INSTANCE_CUSTOM.x;\n\tpuff_alpha = INSTANCE_CUSTOM.w;\n\tVERTEX.xy *= INSTANCE_CUSTOM.yz;\n\tfloat puff_rotation = puff_phase * 6.28318530718;\n\tfloat rotation_sine = sin(puff_rotation);\n\tfloat rotation_cosine = cos(puff_rotation);\n\tVERTEX.xy = mat2(\n\t\tvec2(rotation_cosine, -rotation_sine),\n\t\tvec2(rotation_sine, rotation_cosine)\n\t) * VERTEX.xy;\n\tMODELVIEW_MATRIX = VIEW_MATRIX * mat4(\n\t\tINV_VIEW_MATRIX[0],\n\t\tINV_VIEW_MATRIX[1],\n\t\tINV_VIEW_MATRIX[2],\n\t\tMODEL_MATRIX[3]\n\t);\n}\n\nvoid fragment() {\n\tvec2 centered_uv = UV * 2.0 - 1.0;\n\tfloat angle = atan(centered_uv.y, centered_uv.x);\n\tfloat edge_wobble =\n\t\tsin(angle * 5.0 + puff_phase * 8.0) * 0.07\n\t\t+ sin(angle * 3.0 - puff_phase * 5.0) * 0.025;\n\tfloat radius = length(centered_uv);\n\tif (radius > 0.82 + edge_wobble) {\n\t\tdiscard;\n\t}\n\tALBEDO = dust_color;\n\tALPHA = puff_alpha;\n}\n"
+[sub_resource type="StandardMaterial3D" id="PlayerMaterial"]
+albedo_color = Color(0.95, 0.48, 0.18, 1)
-[sub_resource type="ShaderMaterial" id="SprintDustMaterial"]
-render_priority = 1
-shader = SubResource("SprintDustShader")
+[sub_resource type="BoxMesh" id="FacingMarkerMesh"]
+size = Vector3(0.24, 0.2, 0.4)
-[sub_resource type="QuadMesh" id="SprintDustMesh"]
-material = SubResource("SprintDustMaterial")
-size = Vector2(1, 1)
+[sub_resource type="StandardMaterial3D" id="FacingMarkerMaterial"]
+albedo_color = Color(0.12, 0.18, 0.28, 1)
+
+[sub_resource type="CylinderMesh" id="FishingRodMesh"]
+top_radius = 0.025
+bottom_radius = 0.045
+height = 1.2
+
+[sub_resource type="StandardMaterial3D" id="FishingRodMaterial"]
+albedo_color = Color(0.22, 0.12, 0.06, 1)
+roughness = 0.8
[node name="Player" type="CharacterBody3D"]
collision_layer = 2
@@ -49,36 +49,29 @@ shape = SubResource("PlayerShape")
[node name="Visuals" type="Node3D" parent="."]
unique_name_in_owner = true
-[node name="GroundShadow" type="Node3D" parent="."]
-script = ExtResource("11_ground_shadow")
+[node name="MeshInstance3D" type="MeshInstance3D" parent="Visuals"]
+position = Vector3(0, 0.9, 0)
+mesh = SubResource("PlayerMesh")
+material_override = SubResource("PlayerMaterial")
-[node name="GroundProbe" type="RayCast3D" parent="GroundShadow"]
+[node name="FacingMarker" type="MeshInstance3D" parent="Visuals"]
+position = Vector3(0, 1.0, -0.55)
+mesh = SubResource("FacingMarkerMesh")
+material_override = SubResource("FacingMarkerMaterial")
+
+[node name="FishingRod" type="Node3D" parent="Visuals"]
unique_name_in_owner = true
-position = Vector3(0, 0.45, 0)
-target_position = Vector3(0, -4.5, 0)
-collision_mask = 9
-collide_with_areas = true
+position = Vector3(0.42, 1.05, -0.2)
+rotation = Vector3(-0.436332, 0, 0)
-[node name="GroundShadowPlacement" type="Node3D" parent="GroundShadow"]
+[node name="RodMesh" type="MeshInstance3D" parent="Visuals/FishingRod"]
+position = Vector3(0, 0.6, 0)
+mesh = SubResource("FishingRodMesh")
+material_override = SubResource("FishingRodMaterial")
+
+[node name="FishingRodTip" type="Marker3D" parent="Visuals/FishingRod"]
unique_name_in_owner = true
-
-[node name="GroundShadowVisual" type="MeshInstance3D" parent="GroundShadow/GroundShadowPlacement"]
-unique_name_in_owner = true
-rotation_degrees = Vector3(-90, 0, 0)
-cast_shadow = 0
-mesh = SubResource("GroundShadowMesh")
-
-[node name="SprintDust" type="MultiMeshInstance3D" parent="."]
-unique_name_in_owner = true
-cast_shadow = 0
-script = ExtResource("16_sprint_dust")
-puff_mesh = SubResource("SprintDustMesh")
-
-[node name="CharacterRig" parent="Visuals" instance=ExtResource("13_character")]
-rotation_degrees = Vector3(0, 180, 0)
-
-[node name="AnimationPlayer" parent="Visuals/CharacterRig"]
-autoplay = "idle"
+position = Vector3(0, 1.2, 0)
[node name="CastOrigin" type="Marker3D" parent="."]
unique_name_in_owner = true
@@ -120,17 +113,21 @@ script = ExtResource("9_effects")
unique_name_in_owner = true
script = ExtResource("10_capacity")
-[node name="InventoryLayout" type="Node" parent="."]
+[node name="CatchDisplayAnchor" type="Marker3D" parent="Visuals"]
unique_name_in_owner = true
-script = ExtResource("17_inventory_layout")
+position = Vector3(0, 1.45, -1.15)
-[node name="ArtUnlocks" type="Node" parent="."]
+[node name="CatchDisplay" type="Node3D" parent="Visuals/CatchDisplayAnchor"]
unique_name_in_owner = true
-script = ExtResource("14_art_unlocks")
+visible = false
-[node name="Experience" type="Node" parent="."]
+[node name="CatchSprite" type="Sprite3D" parent="Visuals/CatchDisplayAnchor/CatchDisplay"]
unique_name_in_owner = true
-script = ExtResource("15_experience")
+pixel_size = 0.012
+billboard = 1
+shaded = false
+double_sided = true
+texture_filter = 0
[node name="CameraYaw" type="Node3D" parent="."]
unique_name_in_owner = true
diff --git a/player/player_ground_shadow.gd b/player/player_ground_shadow.gd
deleted file mode 100644
index 0efa948..0000000
--- a/player/player_ground_shadow.gd
+++ /dev/null
@@ -1,66 +0,0 @@
-class_name PlayerGroundShadow
-extends Node3D
-
-@export_range(0.0, 0.1, 0.001) var surface_offset: float = 0.018
-@export_range(0.0, 0.5, 0.01) var full_opacity_height: float = 0.12
-@export_range(0.5, 4.0, 0.05) var fade_end_height: float = 2.0
-@export_range(1.0, 20.0, 0.5) var fade_speed: float = 10.0
-@export_range(0.25, 1.0, 0.01) var airborne_scale: float = 0.72
-
-@onready var _player := get_parent() as Player
-@onready var _ground_probe: RayCast3D = %GroundProbe
-@onready var _placement: Node3D = %GroundShadowPlacement
-@onready var _visual: MeshInstance3D = %GroundShadowVisual
-
-var _opacity: float = 0.0
-var _visual_scale: float = 1.0
-
-
-func _ready() -> void:
- _visual.visible = false
-
-
-func _physics_process(delta: float) -> void:
- _ground_probe.force_raycast_update()
- var target_opacity := 0.0
- var target_scale := airborne_scale
- var can_place := (
- _player != null
- and _player.is_remote_presentation_visible()
- and not _player.is_water_recovery_active()
- and _ground_probe.is_colliding()
- and not (_ground_probe.get_collider() is PlayerWaterTrigger)
- )
- if can_place:
- var hit_position: Vector3 = _ground_probe.get_collision_point()
- var hit_normal: Vector3 = _ground_probe.get_collision_normal().normalized()
- var height := maxf(global_position.y - hit_position.y, 0.0)
- var height_fade := 1.0 - smoothstep(
- full_opacity_height,
- fade_end_height,
- height,
- )
- target_opacity = height_fade
- target_scale = lerpf(airborne_scale, 1.0, height_fade)
- _place_on_surface(hit_position, hit_normal)
- _opacity = move_toward(_opacity, target_opacity, fade_speed * delta)
- _visual_scale = move_toward(
- _visual_scale,
- target_scale,
- fade_speed * 0.35 * delta,
- )
- _placement.scale = Vector3.ONE * _visual_scale
- _visual.visible = _opacity > 0.001
- _visual.transparency = 1.0 - _opacity
-
-
-func _place_on_surface(surface_position: Vector3, normal: Vector3) -> void:
- var tangent := Vector3.RIGHT - normal * Vector3.RIGHT.dot(normal)
- if tangent.length_squared() < 0.001:
- tangent = Vector3.FORWARD - normal * Vector3.FORWARD.dot(normal)
- tangent = tangent.normalized()
- var bitangent := tangent.cross(normal).normalized()
- _placement.global_transform = Transform3D(
- Basis(tangent, normal, bitangent),
- surface_position + normal * surface_offset,
- )
diff --git a/player/player_ground_shadow.gd.uid b/player/player_ground_shadow.gd.uid
deleted file mode 100644
index 1dd7974..0000000
--- a/player/player_ground_shadow.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://i8vkdcyckug3
diff --git a/player/player_visual_presenter.gd b/player/player_visual_presenter.gd
deleted file mode 100644
index b071b2c..0000000
--- a/player/player_visual_presenter.gd
+++ /dev/null
@@ -1,321 +0,0 @@
-class_name PlayerVisualPresenter
-extends RefCounted
-
-const PlayerScene: PackedScene = preload("res://player/player.tscn")
-const FurPatternShader: Shader = preload("res://player/fur_pattern.gdshader")
-const RUNTIME_MATERIAL_META: StringName = &"straywild_runtime_material"
-const RUNTIME_FUR_PATTERN_MATERIAL_META: StringName = (
- &"straywild_runtime_fur_pattern_material"
-)
-const EAR_MESH_NAMES: Array[String] = [
- "ears_antlers_round",
- "ears_bear",
- "ears_bunny",
- "ears_pointy_long",
- "ears_pointy_short",
- "ears_pointy_wide",
-]
-const TAIL_MESH_NAMES: Array[String] = [
- "tails_bear",
- "tails_bunny",
- "tails_cat",
- "tails_fox",
- "tails_gator",
- "tails_pointy",
-]
-
-static var _feature_uv_aspects: Dictionary = {}
-
-
-static func feature_uv_aspect(feature_name: String) -> float:
- if _feature_uv_aspects.has(feature_name):
- return float(_feature_uv_aspects[feature_name])
- var visuals := instantiate_visuals()
- var skeleton := visuals.find_child("Skeleton3D", true, false) as Skeleton3D
- var aspects: Array[float] = []
- for head_name: String in ["head_round", "head_pointy"]:
- var mesh_instance := skeleton.get_node_or_null(
- "%s_%s" % [head_name, feature_name]
- ) as MeshInstance3D
- if mesh_instance == null or mesh_instance.mesh == null:
- continue
- var arrays := mesh_instance.mesh.surface_get_arrays(0)
- var uv: PackedVector2Array = arrays[Mesh.ARRAY_TEX_UV]
- if uv.is_empty():
- continue
- var min_uv := Vector2(INF, INF)
- var max_uv := Vector2(-INF, -INF)
- for value: Vector2 in uv:
- min_uv = min_uv.min(value)
- max_uv = max_uv.max(value)
- var size := max_uv - min_uv
- if size.y > 0.001:
- aspects.append(size.x / size.y)
- visuals.free()
- var result := 1.0
- if not aspects.is_empty():
- result = 0.0
- for aspect: float in aspects:
- result += aspect
- result /= aspects.size()
- result = clampf(result, 0.35, 2.5)
- _feature_uv_aspects[feature_name] = result
- return result
-
-
-static func instantiate_visuals() -> Node3D:
- var source: Player = PlayerScene.instantiate()
- var visuals := source.get_node("Visuals") as Node3D
- var source_rod_attachment: Node = visuals.find_child(
- "FishingRodAttachment", true, false
- )
- var source_rod_parent: Node = null
- var source_rod_index: int = -1
- if source_rod_attachment != null:
- source_rod_parent = source_rod_attachment.get_parent()
- source_rod_index = source_rod_attachment.get_index()
- source_rod_attachment.owner = null
- source_rod_parent.remove_child(source_rod_attachment)
- var presentation := visuals.duplicate(
- Node.DUPLICATE_SIGNALS
- | Node.DUPLICATE_GROUPS
- | Node.DUPLICATE_SCRIPTS
- | Node.DUPLICATE_USE_INSTANTIATION
- ) as Node3D
- if source_rod_attachment != null and source_rod_parent != null:
- source_rod_parent.add_child(source_rod_attachment)
- source_rod_parent.move_child(source_rod_attachment, source_rod_index)
- source.free()
- presentation.name = "PlayerVisuals"
- return presentation
-
-
-static func apply_appearance(
- visuals: Node3D,
- snapshot: Dictionary,
-) -> void:
- if not CharacterCustomizationCatalog.validate_snapshot(snapshot):
- return
- # Gameplay and preview deliberately share this presentation seam. Keep all
- # mesh selection here so profile previews and network avatars stay identical.
- var character_scale: float = CharacterCustomizationCatalog.character_scale(
- snapshot.get(
- CharacterCustomizationCatalog.SCALE_CATEGORY_ID,
- CharacterCustomizationCatalog.DEFAULT_CHARACTER_SCALE,
- )
- )
- visuals.scale = Vector3.ONE * character_scale
- var skeleton: Skeleton3D = visuals.find_child("Skeleton3D", true, false) as Skeleton3D
- if skeleton == null:
- return
-
- var species_id: String = CharacterCustomizationCatalog.canonical_option_id(
- "species", str(snapshot.get("species", "round"))
- )
- var head_id: String = "head_pointy" if species_id == "pointy" else "head_round"
- var head_ids: Array[String] = ["head_pointy", "head_round"]
- for head_name: String in head_ids:
- var head: Node3D = skeleton.get_node_or_null(head_name) as Node3D
- if head != null:
- head.visible = head_name == head_id
- for feature: String in ["eyes", "mouth", "nose"]:
- var decal: Node3D = skeleton.get_node_or_null(
- "%s_%s" % [head_name, feature]
- ) as Node3D
- if decal != null:
- var feature_id := CharacterCustomizationCatalog.canonical_option_id(
- feature, str(snapshot.get(feature, "none"))
- )
- decal.visible = (
- head_name == head_id and feature_id != "none"
- )
-
- var selected_ears: String = CharacterCustomizationCatalog.canonical_option_id(
- "ears", str(snapshot.get("ears", "none"))
- )
- for ear_name: String in EAR_MESH_NAMES:
- var ears: Node3D = skeleton.get_node_or_null(ear_name) as Node3D
- if ears != null:
- ears.visible = selected_ears == ear_name.trim_prefix("ears_")
- var selected_tail: String = CharacterCustomizationCatalog.canonical_option_id(
- "tail", str(snapshot.get("tail", "none"))
- )
- for tail_name: String in TAIL_MESH_NAMES:
- var tail: Node3D = skeleton.get_node_or_null(tail_name) as Node3D
- if tail != null:
- tail.visible = selected_tail == tail_name.trim_prefix("tails_")
-
- var fur_color: Color = CharacterCustomizationCatalog.option_color(
- "fur_pattern", str(snapshot.get("fur_pattern", "white"))
- )
- for fur_mesh_name: String in [
- "body_main",
- "body_arms",
- "head_pointy",
- "head_round",
- ]:
- _set_fur_mesh(skeleton, fur_mesh_name, snapshot, fur_color)
- for ear_name: String in EAR_MESH_NAMES:
- _set_fur_mesh(skeleton, ear_name, snapshot, fur_color)
- for tail_name: String in TAIL_MESH_NAMES:
- _set_fur_mesh(skeleton, tail_name, snapshot, fur_color)
-
- var eye_id: String = CharacterCustomizationCatalog.canonical_option_id(
- "eyes", str(snapshot.get("eyes", "simple_shine"))
- )
- var mouth_id: String = CharacterCustomizationCatalog.canonical_option_id(
- "mouth", str(snapshot.get("mouth", "three"))
- )
- var nose_id: String = CharacterCustomizationCatalog.canonical_option_id(
- "nose", str(snapshot.get("nose", "dog_round"))
- )
- _set_feature_texture(
- skeleton,
- "eyes",
- CharacterCustomizationCatalog.texture_for("eyes", eye_id),
- )
- _set_feature_texture(
- skeleton,
- "mouth",
- CharacterCustomizationCatalog.texture_for("mouth", mouth_id),
- )
- _set_feature_texture(
- skeleton,
- "nose",
- CharacterCustomizationCatalog.texture_for("nose", nose_id),
- )
-
-
-static func _set_feature_texture(
- skeleton: Skeleton3D,
- feature_name: String,
- texture: Texture2D,
-) -> void:
- for head_name: String in ["head_pointy", "head_round"]:
- var mesh_instance: MeshInstance3D = skeleton.get_node_or_null(
- "%s_%s" % [head_name, feature_name]
- ) as MeshInstance3D
- if texture == null:
- if mesh_instance != null:
- mesh_instance.visible = false
- continue
- var material: StandardMaterial3D = _runtime_material(mesh_instance)
- if material != null:
- # Feature plates are transparent geometry. The exported base model may
- # have no preview texture, so enforce the runtime alpha mode when a
- # drop-in PNG is assigned.
- material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
- material.albedo_color = Color.WHITE
- material.albedo_texture = texture
-
-
-static func _set_fur_mesh(
- skeleton: Skeleton3D,
- mesh_name: String,
- snapshot: Dictionary,
- base_color: Color,
-) -> void:
- var mesh_instance := skeleton.get_node_or_null(mesh_name) as MeshInstance3D
- if mesh_instance == null:
- return
- var style_field := (
- CharacterCustomizationCatalog.fur_style_field_for_component(mesh_name)
- )
- var style_id := CharacterCustomizationCatalog.canonical_option_id(
- style_field,
- str(snapshot.get(
- style_field,
- CharacterCustomizationCatalog.DEFAULT_FUR_STYLE,
- )),
- )
- var pattern_texture := CharacterCustomizationCatalog.fur_pattern_texture(
- style_id,
- mesh_name,
- )
- if pattern_texture == null:
- var solid_material := _runtime_material(mesh_instance)
- if solid_material != null:
- solid_material.albedo_color = base_color
- mesh_instance.material_override = solid_material
- return
-
- var pattern_material := _runtime_fur_pattern_material(mesh_instance)
- pattern_material.set_shader_parameter("pattern_texture", pattern_texture)
- pattern_material.set_shader_parameter("base_color", base_color)
- pattern_material.set_shader_parameter(
- "red_zone_color",
- _fur_snapshot_color(snapshot, "fur_color_2", "cream"),
- )
- pattern_material.set_shader_parameter(
- "green_zone_color",
- _fur_snapshot_color(snapshot, "fur_color_3", "brown"),
- )
- pattern_material.set_shader_parameter(
- "blue_zone_color",
- _fur_snapshot_color(snapshot, "fur_color_4", "red"),
- )
- mesh_instance.material_override = pattern_material
-
-
-static func _fur_snapshot_color(
- snapshot: Dictionary,
- category_id: String,
- default_id: String,
-) -> Color:
- return CharacterCustomizationCatalog.option_color(
- category_id,
- str(snapshot.get(category_id, default_id)),
- )
-
-
-static func _runtime_material(
- mesh_instance: MeshInstance3D,
-) -> StandardMaterial3D:
- if mesh_instance == null or mesh_instance.mesh == null:
- return null
- if mesh_instance.has_meta(RUNTIME_MATERIAL_META):
- var runtime := mesh_instance.get_meta(
- RUNTIME_MATERIAL_META
- ) as StandardMaterial3D
- _configure_matte_material(runtime)
- return runtime
- var source: StandardMaterial3D = mesh_instance.mesh.surface_get_material(
- 0
- ) as StandardMaterial3D
- if source == null:
- return null
- var material: StandardMaterial3D = source.duplicate() as StandardMaterial3D
- _configure_matte_material(material)
- mesh_instance.material_override = material
- mesh_instance.set_meta(RUNTIME_MATERIAL_META, material)
- return material
-
-
-static func _runtime_fur_pattern_material(
- mesh_instance: MeshInstance3D,
-) -> ShaderMaterial:
- if mesh_instance.has_meta(RUNTIME_FUR_PATTERN_MATERIAL_META):
- return mesh_instance.get_meta(
- RUNTIME_FUR_PATTERN_MATERIAL_META
- ) as ShaderMaterial
- var material := ShaderMaterial.new()
- material.shader = FurPatternShader
- mesh_instance.set_meta(RUNTIME_FUR_PATTERN_MATERIAL_META, material)
- return material
-
-
-static func _configure_matte_material(material: StandardMaterial3D) -> void:
- if material == null:
- return
- material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST
- # Character artwork is deliberately graphic and flat. Unshaded rendering
- # keeps the authored fur and facial colors invariant across world weather,
- # time of day, and the isolated Profile preview.
- material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
- material.metallic = 0.0
- material.metallic_specular = 0.0
- material.roughness = 1.0
- material.diffuse_mode = BaseMaterial3D.DIFFUSE_LAMBERT
- material.anisotropy_enabled = false
- material.clearcoat_enabled = false
- material.rim_enabled = false
diff --git a/player/player_visual_presenter.gd.uid b/player/player_visual_presenter.gd.uid
deleted file mode 100644
index f67f8da..0000000
--- a/player/player_visual_presenter.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b4w23asm85rtu
diff --git a/player/shovel_attachment.tscn b/player/shovel_attachment.tscn
deleted file mode 100644
index a57dfe5..0000000
--- a/player/shovel_attachment.tscn
+++ /dev/null
@@ -1,68 +0,0 @@
-[gd_scene load_steps=8 format=3]
-
-[ext_resource type="Script" path="res://player/tool_socket_attachment.gd" id="1_socket"]
-
-[sub_resource type="StandardMaterial3D" id="HandleMaterial"]
-shading_mode = 0
-albedo_color = Color(0.36, 0.2, 0.1, 1)
-metallic = 0.0
-roughness = 1.0
-
-[sub_resource type="StandardMaterial3D" id="BladeMaterial"]
-shading_mode = 0
-albedo_color = Color(0.34, 0.57, 0.58, 1)
-metallic = 0.0
-roughness = 1.0
-
-[sub_resource type="CylinderMesh" id="ShaftMesh"]
-top_radius = 0.018
-bottom_radius = 0.018
-height = 0.7
-radial_segments = 8
-rings = 1
-material = SubResource("HandleMaterial")
-
-[sub_resource type="BoxMesh" id="BladeMesh"]
-size = Vector3(0.19, 0.25, 0.025)
-material = SubResource("BladeMaterial")
-
-[sub_resource type="BoxMesh" id="GripCrossMesh"]
-size = Vector3(0.18, 0.035, 0.035)
-material = SubResource("HandleMaterial")
-
-[sub_resource type="BoxMesh" id="GripSideMesh"]
-size = Vector3(0.035, 0.13, 0.035)
-material = SubResource("HandleMaterial")
-
-[node name="ShovelAttachment" type="BoneAttachment3D"]
-bone_name = "rod_socket"
-script = ExtResource("1_socket")
-aligned_mount_path = NodePath("Shovel")
-
-[node name="Shovel" type="Node3D" parent="."]
-visible = false
-
-[node name="Shaft" type="MeshInstance3D" parent="Shovel"]
-position = Vector3(0, 0.35, 0)
-cast_shadow = 0
-mesh = SubResource("ShaftMesh")
-
-[node name="Blade" type="MeshInstance3D" parent="Shovel"]
-position = Vector3(0, 0.78, 0)
-cast_shadow = 0
-mesh = SubResource("BladeMesh")
-
-[node name="GripCross" type="MeshInstance3D" parent="Shovel"]
-position = Vector3(0, -0.09, 0)
-cast_shadow = 0
-mesh = SubResource("GripCrossMesh")
-
-[node name="GripLeft" type="MeshInstance3D" parent="Shovel"]
-position = Vector3(-0.0725, -0.035, 0)
-cast_shadow = 0
-mesh = SubResource("GripSideMesh")
-
-[node name="GripRight" type="MeshInstance3D" parent="Shovel"]
-position = Vector3(0.0725, -0.035, 0)
-cast_shadow = 0
-mesh = SubResource("GripSideMesh")
diff --git a/player/sprint_dust_trail.gd b/player/sprint_dust_trail.gd
deleted file mode 100644
index 124b6ae..0000000
--- a/player/sprint_dust_trail.gd
+++ /dev/null
@@ -1,249 +0,0 @@
-class_name SprintDustTrail
-extends MultiMeshInstance3D
-
-const PUFF_COUNT: int = 12
-const PUFF_SPACING: float = 0.46
-const PUFF_LIFETIME: float = 0.62
-const PUFF_GROW_TIME: float = 0.16
-const PUFF_FADE_TIME: float = 0.22
-const PUFF_BACK_OFFSET: float = 0.14
-const PUFF_SIDE_OFFSET: float = 0.11
-const PUFF_HEIGHT: float = 0.08
-const PUFF_MIN_SCALE: float = 0.22
-
-@export var puff_mesh: Mesh
-
-var _puff_positions: Array[Vector3] = []
-var _puff_ages := PackedFloat32Array()
-var _puff_widths := PackedFloat32Array()
-var _puff_heights := PackedFloat32Array()
-var _puff_phases := PackedFloat32Array()
-var _next_puff: int = 0
-var _spawn_serial: int = 0
-var _foot_side: float = -1.0
-var _tracking_run: bool = false
-var _previous_source_position: Vector3
-var _distance_since_puff: float = 0.0
-
-
-func _ready() -> void:
- top_level = true
- global_transform = Transform3D.IDENTITY
- _initialize_pool()
-
-
-func update_trail(
- delta: float,
- source_position: Vector3,
- movement_velocity: Vector3,
- is_running: bool,
-) -> void:
- _update_existing_puffs(delta)
- custom_aabb = AABB(
- source_position - Vector3(5.0, 2.0, 5.0),
- Vector3(10.0, 5.0, 10.0),
- )
- if not is_running:
- _tracking_run = false
- _distance_since_puff = 0.0
- return
-
- var flat_source := Vector3(
- source_position.x,
- source_position.y,
- source_position.z,
- )
- if not _tracking_run:
- _tracking_run = true
- _previous_source_position = flat_source
- _distance_since_puff = 0.0
- return
-
- var segment := flat_source - _previous_source_position
- segment.y = 0.0
- var remaining_distance: float = segment.length()
- if remaining_distance <= 0.0001:
- _previous_source_position = flat_source
- return
-
- var segment_direction: Vector3 = segment / remaining_distance
- var movement_direction := Vector3(
- movement_velocity.x,
- 0.0,
- movement_velocity.z,
- ).normalized()
- if movement_direction.is_zero_approx():
- movement_direction = segment_direction
- var cursor: Vector3 = _previous_source_position
- var distance_to_next: float = PUFF_SPACING - _distance_since_puff
- while remaining_distance >= distance_to_next:
- cursor += segment_direction * distance_to_next
- _spawn_puff(cursor, movement_direction)
- remaining_distance -= distance_to_next
- _distance_since_puff = 0.0
- distance_to_next = PUFF_SPACING
- _distance_since_puff += remaining_distance
- _previous_source_position = flat_source
-
-
-func clear_trail() -> void:
- _tracking_run = false
- _distance_since_puff = 0.0
- for index: int in PUFF_COUNT:
- _puff_ages[index] = PUFF_LIFETIME
- _write_inactive_puff(index)
-
-
-func emit_landing_burst(
- source_position: Vector3,
- facing_direction: Vector3,
-) -> void:
- var direction := Vector3(
- facing_direction.x,
- 0.0,
- facing_direction.z,
- ).normalized()
- if direction.is_zero_approx():
- direction = Vector3.FORWARD
- var first_direction: Vector3 = -direction
- for puff_index: int in 5:
- var spread_direction := first_direction.rotated(
- Vector3.UP,
- TAU * float(puff_index) / 5.0,
- )
- var radius: float = lerpf(
- 0.38,
- 0.46,
- _stable_variation(_spawn_serial + puff_index + 81),
- )
- _spawn_puff_at(
- source_position
- + spread_direction * radius
- + Vector3.UP * PUFF_HEIGHT,
- 0.95,
- )
-
-
-func get_active_puff_count() -> int:
- var active_count: int = 0
- for age: float in _puff_ages:
- if age < PUFF_LIFETIME:
- active_count += 1
- return active_count
-
-
-func get_first_active_puff_position() -> Vector3:
- for index: int in PUFF_COUNT:
- if _puff_ages[index] < PUFF_LIFETIME:
- return _puff_positions[index]
- return Vector3.INF
-
-
-func _initialize_pool() -> void:
- var pool := MultiMesh.new()
- pool.transform_format = MultiMesh.TRANSFORM_3D
- pool.use_custom_data = true
- pool.instance_count = PUFF_COUNT
- pool.mesh = puff_mesh
- multimesh = pool
- _puff_positions.resize(PUFF_COUNT)
- _puff_ages.resize(PUFF_COUNT)
- _puff_widths.resize(PUFF_COUNT)
- _puff_heights.resize(PUFF_COUNT)
- _puff_phases.resize(PUFF_COUNT)
- clear_trail()
-
-
-func _spawn_puff(source_position: Vector3, direction: Vector3) -> void:
- var lateral := Vector3(-direction.z, 0.0, direction.x)
- var position := (
- source_position
- - direction * PUFF_BACK_OFFSET
- + lateral * PUFF_SIDE_OFFSET * _foot_side
- + Vector3.UP * PUFF_HEIGHT
- )
- _spawn_puff_at(position)
- _foot_side *= -1.0
-
-
-func _spawn_puff_at(
- position: Vector3,
- size_multiplier: float = 1.0,
-) -> void:
- var variation: float = _stable_variation(_spawn_serial)
- var second_variation: float = _stable_variation(_spawn_serial + 37)
- var index: int = _next_puff
- _puff_positions[index] = position
- _puff_ages[index] = 0.0
- _puff_widths[index] = (
- lerpf(0.34, 0.43, variation) * size_multiplier
- )
- _puff_heights[index] = (
- lerpf(0.23, 0.30, second_variation) * size_multiplier
- )
- _puff_phases[index] = variation
- _write_puff(index, PUFF_MIN_SCALE, 1.0)
- _next_puff = (_next_puff + 1) % PUFF_COUNT
- _spawn_serial += 1
-
-
-func _update_existing_puffs(delta: float) -> void:
- if multimesh == null:
- return
- for index: int in PUFF_COUNT:
- var age: float = _puff_ages[index]
- if age >= PUFF_LIFETIME:
- continue
- age += delta
- _puff_ages[index] = age
- if age >= PUFF_LIFETIME:
- _write_inactive_puff(index)
- continue
- var visual_scale: float = 1.0
- if age < PUFF_GROW_TIME:
- var grow_weight: float = smoothstep(
- 0.0,
- 1.0,
- age / PUFF_GROW_TIME,
- )
- visual_scale = lerpf(PUFF_MIN_SCALE, 1.0, grow_weight)
- var visual_alpha: float = 1.0
- if age > PUFF_LIFETIME - PUFF_FADE_TIME:
- var fade_weight: float = smoothstep(
- 0.0,
- 1.0,
- (age - (PUFF_LIFETIME - PUFF_FADE_TIME))
- / PUFF_FADE_TIME,
- )
- visual_alpha = 1.0 - fade_weight
- _write_puff(index, visual_scale, visual_alpha)
-
-
-func _write_puff(
- index: int,
- visual_scale: float,
- visual_alpha: float,
-) -> void:
- var puff_transform := Transform3D.IDENTITY
- puff_transform.origin = _puff_positions[index]
- multimesh.set_instance_transform(index, puff_transform)
- multimesh.set_instance_custom_data(
- index,
- Color(
- _puff_phases[index],
- _puff_widths[index] * visual_scale,
- _puff_heights[index] * visual_scale,
- visual_alpha,
- ),
- )
-
-
-func _write_inactive_puff(index: int) -> void:
- var inactive_transform := Transform3D.IDENTITY
- inactive_transform.origin = _puff_positions[index]
- multimesh.set_instance_transform(index, inactive_transform)
- multimesh.set_instance_custom_data(index, Color(0.0, 0.0, 0.0, 0.0))
-
-
-func _stable_variation(serial: int) -> float:
- return fposmod(sin(float(serial + 1) * 12.9898) * 43758.5453, 1.0)
diff --git a/player/sprint_dust_trail.gd.uid b/player/sprint_dust_trail.gd.uid
deleted file mode 100644
index c40239c..0000000
--- a/player/sprint_dust_trail.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://o2gi5oqunsp8
diff --git a/player/tool_socket_attachment.gd b/player/tool_socket_attachment.gd
deleted file mode 100644
index 4bbd1af..0000000
--- a/player/tool_socket_attachment.gd
+++ /dev/null
@@ -1,57 +0,0 @@
-class_name ToolSocketAttachment
-extends BoneAttachment3D
-
-@export var direction_guide_bone: StringName = &"rod_socket.001"
-@export_node_path("Node3D") var aligned_mount_path: NodePath
-
-
-func _ready() -> void:
- _align_mount_to_direction_guide()
-
-
-func _align_mount_to_direction_guide() -> void:
- var skeleton := get_parent() as Skeleton3D
- var mount := get_node_or_null(aligned_mount_path) as Node3D
- if skeleton == null or mount == null:
- push_error("Tool socket alignment requires a Skeleton3D parent and mount.")
- return
- var socket_index := skeleton.find_bone(bone_name)
- var guide_index := skeleton.find_bone(direction_guide_bone)
- if socket_index < 0 or guide_index < 0:
- push_error(
- "Tool socket alignment is missing %s or %s."
- % [bone_name, direction_guide_bone]
- )
- return
- if skeleton.get_bone_parent(guide_index) != socket_index:
- push_error(
- "Tool direction guide %s must be a child of %s."
- % [direction_guide_bone, bone_name]
- )
- return
- var socket_rest := skeleton.get_bone_global_rest(socket_index)
- var guide_rest := skeleton.get_bone_global_rest(guide_index)
- var guide_in_socket := socket_rest.affine_inverse() * guide_rest
- var rear_direction := guide_in_socket.basis.y.normalized()
- # rod_socket already aims local +Y from the hand toward the working end.
- # Its child guide is perpendicular to that axis and identifies which face
- # of the held tool points rearward. Projecting it onto the roll plane keeps
- # imperfect authored guides from changing the tool's lengthwise direction.
- rear_direction -= Vector3.UP * rear_direction.dot(Vector3.UP)
- if rear_direction.is_zero_approx():
- push_error("Tool direction guide has no usable direction.")
- return
- rear_direction = rear_direction.normalized()
- var rear_roll := Vector3.BACK.signed_angle_to(
- rear_direction,
- Vector3.UP,
- )
- var mount_transform := mount.transform
- mount_transform.basis = (
- Basis(Vector3.UP, rear_roll).orthonormalized()
- * mount_transform.basis
- )
- # The guide starts at the socket tip, but the held tool must remain anchored
- # at the socket origin in the hand. Only its authored direction is applied.
- mount_transform.origin = mount.transform.origin
- mount.transform = mount_transform
diff --git a/player/tool_socket_attachment.gd.uid b/player/tool_socket_attachment.gd.uid
deleted file mode 100644
index 7ddb71c..0000000
--- a/player/tool_socket_attachment.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://toolsocketalignment01
diff --git a/playtest/README-PLAYTEST.txt b/playtest/README-PLAYTEST.txt
new file mode 100644
index 0000000..f781b31
--- /dev/null
+++ b/playtest/README-PLAYTEST.txt
@@ -0,0 +1,111 @@
+NETFISHING
+Pre-Alpha Playtest 0.2
+Version 0.2.0-prealpha.2
+
+Thank you for trying this early private playtest.
+
+NETFISHING is currently a local gameplay prototype for a future
+multiplayer-first game. Networking and multiplayer are NOT implemented in
+this build.
+
+INSTALLATION
+
+1. Extract the entire ZIP into a new folder.
+2. Keep the executable and netfishing.pck together.
+3. Launch netfishing.exe on Windows or netfishing on Linux.
+
+Windows may show a warning for an unsigned application. This private build is
+not code signed. Only run a copy received from the playtest organizer.
+
+Linux users may need to allow execution:
+
+ chmod +x netfishing
+
+CONTROLS
+
+Move W / A / S / D or left stick
+Jump Space
+Sprint Shift
+Sneak Ctrl
+Slow walk Alt
+Rotate camera Hold right mouse, or use right stick
+Cycle active hotbar slot Mouse wheel
+Select hotbar slot 1 through 9
+Zoom camera Shift + mouse wheel
+Cast / withdraw / reel Left mouse
+Cooler, Bag, and Logbook Tab
+Game Menu / back Escape
+Open Fishing Shop E while near the shop
+
+FISHING
+
+- Hold left mouse to select cast distance, then release to cast.
+- The target preview shows whether the selected landing point is fishable.
+- While waiting, hold left mouse to withdraw the bobber.
+- A bite starts the chase automatically.
+- Hold left mouse to advance the green catch meter.
+- At barriers, use distinct left-click presses to damage the barrier.
+- Do not let the red chase meter catch the green meter.
+- Accessibility auto-click can be enabled in Settings.
+- Approach the bright Fishing Shop booth and press E to open it.
+- The Fishing Shop buys one Cooler fish at a time for full base value.
+- Buy Coffee, Energy Drinks, Snacks, and Fish Finders as Bag supplies.
+- Drag supplies from Bag to the hotbar and left-click in READY to use one.
+- Coffee improves movement; other supplies temporarily improve fishing.
+- The Cooler starts at 12 fish; buy permanent capacity expansions at the shop.
+- Reel Speed upgrades increase authoritative green reeling progress.
+- Barrier Power upgrades increase damage per valid barrier action.
+- Shop upgrades persist in the local progression save.
+- Escape closes shop confirmations before closing the shop itself.
+
+FEATURES TO TRY
+
+- Movement, jumping, camera rotation, and zoom
+- Short, medium, and maximum-distance casts
+- Valid water and invalid land casts
+- Manual withdrawal before a bite
+- Barrier-and-chase catching
+- Accessibility auto-click settings
+- Catch showcase and fish size variation
+- Cooler, Bag, Logbook, favorites, and sorting
+- Basic Fishing Rod equipment and the 1–9 hotbar
+- Pelican selling and wallet updates
+- Physical Fishing Shop sales and persistent fishing upgrades
+- Save, Continue, New Game, and Delete Save
+- Game Menu and persistent camera settings
+- Water-entry recovery from several shores
+
+SAVES AND SETTINGS
+
+Progression and settings are stored in Godot's per-user application-data
+directory, outside this extracted game folder. Delete Save removes progression
+but preserves settings.
+
+Windows:
+ %APPDATA%\Godot\app_userdata\NETFISHING\
+
+Linux:
+ ~/.local/share/godot/app_userdata/NETFISHING/
+
+This is an early save format. Keep expectations modest and report any failure
+to Continue, save, sell, favorite, or retain an individual fish.
+
+FEEDBACK
+
+Please include:
+
+- Your operating system and whether you used mouse/keyboard or controller
+- What you were doing when a problem occurred
+- Whether the issue repeated after restarting
+- Screenshots or a short recording when useful
+- How movement, casting, chase pressure, barrier clicking, menus, and camera
+ controls felt
+
+KNOWN PRE-ALPHA LIMITATIONS
+
+- Multiplayer/networking is not implemented.
+- Fish and character art are temporary.
+- UI and balance values are placeholders.
+- There is no audio.
+- There is no installer or automatic updater.
+- Windows builds are unsigned.
diff --git a/progression/character_customization_catalog.gd b/progression/character_customization_catalog.gd
deleted file mode 100644
index 5a02f09..0000000
--- a/progression/character_customization_catalog.gd
+++ /dev/null
@@ -1,782 +0,0 @@
-class_name CharacterCustomizationCatalog
-extends RefCounted
-
-const CATEGORY_IDS: PackedStringArray = [
- "species",
- "scale",
- "fur_pattern",
- "ears",
- "eyes",
- "nose",
- "mouth",
- "tail",
-]
-
-# CATEGORY_IDS controls the visible Profile navigation. The additional fur
-# fields are edited together on the existing fur page, but remain explicit in
-# saves and signed multiplayer appearance snapshots.
-const SNAPSHOT_IDS: PackedStringArray = [
- "species",
- "scale",
- "fur_pattern",
- "fur_style",
- "fur_style_arms",
- "fur_style_head",
- "fur_style_ears",
- "fur_style_tail",
- "fur_color_2",
- "fur_color_3",
- "fur_color_4",
- "ears",
- "eyes",
- "nose",
- "mouth",
- "tail",
-]
-
-const CATEGORY_LABELS: Dictionary = {
- "species": "head",
- "scale": "size",
- "fur_pattern": "fur",
- "ears": "ears",
- "eyes": "eyes",
- "nose": "nose",
- "mouth": "mouth",
- "tail": "tail",
-}
-
-const SCALE_CATEGORY_ID: String = "scale"
-const MIN_CHARACTER_SCALE: float = 0.5
-const MAX_CHARACTER_SCALE: float = 1.5
-const DEFAULT_CHARACTER_SCALE: float = 1.0
-const CHARACTER_SCALE_STEP: float = 0.05
-const FUR_STYLE_ID: String = "fur_style"
-const FUR_STYLE_ARMS_ID: String = "fur_style_arms"
-const FUR_STYLE_HEAD_ID: String = "fur_style_head"
-const FUR_STYLE_EARS_ID: String = "fur_style_ears"
-const FUR_STYLE_TAIL_ID: String = "fur_style_tail"
-const FUR_STYLE_IDS: PackedStringArray = [
- FUR_STYLE_ID,
- FUR_STYLE_ARMS_ID,
- FUR_STYLE_HEAD_ID,
- FUR_STYLE_EARS_ID,
- FUR_STYLE_TAIL_ID,
-]
-const FUR_STYLE_LABELS: Dictionary = {
- FUR_STYLE_ID: "body",
- FUR_STYLE_ARMS_ID: "arms",
- FUR_STYLE_HEAD_ID: "head",
- FUR_STYLE_EARS_ID: "ears",
- FUR_STYLE_TAIL_ID: "tail",
-}
-const FUR_COLOR_IDS: PackedStringArray = [
- "fur_pattern",
- "fur_color_2",
- "fur_color_3",
- "fur_color_4",
-]
-const FUR_COLOR_LABELS: Dictionary = {
- "fur_pattern": "primary",
- "fur_color_2": "secondary",
- "fur_color_3": "tertiary",
- "fur_color_4": "quaternary",
-}
-const CUSTOM_FUR_COLOR_PREFIX: String = "custom_"
-const DEFAULT_FUR_STYLE: String = "solid"
-const FUR_PATTERN_ASSET_ROOT: String = (
- "res://art/exported/characters/patterns"
-)
-const FEATURE_TEXTURE_CACHE_LIMIT: int = 24
-
-const FEATURE_CATEGORIES: PackedStringArray = ["eyes", "nose", "mouth"]
-const FEATURE_ASSET_ROOTS: Dictionary = {
- "eyes": "res://art/exported/characters/faces/eyes",
- "nose": "res://art/exported/characters/faces/noses",
- "mouth": "res://art/exported/characters/faces/mouth",
-}
-const FEATURE_LABEL_OVERRIDES: Dictionary = {
- "simple_shine": "simple shine",
- "simple_shine_eyebrows": "simple shine + eyebrows",
- "dog_round": "round",
- "three": "three",
-}
-const FUR_COLOR_OPTIONS: Array = [
- {"id": "white", "label": "white", "color": Color("f2f0e8")},
- {"id": "ivory", "label": "ivory", "color": Color("f0e2c2")},
- {"id": "cream", "label": "cream", "color": Color("e6d39b")},
- {"id": "sand", "label": "sand", "color": Color("c6a66f")},
- {"id": "tan", "label": "tan", "color": Color("ad7c55")},
- {"id": "taupe", "label": "taupe", "color": Color("89756a")},
- {"id": "silver", "label": "silver", "color": Color("b8c3c1")},
- {"id": "gray", "label": "gray", "color": Color("819398")},
- {"id": "slate", "label": "slate", "color": Color("58686c")},
- {"id": "charcoal", "label": "charcoal", "color": Color("34444a")},
- {"id": "black", "label": "black", "color": Color("192126")},
- {"id": "dark_brown", "label": "dark brown", "color": Color("432a25")},
- {"id": "chocolate", "label": "chocolate", "color": Color("603b2c")},
- {"id": "brown", "label": "brown", "color": Color("7b4a32")},
- {"id": "chestnut", "label": "chestnut", "color": Color("98513d")},
- {"id": "ginger", "label": "ginger", "color": Color("d28742")},
- {"id": "orange", "label": "orange", "color": Color("c86c36")},
- {"id": "red", "label": "red", "color": Color("a9433f")},
- {"id": "pink", "label": "pink", "color": Color("d8899e")},
- {"id": "yellow", "label": "yellow", "color": Color("d8c545")},
- {"id": "gold", "label": "gold", "color": Color("c29a3d")},
- {"id": "sage", "label": "sage", "color": Color("8fa16b")},
- {"id": "green", "label": "green", "color": Color("6f913c")},
- {"id": "forest", "label": "forest", "color": Color("3f6945")},
- {"id": "aqua", "label": "aqua", "color": Color("69aeb0")},
- {"id": "teal", "label": "teal", "color": Color("3a8790")},
- {"id": "blue", "label": "blue", "color": Color("4d76a8")},
- {"id": "navy", "label": "navy", "color": Color("344b78")},
- {"id": "lavender", "label": "lavender", "color": Color("9a7eaa")},
- {"id": "purple", "label": "purple", "color": Color("76558f")},
-]
-const OPTIONS: Dictionary = {
- "species": [
- {"id": "round", "label": "round"},
- {"id": "pointy", "label": "pointy"},
- ],
- # `fur_pattern` remains the primary color key for compatibility with
- # existing appearance saves. The actual authored pattern has its own field.
- "fur_pattern": FUR_COLOR_OPTIONS,
- "fur_color_2": FUR_COLOR_OPTIONS,
- "fur_color_3": FUR_COLOR_OPTIONS,
- "fur_color_4": FUR_COLOR_OPTIONS,
- "ears": [
- {"id": "none", "label": "none"},
- {"id": "antlers_round", "label": "antlers"},
- {"id": "bear", "label": "bear"},
- {"id": "bunny", "label": "bunny"},
- {"id": "pointy_long", "label": "long"},
- {"id": "pointy_short", "label": "short"},
- {"id": "pointy_wide", "label": "wide"},
- ],
- "eyes": [
- {"id": "simple_shine", "label": "simple shine"},
- {
- "id": "simple_shine_eyebrows",
- "label": "simple shine + eyebrows",
- },
- ],
- "nose": [{"id": "dog_round", "label": "round"}],
- "mouth": [{"id": "three", "label": "three"}],
- "tail": [
- {"id": "none", "label": "none"},
- {"id": "bear", "label": "bear"},
- {"id": "bunny", "label": "bunny"},
- {"id": "cat", "label": "cat"},
- {"id": "fox", "label": "fox"},
- {"id": "gator", "label": "gator"},
- {"id": "pointy", "label": "pointy"},
- ],
-}
-
-const LEGACY_OPTION_ALIASES: Dictionary = {
- "species": {"default": "round"},
- "fur_pattern": {"solid": "white"},
- "fur_style": {"spots_bengal": "bengal"},
- "ears": {"default": "none"},
- "tail": {"default": "none"},
- "eyes": {"default": "simple_shine"},
- "nose": {"default": "dog_round"},
- "mouth": {"default": "three"},
-}
-
-static var _feature_assets_ready: bool = false
-static var _feature_options: Dictionary = {}
-static var _feature_resource_paths: Dictionary = {}
-static var _feature_textures: Dictionary = {}
-static var _feature_texture_cache_order: Array[String] = []
-static var _fur_pattern_assets_ready: bool = false
-static var _fur_pattern_options: Array = []
-static var _fur_pattern_resource_paths: Dictionary = {}
-static var _fur_pattern_textures: Dictionary = {}
-
-
-static func default_snapshot() -> Dictionary:
- _ensure_feature_assets()
- return {
- "species": "round",
- "scale": DEFAULT_CHARACTER_SCALE,
- "fur_pattern": "white",
- "fur_style": DEFAULT_FUR_STYLE,
- "fur_style_arms": DEFAULT_FUR_STYLE,
- "fur_style_head": DEFAULT_FUR_STYLE,
- "fur_style_ears": DEFAULT_FUR_STYLE,
- "fur_style_tail": DEFAULT_FUR_STYLE,
- "fur_color_2": "cream",
- "fur_color_3": "brown",
- "fur_color_4": "red",
- "ears": "none",
- "eyes": _default_feature_option("eyes", "simple_shine"),
- "nose": _default_feature_option("nose", "dog_round"),
- "mouth": _default_feature_option("mouth", "three"),
- "tail": "none",
- }
-
-
-static func _default_feature_option(
- category_id: String,
- legacy_id: String,
-) -> String:
- var options: Array = _feature_options.get(category_id, []) as Array
- for option: Dictionary in options:
- if str(option.get("id", "")) == legacy_id:
- return legacy_id
- for option: Dictionary in options:
- var option_id := str(option.get("id", ""))
- if option_id != "none":
- return option_id
- return "none"
-
-
-static func options_for(category_id: String) -> Array:
- if category_id in FEATURE_CATEGORIES:
- _ensure_feature_assets()
- return _feature_options.get(category_id, []) as Array
- if category_id in FUR_STYLE_IDS:
- _ensure_fur_pattern_assets()
- return _fur_pattern_options
- return OPTIONS.get(category_id, [])
-
-
-static func feature_option_groups(category_id: String) -> Array:
- if category_id not in FEATURE_CATEGORIES:
- return []
- var options: Array = options_for(category_id)
- var option_ids: Array[String] = []
- for option: Dictionary in options:
- option_ids.append(str(option.get("id", "")))
- var grouped_options: Dictionary = {}
- var group_order: Array[String] = []
- for option: Dictionary in options:
- var option_id := str(option.get("id", ""))
- var group_id := _feature_variant_root(option_id, option_ids)
- if not grouped_options.has(group_id):
- grouped_options[group_id] = []
- group_order.append(group_id)
- var group: Array = grouped_options[group_id] as Array
- group.append(option)
- grouped_options[group_id] = group
- var result: Array = []
- for group_id: String in group_order:
- result.append({
- "id": group_id,
- "options": grouped_options[group_id],
- })
- return result
-
-
-static func _feature_variant_root(
- option_id: String,
- option_ids: Array[String],
-) -> String:
- if option_id == "none":
- return option_id
- # Prefer the shortest authored base option. This keeps nested names such as
- # `beady_extra_shine` in the `beady` drawer rather than creating drawers
- # inside drawers.
- var authored_root := ""
- for candidate_id: String in option_ids:
- if candidate_id == "none":
- continue
- if (
- option_id != candidate_id
- and not option_id.begins_with(candidate_id + "_")
- ):
- continue
- var has_descendant := false
- for descendant_id: String in option_ids:
- if descendant_id.begins_with(candidate_id + "_"):
- has_descendant = true
- break
- if (
- has_descendant
- and (
- authored_root.is_empty()
- or candidate_id.length() < authored_root.length()
- )
- ):
- authored_root = candidate_id
- if not authored_root.is_empty():
- return authored_root
-
- # Some families have no unqualified base asset. Group those siblings by
- # their leading name segment, making the sorted first asset representative.
- var separator_index := option_id.find("_")
- if separator_index <= 0:
- return option_id
- var leading_segment := option_id.substr(0, separator_index)
- var sibling_count := 0
- for candidate_id: String in option_ids:
- if candidate_id.begins_with(leading_segment + "_"):
- sibling_count += 1
- if sibling_count >= 2:
- return leading_segment
- return option_id
-
-
-static func texture_for(category_id: String, option_id: String) -> Texture2D:
- if category_id not in FEATURE_CATEGORIES:
- return null
- _ensure_feature_assets()
- var canonical_id := canonical_option_id(category_id, option_id)
- var cache_id := "%s:%s" % [category_id, canonical_id]
- if _feature_textures.has(cache_id):
- _touch_feature_texture(cache_id)
- return _feature_textures[cache_id] as Texture2D
- var resource_path := feature_texture_path(category_id, canonical_id)
- if resource_path.is_empty():
- return null
- var texture := ResourceLoader.load(
- resource_path,
- "Texture2D",
- ResourceLoader.CACHE_MODE_IGNORE,
- ) as Texture2D
- if texture == null:
- return null
- _feature_textures[cache_id] = texture
- _touch_feature_texture(cache_id)
- _trim_feature_texture_cache()
- return texture
-
-
-static func feature_texture_path(category_id: String, option_id: String) -> String:
- if category_id not in FEATURE_CATEGORIES:
- return ""
- _ensure_feature_assets()
- var canonical_id := canonical_option_id(category_id, option_id)
- var paths: Dictionary = (
- _feature_resource_paths.get(category_id, {}) as Dictionary
- )
- return str(paths.get(canonical_id, ""))
-
-
-static func _touch_feature_texture(cache_id: String) -> void:
- var existing_index := _feature_texture_cache_order.find(cache_id)
- if existing_index >= 0:
- _feature_texture_cache_order.remove_at(existing_index)
- _feature_texture_cache_order.append(cache_id)
-
-
-static func _trim_feature_texture_cache() -> void:
- while _feature_texture_cache_order.size() > FEATURE_TEXTURE_CACHE_LIMIT:
- var oldest_id: String = str(_feature_texture_cache_order.pop_front())
- _feature_textures.erase(oldest_id)
-
-
-static func refresh_feature_assets() -> void:
- _feature_assets_ready = false
- _feature_options.clear()
- _feature_resource_paths.clear()
- _feature_textures.clear()
- _feature_texture_cache_order.clear()
- _ensure_feature_assets()
-
-
-static func refresh_fur_pattern_assets() -> void:
- _fur_pattern_assets_ready = false
- _fur_pattern_options.clear()
- _fur_pattern_resource_paths.clear()
- _fur_pattern_textures.clear()
- _ensure_fur_pattern_assets()
-
-
-static func _ensure_fur_pattern_assets() -> void:
- if _fur_pattern_assets_ready:
- return
- _fur_pattern_assets_ready = true
- _fur_pattern_options = [{"id": DEFAULT_FUR_STYLE, "label": "solid"}]
- var root_directory := DirAccess.open(FUR_PATTERN_ASSET_ROOT)
- if root_directory == null:
- return
- var directory_names := root_directory.get_directories()
- directory_names.sort()
- for directory_name: String in directory_names:
- var pattern_id := _normalized_asset_id(directory_name)
- if pattern_id.is_empty() or pattern_id == DEFAULT_FUR_STYLE:
- continue
- var pattern_directory_path := FUR_PATTERN_ASSET_ROOT.path_join(
- directory_name
- )
- var pattern_directory := DirAccess.open(pattern_directory_path)
- if pattern_directory == null:
- continue
- var file_names := pattern_directory.get_files()
- file_names.sort()
- var canonical_files: Dictionary = {}
- var found_pattern_texture := false
- for file_name: String in file_names:
- var canonical_file_name := _canonical_feature_filename(file_name)
- if (
- canonical_file_name.is_empty()
- or canonical_files.has(canonical_file_name)
- ):
- continue
- canonical_files[canonical_file_name] = true
- var file_base_name := canonical_file_name.get_basename().to_lower()
- var pattern_suffix := "_" + pattern_id
- if not file_base_name.ends_with(pattern_suffix):
- continue
- var component_id := file_base_name.substr(
- 0,
- file_base_name.length() - pattern_suffix.length(),
- )
- component_id = _normalized_asset_id(component_id)
- if component_id.is_empty():
- continue
- var resource_path := pattern_directory_path.path_join(
- canonical_file_name
- )
- if not ResourceLoader.exists(resource_path, "Texture2D"):
- push_warning(
- "Ignoring fur mask without a loadable texture: "
- + resource_path
- )
- continue
- _fur_pattern_resource_paths[
- _fur_pattern_cache_id(pattern_id, component_id)
- ] = resource_path
- found_pattern_texture = true
- if found_pattern_texture:
- _fur_pattern_options.append({
- "id": pattern_id,
- "label": pattern_id.replace("_", " "),
- })
-
-
-static func _normalized_asset_id(value: String) -> String:
- var result := value.to_lower()
- result = result.replace("-", "_")
- result = result.replace(" ", "_")
- result = result.replace(".", "_")
- while result.contains("__"):
- result = result.replace("__", "_")
- while result.begins_with("_"):
- result = result.substr(1)
- while result.ends_with("_"):
- result = result.substr(0, result.length() - 1)
- return result
-
-
-static func _fur_pattern_cache_id(
- pattern_id: String,
- component_id: String,
-) -> String:
- return "%s:%s" % [pattern_id, component_id]
-
-
-static func _ensure_feature_assets() -> void:
- if _feature_assets_ready:
- return
- _feature_assets_ready = true
- for category_id: String in FEATURE_CATEGORIES:
- var options: Array = [{"id": "none", "label": "none"}]
- var resource_paths: Dictionary = {}
- var root_path := str(FEATURE_ASSET_ROOTS[category_id])
- var directory := DirAccess.open(root_path)
- if directory != null:
- var files := directory.get_files()
- files.sort()
- var normalized_paths: Dictionary = {}
- for file_name: String in files:
- var normalized_file_name := _canonical_feature_filename(file_name)
- if (
- normalized_file_name.is_empty()
- or normalized_paths.has(normalized_file_name)
- ):
- continue
- normalized_paths[normalized_file_name] = true
- var option_id := _feature_id_from_filename(
- category_id, normalized_file_name
- )
- if option_id.is_empty() or resource_paths.has(option_id):
- continue
- var resource_path := root_path.path_join(normalized_file_name)
- if not ResourceLoader.exists(resource_path, "Texture2D"):
- push_warning(
- "Ignoring facial feature without a loadable texture: "
- + resource_path
- )
- continue
- resource_paths[option_id] = resource_path
- options.append({
- "id": option_id,
- "label": _feature_label(option_id),
- })
-
- _feature_options[category_id] = options
- _feature_resource_paths[category_id] = resource_paths
-
-
-static func _canonical_feature_filename(file_name: String) -> String:
- var lower_name := file_name.to_lower()
- for sidecar_suffix: String in [".import", ".remap"]:
- if lower_name.ends_with(".png" + sidecar_suffix):
- return file_name.substr(
- 0, file_name.length() - sidecar_suffix.length()
- )
- if lower_name.ends_with(".png"):
- return file_name
- return ""
-
-
-static func _feature_id_from_filename(
- category_id: String,
- file_name: String,
-) -> String:
- var option_id := file_name.get_basename().to_lower()
- var prefix := category_id + "_"
- if option_id.begins_with(prefix):
- option_id = option_id.trim_prefix(prefix)
- option_id = option_id.replace("-", "_")
- option_id = option_id.replace(" ", "_")
- option_id = option_id.replace(".", "_")
- while option_id.contains("__"):
- option_id = option_id.replace("__", "_")
- while option_id.begins_with("_"):
- option_id = option_id.substr(1)
- while option_id.ends_with("_"):
- option_id = option_id.substr(0, option_id.length() - 1)
- return option_id
-
-
-static func _feature_label(option_id: String) -> String:
- if FEATURE_LABEL_OVERRIDES.has(option_id):
- return str(FEATURE_LABEL_OVERRIDES[option_id])
- return option_id.replace("_", " ")
-
-
-static func category_label(category_id: String) -> String:
- return str(CATEGORY_LABELS.get(category_id, category_id))
-
-
-static func is_valid_option(category_id: String, option_id: String) -> bool:
- option_id = canonical_option_id(category_id, option_id)
- if category_id in FUR_COLOR_IDS and is_custom_fur_color_id(option_id):
- return true
- for option: Dictionary in options_for(category_id):
- if str(option.get("id", "")) == option_id:
- return true
- return false
-
-
-static func canonical_option_id(category_id: String, option_id: String) -> String:
- if (
- category_id in FUR_COLOR_IDS
- and option_id.to_lower().begins_with(CUSTOM_FUR_COLOR_PREFIX)
- ):
- return option_id.to_lower()
- var alias_category_id := (
- FUR_STYLE_ID if category_id in FUR_STYLE_IDS else category_id
- )
- var aliases: Dictionary = LEGACY_OPTION_ALIASES.get(alias_category_id, {})
- return str(aliases.get(option_id, option_id))
-
-
-static func option_color(category_id: String, option_id: String) -> Color:
- var canonical_id: String = canonical_option_id(category_id, option_id)
- if category_id in FUR_COLOR_IDS and is_custom_fur_color_id(canonical_id):
- return Color(canonical_id.trim_prefix(CUSTOM_FUR_COLOR_PREFIX))
- for option: Dictionary in options_for(category_id):
- if str(option.get("id", "")) == canonical_id:
- return option.get("color", Color.WHITE) as Color
- return Color.WHITE
-
-
-static func custom_fur_color_id(color: Color) -> String:
- var opaque_color := Color(color.r, color.g, color.b, 1.0)
- return CUSTOM_FUR_COLOR_PREFIX + opaque_color.to_html(false)
-
-
-static func is_custom_fur_color_id(option_id: String) -> bool:
- var canonical_id := option_id.to_lower()
- if (
- canonical_id.length() != CUSTOM_FUR_COLOR_PREFIX.length() + 6
- or not canonical_id.begins_with(CUSTOM_FUR_COLOR_PREFIX)
- ):
- return false
- var hex_value := canonical_id.trim_prefix(CUSTOM_FUR_COLOR_PREFIX)
- for character_index: int in range(hex_value.length()):
- if hex_value[character_index] not in "0123456789abcdef":
- return false
- return true
-
-
-static func fur_color_label(category_id: String) -> String:
- return str(FUR_COLOR_LABELS.get(category_id, category_id))
-
-
-static func fur_style_label(category_id: String) -> String:
- return str(FUR_STYLE_LABELS.get(category_id, category_id))
-
-
-static func fur_style_field_for_component(component_id: String) -> String:
- if component_id == "body_arms":
- return FUR_STYLE_ARMS_ID
- if component_id.begins_with("head_"):
- return FUR_STYLE_HEAD_ID
- if component_id.begins_with("ears_"):
- return FUR_STYLE_EARS_ID
- if component_id.begins_with("tails_"):
- return FUR_STYLE_TAIL_ID
- return FUR_STYLE_ID
-
-
-static func fur_component_for_style_field(
- category_id: String,
- snapshot: Dictionary,
-) -> String:
- match category_id:
- FUR_STYLE_ID:
- return "body_main"
- FUR_STYLE_ARMS_ID:
- return "body_arms"
- FUR_STYLE_HEAD_ID:
- var species_id := canonical_option_id(
- "species", str(snapshot.get("species", "round"))
- )
- return "head_pointy" if species_id == "pointy" else "head_round"
- FUR_STYLE_EARS_ID:
- var ears_id := canonical_option_id(
- "ears", str(snapshot.get("ears", "none"))
- )
- return "" if ears_id == "none" else "ears_" + ears_id
- FUR_STYLE_TAIL_ID:
- var tail_id := canonical_option_id(
- "tail", str(snapshot.get("tail", "none"))
- )
- return "" if tail_id == "none" else "tails_" + tail_id
- return ""
-
-
-static func fur_pattern_options_for_field(
- category_id: String,
- snapshot: Dictionary,
-) -> Array:
- if category_id not in FUR_STYLE_IDS:
- return []
- _ensure_fur_pattern_assets()
- var component_id := fur_component_for_style_field(category_id, snapshot)
- var result: Array = []
- for option: Dictionary in _fur_pattern_options:
- var pattern_id := str(option.get("id", ""))
- if (
- pattern_id == DEFAULT_FUR_STYLE
- or (
- not component_id.is_empty()
- and _fur_pattern_resource_paths.has(
- _fur_pattern_cache_id(pattern_id, component_id)
- )
- )
- ):
- result.append(option)
- return result
-
-
-static func fur_pattern_texture(
- option_id: String,
- component_id: String = "body_main",
-) -> Texture2D:
- _ensure_fur_pattern_assets()
- var canonical_id := canonical_option_id(FUR_STYLE_ID, option_id)
- if canonical_id == DEFAULT_FUR_STYLE:
- return null
- var cache_id := _fur_pattern_cache_id(canonical_id, component_id)
- if _fur_pattern_textures.has(cache_id):
- return _fur_pattern_textures[cache_id] as Texture2D
- var resource_path := str(_fur_pattern_resource_paths.get(cache_id, ""))
- if resource_path.is_empty():
- return null
- var texture := ResourceLoader.load(resource_path, "Texture2D") as Texture2D
- if texture != null:
- _fur_pattern_textures[cache_id] = texture
- return texture
-
-
-static func appearance_signature_values(snapshot: Dictionary) -> Array:
- var result: Array = []
- for category_id: String in SNAPSHOT_IDS:
- if category_id == SCALE_CATEGORY_ID:
- result.append(character_scale_percent(snapshot.get(
- category_id,
- DEFAULT_CHARACTER_SCALE,
- )))
- else:
- result.append(str(snapshot.get(category_id, "")))
- return result
-
-
-static func validate_snapshot(value: Variant) -> bool:
- if typeof(value) != TYPE_DICTIONARY:
- return false
- var snapshot: Dictionary = value
- if snapshot.size() != SNAPSHOT_IDS.size():
- return false
- for category_id: String in SNAPSHOT_IDS:
- if category_id == SCALE_CATEGORY_ID:
- if not is_valid_character_scale(snapshot.get(category_id)):
- return false
- continue
- if (
- typeof(snapshot.get(category_id)) != TYPE_STRING
- or not is_valid_option(category_id, str(snapshot[category_id]))
- ):
- return false
- return true
-
-
-static func sanitized_snapshot(value: Variant) -> Dictionary:
- var result := default_snapshot()
- if typeof(value) != TYPE_DICTIONARY:
- return result
- var snapshot: Dictionary = value
- for category_id: String in SNAPSHOT_IDS:
- if category_id == SCALE_CATEGORY_ID:
- result[category_id] = character_scale(
- snapshot.get(category_id, DEFAULT_CHARACTER_SCALE)
- )
- continue
- var raw_option: Variant = snapshot.get(category_id, "")
- if (
- category_id in FUR_STYLE_IDS
- and category_id != FUR_STYLE_ID
- and not snapshot.has(category_id)
- ):
- raw_option = snapshot.get(FUR_STYLE_ID, DEFAULT_FUR_STYLE)
- var option_id: String = canonical_option_id(
- category_id, str(raw_option)
- )
- if is_valid_option(category_id, option_id):
- result[category_id] = option_id
- return result
-
-
-static func character_scale(value: Variant) -> float:
- if typeof(value) not in [TYPE_FLOAT, TYPE_INT]:
- return DEFAULT_CHARACTER_SCALE
- var number: float = float(value)
- if not is_finite(number):
- return DEFAULT_CHARACTER_SCALE
- return clampf(
- snappedf(number, CHARACTER_SCALE_STEP),
- MIN_CHARACTER_SCALE,
- MAX_CHARACTER_SCALE,
- )
-
-
-static func is_valid_character_scale(value: Variant) -> bool:
- if typeof(value) not in [TYPE_FLOAT, TYPE_INT]:
- return false
- var number: float = float(value)
- return (
- is_finite(number)
- and number >= MIN_CHARACTER_SCALE
- and number <= MAX_CHARACTER_SCALE
- and is_equal_approx(number, character_scale(number))
- )
-
-
-static func character_scale_percent(value: Variant) -> int:
- return roundi(character_scale(value) * 100.0)
diff --git a/progression/character_customization_catalog.gd.uid b/progression/character_customization_catalog.gd.uid
deleted file mode 100644
index e2f3303..0000000
--- a/progression/character_customization_catalog.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c08tu8ntuqig3
diff --git a/progression/player_appearance_store.gd b/progression/player_appearance_store.gd
deleted file mode 100644
index febe43d..0000000
--- a/progression/player_appearance_store.gd
+++ /dev/null
@@ -1,128 +0,0 @@
-class_name PlayerAppearanceStore
-extends Node
-
-const FORMAT_VERSION: int = 1
-var _snapshot: Dictionary = CharacterCustomizationCatalog.default_snapshot()
-var _loaded: bool = false
-var _future_version: bool = false
-var _profile_path := ""
-var _expected_hash := ""
-var _data_root: PlayerDataRoot
-
-
-func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
- _profile_path = path
- _data_root = data_root
- _snapshot = CharacterCustomizationCatalog.default_snapshot()
- _loaded = false
- _future_version = false
- _expected_hash = ""
-
-
-func _temp_path() -> String:
- return _profile_path + ".tmp"
-
-
-func _backup_path() -> String:
- return _profile_path + ".backup"
-
-
-func load_preferences() -> bool:
- if _profile_path.is_empty():
- return false
- _recover_interrupted_write()
- if not FileAccess.file_exists(_profile_path):
- _snapshot = CharacterCustomizationCatalog.default_snapshot()
- _loaded = true
- return true
- var file := FileAccess.open(_profile_path, FileAccess.READ)
- if file == null:
- return false
- var json := JSON.new()
- var error := json.parse(file.get_as_text())
- file.close()
- if error != OK or typeof(json.data) != TYPE_DICTIONARY:
- return _recover_backup()
- var data: Dictionary = json.data
- if typeof(data.get("format_version")) not in [TYPE_INT, TYPE_FLOAT]:
- return _recover_backup()
- var version := int(data["format_version"])
- if version > FORMAT_VERSION:
- _future_version = true
- _loaded = false
- return false
- if version != FORMAT_VERSION or typeof(data.get("appearance")) != TYPE_DICTIONARY:
- return _recover_backup()
- _snapshot = CharacterCustomizationCatalog.sanitized_snapshot(data["appearance"])
- _loaded = true
- _expected_hash = PortableFileGuard.hash_file(_profile_path)
- return true
-
-
-func get_snapshot() -> Dictionary:
- return _snapshot.duplicate(true)
-
-
-func save_snapshot(value: Dictionary) -> bool:
- if _future_version or not CharacterCustomizationCatalog.validate_snapshot(value):
- return false
- var previous := _snapshot.duplicate(true)
- _snapshot = value.duplicate(true)
- if _save_atomic():
- _loaded = true
- return true
- _snapshot = previous
- return false
-
-
-func is_loaded() -> bool:
- return _loaded
-
-
-func _save_atomic() -> bool:
- var bytes := JSON.stringify({
- "format_version": FORMAT_VERSION,
- "appearance": _snapshot,
- }, "\t").to_utf8_buffer()
- var result := PortableFileGuard.write_guarded(
- _profile_path, bytes, _expected_hash, _data_root.conflict_directory(),
- _data_root.device_id,
- )
- if bool(result.get("conflict", false)):
- _data_root.report_conflict(str(result.get("message", "")), str(result.get("conflict_path", "")))
- if bool(result.get("ok", false)):
- _expected_hash = str(result["hash"])
- return bool(result.get("ok", false))
-
-
-func _recover_interrupted_write() -> void:
- if FileAccess.file_exists(_profile_path):
- _remove(_temp_path())
- return
- if FileAccess.file_exists(_backup_path()):
- _rename(_backup_path(), _profile_path)
- _remove(_temp_path())
-
-
-func _recover_backup() -> bool:
- if not FileAccess.file_exists(_backup_path()):
- return false
- var primary_path := _profile_path
- var backup_path := _backup_path()
- var corrupt_path := "%s.corrupt.%d.json" % [_profile_path.get_basename(), int(Time.get_unix_time_from_system())]
- DirAccess.rename_absolute(primary_path, corrupt_path)
- if DirAccess.rename_absolute(backup_path, primary_path) != OK:
- return false
- return load_preferences()
-
-
-func _rename(from_path: String, to_path: String) -> bool:
- return DirAccess.rename_absolute(
- ProjectSettings.globalize_path(from_path),
- ProjectSettings.globalize_path(to_path),
- ) == OK
-
-
-func _remove(path: String) -> void:
- if FileAccess.file_exists(path):
- DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
diff --git a/progression/player_appearance_store.gd.uid b/progression/player_appearance_store.gd.uid
deleted file mode 100644
index 35c2e85..0000000
--- a/progression/player_appearance_store.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ci07gm3e7r8p6
diff --git a/progression/player_art_unlocks.gd b/progression/player_art_unlocks.gd
deleted file mode 100644
index 0bc7acc..0000000
--- a/progression/player_art_unlocks.gd
+++ /dev/null
@@ -1,179 +0,0 @@
-class_name PlayerArtUnlocks
-extends Node
-
-const PlayerWalletType = preload("res://economy/player_wallet.gd")
-
-signal unlocks_changed(unlock_mask: int)
-
-const BASE_BRUSH_SIZE: int = 1
-const BASE_GRID_SIZE: int = 16
-const STAMP_PRODUCT_ID: StringName = &"stamp_tool"
-const BRUSH_SIZES: Array[int] = [1, 2, 3, 4]
-const GRID_SIZES: Array[int] = [16, 32, 64, 128]
-
-# Stable product IDs are both the save and shop transaction boundary.
-const PRODUCT_BITS: Dictionary[StringName, int] = {
- &"marker_ocean_teal": 0,
- &"marker_coral": 1,
- &"marker_sunny": 2,
- &"marker_leaf": 3,
- &"marker_blue": 4,
- &"marker_violet": 5,
- &"marker_charcoal": 6,
- &"brush_2x": 7,
- &"brush_3x": 8,
- &"brush_4x": 9,
- &"grid_32x": 10,
- &"grid_64x": 11,
- &"grid_128x": 12,
- STAMP_PRODUCT_ID: 13,
-}
-const COLOR_PRODUCTS: Dictionary[StringName, StringName] = {
- &"marker_ocean_teal": &"ocean_teal",
- &"marker_coral": &"coral",
- &"marker_sunny": &"sunny",
- &"marker_leaf": &"leaf",
- &"marker_blue": &"blue",
- &"marker_violet": &"violet",
- &"marker_charcoal": &"charcoal",
-}
-const BRUSH_PRODUCTS: Dictionary[StringName, int] = {
- &"brush_2x": 2,
- &"brush_3x": 3,
- &"brush_4x": 4,
-}
-const GRID_PRODUCTS: Dictionary[StringName, int] = {
- &"grid_32x": 32,
- &"grid_64x": 64,
- &"grid_128x": 128,
-}
-const ALL_UNLOCK_MASK: int = (1 << 14) - 1
-
-var _unlock_mask: int = 0
-
-
-func get_unlock_mask() -> int:
- return _unlock_mask
-
-
-func owns_product(product_id: StringName) -> bool:
- var bit: int = get_product_bit(product_id)
- return bit >= 0 and (_unlock_mask & (1 << bit)) != 0
-
-
-func unlock_product(product_id: StringName) -> bool:
- var bit: int = get_product_bit(product_id)
- if bit < 0 or owns_product(product_id):
- return false
- _unlock_mask |= 1 << bit
- unlocks_changed.emit(_unlock_mask)
- return true
-
-
-func purchase_product(
- product_id: StringName,
- wallet: PlayerWalletType,
- cost: int,
-) -> bool:
- if (
- wallet == null
- or cost < 0
- or owns_product(product_id)
- or get_product_bit(product_id) < 0
- or not wallet.can_afford(cost)
- ):
- return false
- if not wallet.debit(cost):
- return false
- if unlock_product(product_id):
- return true
- if not wallet.credit(cost):
- push_error("Art upgrade purchase rollback failed.")
- return false
-
-
-func restore_mask(value: int) -> bool:
- if value < 0 or (value & ~ALL_UNLOCK_MASK) != 0:
- return false
- var changed: bool = _unlock_mask != value
- _unlock_mask = value
- if changed:
- unlocks_changed.emit(_unlock_mask)
- return true
-
-
-func reset_to_defaults() -> void:
- restore_mask(0)
-
-
-func to_save_data() -> Dictionary:
- return {"unlock_mask": _unlock_mask}
-
-
-func get_unlocked_color_ids() -> Array[StringName]:
- var result: Array[StringName] = [SurfaceDrawingPalette.DEFAULT_COLOR_ID]
- for product_id: StringName in COLOR_PRODUCTS:
- if owns_product(product_id):
- result.append(StringName(str(COLOR_PRODUCTS[product_id])))
- return result
-
-
-func get_unlocked_brush_sizes() -> Array[int]:
- var result: Array[int] = [BASE_BRUSH_SIZE]
- for product_id: StringName in BRUSH_PRODUCTS:
- if owns_product(product_id):
- result.append(int(BRUSH_PRODUCTS[product_id]))
- result.sort()
- return result
-
-
-func get_unlocked_grid_sizes() -> Array[int]:
- var result: Array[int] = [BASE_GRID_SIZE]
- for product_id: StringName in GRID_PRODUCTS:
- if owns_product(product_id):
- result.append(int(GRID_PRODUCTS[product_id]))
- result.sort()
- return result
-
-
-func is_color_unlocked(color_id: StringName) -> bool:
- return color_id in get_unlocked_color_ids()
-
-
-func is_brush_size_unlocked(brush_size: int) -> bool:
- return brush_size in get_unlocked_brush_sizes()
-
-
-func is_grid_size_unlocked(grid_size: int) -> bool:
- return grid_size in get_unlocked_grid_sizes()
-
-
-func is_stamp_unlocked() -> bool:
- return owns_product(STAMP_PRODUCT_ID)
-
-
-static func get_product_bit(product_id: StringName) -> int:
- return int(PRODUCT_BITS.get(product_id, -1))
-
-
-static func is_product_id(product_id: StringName) -> bool:
- return PRODUCT_BITS.has(product_id)
-
-
-static func resulting_mask(current_mask: int, product_id: StringName) -> int:
- var bit: int = get_product_bit(product_id)
- if bit < 0:
- return current_mask
- return current_mask | (1 << bit)
-
-
-static func color_id_for_product(product_id: StringName) -> StringName:
- return StringName(str(COLOR_PRODUCTS.get(product_id, "")))
-
-
-static func brush_size_for_product(product_id: StringName) -> int:
- return int(BRUSH_PRODUCTS.get(product_id, -1))
-
-
-static func grid_size_for_product(product_id: StringName) -> int:
- return int(GRID_PRODUCTS.get(product_id, -1))
diff --git a/progression/player_art_unlocks.gd.uid b/progression/player_art_unlocks.gd.uid
deleted file mode 100644
index e0d2f0e..0000000
--- a/progression/player_art_unlocks.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cixpwmn33k2x2
diff --git a/progression/player_asset_reservation_service.gd b/progression/player_asset_reservation_service.gd
deleted file mode 100644
index f66e672..0000000
--- a/progression/player_asset_reservation_service.gd
+++ /dev/null
@@ -1,251 +0,0 @@
-class_name PlayerAssetReservationService
-extends Node
-
-enum AttachmentType { NONE, FISH_COIN, FISH, CONSUMABLE }
-
-signal reservations_changed
-
-var _wallet: PlayerWallet
-var _inventory: FishInventory
-var _bag: PlayerBag
-var _catalog: ItemCatalog
-var _reservations: Dictionary[String, Dictionary] = {}
-var _mail_transfer_authorizations: Dictionary[String, Dictionary] = {}
-
-
-func setup(
- wallet: PlayerWallet,
- inventory: FishInventory,
- bag: PlayerBag,
- catalog: ItemCatalog,
-) -> void:
- _wallet = wallet
- _inventory = inventory
- _bag = bag
- _catalog = catalog
-
-
-func reserve(reservation_id: String, attachment: Dictionary) -> bool:
- if (
- reservation_id.is_empty()
- or reservation_id.length() > 96
- or _reservations.has(reservation_id)
- or not validate_attachment(attachment)
- ):
- return false
- var kind: int = attachment["type"]
- match kind:
- AttachmentType.NONE:
- return true
- AttachmentType.FISH_COIN:
- if int(attachment["amount"]) > get_available_fish_coin():
- return false
- AttachmentType.FISH:
- var catch_id := StringName(str(attachment["catch_id"]))
- var fish_catch := _inventory.get_catch_by_id(catch_id)
- if (
- fish_catch == null
- or fish_catch.is_favorited
- or is_fish_reserved(catch_id)
- ):
- return false
- AttachmentType.CONSUMABLE:
- var item_id := StringName(str(attachment["item_id"]))
- if (
- int(attachment["quantity"])
- > get_available_item_quantity(item_id)
- ):
- return false
- _reservations[reservation_id] = attachment.duplicate(true)
- reservations_changed.emit()
- return true
-
-
-func release(reservation_id: String) -> bool:
- if not _reservations.erase(reservation_id):
- return false
- _mail_transfer_authorizations.erase(reservation_id)
- reservations_changed.emit()
- return true
-
-
-func release_all() -> void:
- if _reservations.is_empty():
- return
- _reservations.clear()
- _mail_transfer_authorizations.clear()
- reservations_changed.emit()
-
-
-func authorize_mail_transfer(
- reservation_id: String,
- catch_id: StringName,
- transfer_id: String,
-) -> bool:
- var attachment := get_reservation(reservation_id)
- if (
- transfer_id.is_empty()
- or transfer_id.length() > 96
- or attachment.is_empty()
- or int(attachment.get("type", 0)) != AttachmentType.FISH
- or StringName(str(attachment.get("catch_id", ""))) != catch_id
- ):
- return false
- _mail_transfer_authorizations[reservation_id] = {
- "catch_id": catch_id,
- "transfer_id": transfer_id,
- }
- return true
-
-
-func authorize_mail_fish_removal(
- reservation_id: String,
- catch_id: StringName,
- transfer_id: String,
-) -> bool:
- var authorization: Dictionary = _mail_transfer_authorizations.get(
- reservation_id, {}
- )
- return (
- not authorization.is_empty()
- and StringName(authorization.get("catch_id", StringName())) == catch_id
- and str(authorization.get("transfer_id", "")) == transfer_id
- )
-
-
-func has_reservation(reservation_id: String) -> bool:
- return _reservations.has(reservation_id)
-
-
-func get_reservation(reservation_id: String) -> Dictionary:
- return _reservations.get(reservation_id, {}).duplicate(true)
-
-
-func get_available_fish_coin() -> int:
- return maxi(_wallet.get_balance() - get_reserved_fish_coin(), 0)
-
-
-func get_reserved_fish_coin() -> int:
- var total := 0
- for attachment: Dictionary in _reservations.values():
- if int(attachment.get("type", 0)) == AttachmentType.FISH_COIN:
- total += int(attachment.get("amount", 0))
- return total
-
-
-func get_available_item_quantity(item_id: StringName) -> int:
- return maxi(_bag.get_quantity(item_id) - get_reserved_item_quantity(item_id), 0)
-
-
-func get_reserved_item_quantity(item_id: StringName) -> int:
- var total := 0
- for attachment: Dictionary in _reservations.values():
- if (
- int(attachment.get("type", 0)) == AttachmentType.CONSUMABLE
- and StringName(str(attachment.get("item_id", ""))) == item_id
- ):
- total += int(attachment.get("quantity", 0))
- return total
-
-
-func is_fish_reserved(catch_id: StringName) -> bool:
- for attachment: Dictionary in _reservations.values():
- if (
- int(attachment.get("type", 0)) == AttachmentType.FISH
- and StringName(str(attachment.get("catch_id", ""))) == catch_id
- ):
- return true
- return false
-
-
-func can_commit(reservation_id: String) -> bool:
- var attachment := get_reservation(reservation_id)
- if attachment.is_empty():
- return false
- match int(attachment["type"]):
- AttachmentType.FISH_COIN:
- return _wallet.can_afford(int(attachment["amount"]))
- AttachmentType.FISH:
- return _inventory.get_catch_by_id(
- StringName(str(attachment["catch_id"]))
- ) != null
- AttachmentType.CONSUMABLE:
- return (
- _bag.get_quantity(StringName(str(attachment["item_id"])))
- >= int(attachment["quantity"])
- )
- return false
-
-
-func commit_removal(
- reservation_id: String,
- mail_transfer_id: String = "",
-) -> bool:
- var attachment := get_reservation(reservation_id)
- if attachment.is_empty() or not can_commit(reservation_id):
- return false
- var applied := false
- match int(attachment["type"]):
- AttachmentType.FISH_COIN:
- applied = _wallet.debit(int(attachment["amount"]))
- AttachmentType.FISH:
- var catch_id := StringName(str(attachment["catch_id"]))
- if authorize_mail_transfer(
- reservation_id, catch_id, mail_transfer_id
- ):
- applied = (
- _inventory.remove_reserved_catch_for_mail_transfer(
- catch_id, reservation_id, mail_transfer_id
- )
- != null
- )
- AttachmentType.CONSUMABLE:
- applied = _bag.remove_item(
- StringName(str(attachment["item_id"])),
- int(attachment["quantity"])
- )
- if applied:
- release(reservation_id)
- else:
- _mail_transfer_authorizations.erase(reservation_id)
- return applied
-
-
-func validate_attachment(attachment: Dictionary) -> bool:
- if typeof(attachment.get("type")) != TYPE_INT:
- return false
- match int(attachment["type"]):
- AttachmentType.NONE:
- return attachment.size() == 1
- AttachmentType.FISH_COIN:
- return (
- attachment.size() == 2
- and
- typeof(attachment.get("amount")) == TYPE_INT
- and int(attachment["amount"]) >= 1
- and int(attachment["amount"]) <= 1000000000
- )
- AttachmentType.FISH:
- return (
- attachment.size() == 3
- and
- typeof(attachment.get("catch_id")) == TYPE_STRING
- and not str(attachment["catch_id"]).is_empty()
- and str(attachment["catch_id"]).length() <= 160
- and typeof(attachment.get("catch")) == TYPE_DICTIONARY
- )
- AttachmentType.CONSUMABLE:
- if (
- attachment.size() != 3
- or
- typeof(attachment.get("item_id")) != TYPE_STRING
- or typeof(attachment.get("quantity")) != TYPE_INT
- or int(attachment["quantity"]) < 1
- or int(attachment["quantity"]) > 999
- ):
- return false
- var item := _catalog.get_item_by_id(
- StringName(str(attachment["item_id"]))
- )
- return item != null and item.category == ItemData.Category.CONSUMABLE
- return false
diff --git a/progression/player_asset_reservation_service.gd.uid b/progression/player_asset_reservation_service.gd.uid
deleted file mode 100644
index aa45786..0000000
--- a/progression/player_asset_reservation_service.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bheplri3k6ch7
diff --git a/progression/player_cooler_capacity.gd b/progression/player_cooler_capacity.gd
index b8c62be..1e77784 100644
--- a/progression/player_cooler_capacity.gd
+++ b/progression/player_cooler_capacity.gd
@@ -5,13 +5,9 @@ const PlayerWalletType = preload("res://economy/player_wallet.gd")
signal capacity_changed(level: int, capacity: int)
-## Kept under the historical class name so existing saves and network product
-## identifiers migrate without losing their purchased level. This progression
-## now controls the player's private storage box rather than carried catches.
-const CAPACITIES: Array[int] = [9, 18, 27, 36, 45, 54, 63, 72]
-const EXPANSION_COSTS: Array[int] = [75, 175, 400, 850, 1500, 2500, 4000]
-const MAX_LEVEL: int = 7
-const MAX_CAPACITY: int = 72
+const CAPACITIES: Array[int] = [12, 18, 24, 32, 40]
+const EXPANSION_COSTS: Array[int] = [75, 175, 400, 850]
+const MAX_LEVEL: int = 4
var _capacity_level: int = 0
diff --git a/progression/player_experience.gd b/progression/player_experience.gd
deleted file mode 100644
index ec5ba20..0000000
--- a/progression/player_experience.gd
+++ /dev/null
@@ -1,130 +0,0 @@
-class_name PlayerExperience
-extends Node
-
-signal experience_changed(total_experience: int, level: int)
-signal experience_awarded(
- amount: int,
- previous_total: int,
- new_total: int,
- previous_level: int,
- new_level: int,
-)
-
-const MAX_TOTAL_EXPERIENCE: int = 1000000000000
-const MAX_LEVEL: int = 100000
-
-var _total_experience: int = 0
-
-
-func get_total_experience() -> int:
- return _total_experience
-
-
-func get_level() -> int:
- return level_for_total_experience(_total_experience)
-
-
-func get_experience_in_level() -> int:
- return _total_experience - total_experience_for_level(get_level())
-
-
-func get_experience_for_next_level() -> int:
- return experience_required_for_next_level(get_level())
-
-
-func get_level_progress() -> float:
- return progress_for_total_experience(_total_experience)
-
-
-func award_experience(amount: int) -> bool:
- if amount <= 0 or _total_experience >= MAX_TOTAL_EXPERIENCE:
- return false
- var previous_total: int = _total_experience
- var previous_level: int = level_for_total_experience(previous_total)
- _total_experience = mini(
- _total_experience + amount,
- MAX_TOTAL_EXPERIENCE,
- )
- var awarded_amount: int = _total_experience - previous_total
- if awarded_amount <= 0:
- return false
- var new_level: int = level_for_total_experience(_total_experience)
- experience_changed.emit(_total_experience, new_level)
- experience_awarded.emit(
- awarded_amount,
- previous_total,
- _total_experience,
- previous_level,
- new_level,
- )
- return true
-
-
-func restore_total_experience(total_experience: int) -> bool:
- if total_experience < 0 or total_experience > MAX_TOTAL_EXPERIENCE:
- return false
- var changed: bool = _total_experience != total_experience
- _total_experience = total_experience
- if changed:
- experience_changed.emit(_total_experience, get_level())
- return true
-
-
-func reset_to_defaults() -> void:
- restore_total_experience(0)
-
-
-func to_save_data() -> Dictionary:
- return {"total_experience": _total_experience}
-
-
-static func experience_required_for_next_level(level: int) -> int:
- var safe_level: int = maxi(level, 1)
- var level_index: int = safe_level - 1
- return 100 + level_index * 25 + level_index * level_index * 5
-
-
-static func total_experience_for_level(level: int) -> int:
- var completed_levels: int = clampi(level - 1, 0, MAX_LEVEL - 1)
- if completed_levels == 0:
- return 0
- var linear_sum: int = floori(
- float(completed_levels) * float(completed_levels - 1) / 2.0
- )
- var square_sum: int = floori(
- float(completed_levels - 1)
- * float(completed_levels)
- * float(2 * completed_levels - 1)
- / 6.0
- )
- return completed_levels * 100 + linear_sum * 25 + square_sum * 5
-
-
-static func level_for_total_experience(total_experience: int) -> int:
- var safe_total: int = clampi(
- total_experience,
- 0,
- MAX_TOTAL_EXPERIENCE,
- )
- var low: int = 1
- var high: int = MAX_LEVEL
- while low < high:
- var middle: int = low + floori(float(high - low + 1) / 2.0)
- if total_experience_for_level(middle) <= safe_total:
- low = middle
- else:
- high = middle - 1
- return low
-
-
-static func progress_for_total_experience(total_experience: int) -> float:
- var level: int = level_for_total_experience(total_experience)
- var level_start: int = total_experience_for_level(level)
- var required: int = experience_required_for_next_level(level)
- if required <= 0:
- return 0.0
- return clampf(
- float(total_experience - level_start) / float(required),
- 0.0,
- 1.0,
- )
diff --git a/progression/player_experience.gd.uid b/progression/player_experience.gd.uid
deleted file mode 100644
index 9b49089..0000000
--- a/progression/player_experience.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://eml3k7a43e1d
diff --git a/progression/player_fishing_upgrades.gd b/progression/player_fishing_upgrades.gd
index e21a31e..7bc6abd 100644
--- a/progression/player_fishing_upgrades.gd
+++ b/progression/player_fishing_upgrades.gd
@@ -6,17 +6,9 @@ const PlayerWalletType = preload("res://economy/player_wallet.gd")
signal upgrades_changed(reel_speed_level: int, barrier_power_level: int)
const MAX_REEL_SPEED_LEVEL: int = 5
-const MAX_BARRIER_POWER_LEVEL: int = 7
+const MAX_BARRIER_POWER_LEVEL: int = 3
const REEL_SPEED_COSTS: Array[int] = [50, 125, 300, 650, 1250]
-const BARRIER_POWER_COSTS: Array[int] = [
- 100,
- 275,
- 700,
- 1500,
- 3000,
- 6000,
- 12000,
-]
+const BARRIER_POWER_COSTS: Array[int] = [100, 275, 700]
var _reel_speed_level: int = 0
var _barrier_power_level: int = 0
@@ -35,11 +27,7 @@ func get_reel_speed_multiplier() -> float:
func get_barrier_damage() -> int:
- return get_barrier_damage_for_level(_barrier_power_level)
-
-
-static func get_barrier_damage_for_level(level: int) -> int:
- return 1 << clampi(level, 0, MAX_BARRIER_POWER_LEVEL)
+ return _barrier_power_level + 1
func get_next_reel_speed_cost() -> int:
diff --git a/progression/player_item_effects.gd b/progression/player_item_effects.gd
index 2d4c23d..47cdbb5 100644
--- a/progression/player_item_effects.gd
+++ b/progression/player_item_effects.gd
@@ -10,14 +10,6 @@ const COFFEE_ID: StringName = &"coffee"
const ENERGY_DRINK_ID: StringName = &"energy_drink"
const SNACK_ID: StringName = &"snack"
const FISH_FINDER_ID: StringName = &"fish_finder"
-const BATTERIES_ID: StringName = &"batteries"
-const EFFECT_IDS: Array[StringName] = [
- COFFEE_ID,
- ENERGY_DRINK_ID,
- SNACK_ID,
- FISH_FINDER_ID,
-]
-const FISH_FINDER_DEAD_MESSAGE: String = "hmm... batteries are dead..."
const COFFEE_DURATION: float = 90.0
const ENERGY_DRINK_DURATION: float = 90.0
const SNACK_DURATION: float = 60.0
@@ -53,67 +45,22 @@ func use_consumable(
item: ItemDataType,
bag: PlayerBagType,
) -> bool:
- return use_item(item, bag)
-
-
-func use_item(
- item: ItemDataType,
- bag: PlayerBagType,
-) -> bool:
- var is_fish_finder: bool = (
- item != null
- and item.item_id == FISH_FINDER_ID
- and item.category == ItemDataType.Category.TOOL
- and item.equippable
- )
if (
item == null
or bag == null
- or (
- item.category != ItemDataType.Category.CONSUMABLE
- and not is_fish_finder
- )
+ or item.category != ItemDataType.Category.CONSUMABLE
or not item.usable
or not _remaining.has(item.item_id)
or not bag.owns_item(item.item_id)
):
return false
- var consumed_item_id: StringName = (
- BATTERIES_ID if is_fish_finder else item.item_id
- )
- if not bag.remove_item(consumed_item_id, 1):
+ if not bag.remove_item(item.item_id, 1):
return false
_remaining[item.item_id] = _get_duration(item.item_id)
effects_changed.emit()
return true
-func activate_authoritative(item_id: StringName, duration: float) -> bool:
- if not _remaining.has(item_id) or not is_finite(duration) or duration <= 0.0:
- return false
- _remaining[item_id] = duration
- effects_changed.emit()
- return true
-
-
-func restore_remaining(snapshot: Dictionary[StringName, float]) -> void:
- for item_id: StringName in _remaining:
- _remaining[item_id] = maxf(snapshot.get(item_id, 0.0), 0.0)
- effects_changed.emit()
-
-
-func get_remaining_snapshot() -> Dictionary[StringName, float]:
- return _remaining.duplicate()
-
-
-func get_registered_effect_ids() -> Array[StringName]:
- return EFFECT_IDS.duplicate()
-
-
-func get_effect_duration(item_id: StringName) -> float:
- return _get_duration(item_id)
-
-
func reset_all() -> void:
var changed: bool = false
for item_id: StringName in _remaining:
diff --git a/project.godot b/project.godot
index 5bcbb4d..525d8f0 100644
--- a/project.godot
+++ b/project.godot
@@ -10,45 +10,23 @@ config_version=5
[application]
-config/name="straywild"
-config/version="0.20.2-alpha"
+config/name="NETFISHING"
+config/version="0.2.0-prealpha.2"
run/main_scene="res://main/main.tscn"
config/features=PackedStringArray("4.7", "GL Compatibility")
-config/icon="res://art/exported/system_icons/straywild_256.png"
-
-[autoload]
-
-TextureSamplingPolicy="*res://main/texture_sampling_policy.gd"
+config/icon="res://icon.svg"
[display]
+window/stretch/mode="disabled"
window/stretch/aspect="expand"
-[editor_plugins]
-
-enabled=PackedStringArray("res://addons/straywild_shoreline_baker/plugin.cfg")
-
[gui]
-timers/tooltip_delay_sec=0.0
theme/custom="res://ui/game_theme.tres"
[input]
-ui_accept={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194309,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194310,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
-, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":0,"pressure":0.0,"pressed":false,"script":null)
-]
-}
-ui_cancel={
-"deadzone": 0.5,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194305,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":1,"pressure":0.0,"pressed":false,"script":null)
-]
-}
move_forward={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
@@ -76,19 +54,16 @@ move_right={
sprint={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":7,"pressure":0.0,"pressed":false,"script":null)
]
}
jump={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":0,"pressure":0.0,"pressed":false,"script":null)
]
}
sneak={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":8,"pressure":0.0,"pressed":false,"script":null)
]
}
slow_walk={
@@ -99,18 +74,11 @@ slow_walk={
interact={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":3,"pressure":0.0,"pressed":false,"script":null)
]
}
fish_primary={
"deadzone": 0.2,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":32,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
-, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":10,"pressure":0.0,"pressed":false,"script":null)
-]
-}
-reel_alternate={
-"deadzone": 0.2,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":96,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
camera_drag={
@@ -173,53 +141,14 @@ hotbar_9={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":57,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
-hotbar_previous={
-"deadzone": 0.2,
-"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null)
-]
-}
-hotbar_next={
-"deadzone": 0.2,
-"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":14,"pressure":0.0,"pressed":false,"script":null)
-]
-}
open_backpack={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":2,"pressure":0.0,"pressed":false,"script":null)
-]
-}
-open_system_menu={
-"deadzone": 0.2,
-"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":6,"pressure":0.0,"pressed":false,"script":null)
]
}
open_emotes={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":11,"pressure":0.0,"pressed":false,"script":null)
-]
-}
-character_call={
-"deadzone": 0.2,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":71,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":3,"pressure":0.0,"pressed":false,"script":null)
-]
-}
-open_quick_actions={
-"deadzone": 0.2,
-"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":12,"pressure":0.0,"pressed":false,"script":null)
-]
-}
-open_chat={
-"deadzone": 0.2,
-"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":84,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
-, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":4,"pressure":0.0,"pressed":false,"script":null)
-]
-}
-focus_gameplay={
-"deadzone": 0.2,
-"events": [Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":9,"pressure":0.0,"pressed":false,"script":null)
]
}
open_tacklebox={
@@ -258,10 +187,6 @@ alternate_fishing={
]
}
-[network]
-
-discovery/base_url="https://discovery.straywild.io/patron-test"
-
[physics]
3d/physics_engine="Jolt Physics"
@@ -271,4 +196,3 @@ discovery/base_url="https://discovery.straywild.io/patron-test"
rendering_device/driver.windows="d3d12"
renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"
-textures/vram_compression/import_etc2_astc=true
diff --git a/save/player_save_inspection.gd b/save/player_save_inspection.gd
index ff14de6..be5fd24 100644
--- a/save/player_save_inspection.gd
+++ b/save/player_save_inspection.gd
@@ -14,10 +14,6 @@ var detected_version: int = -1
var catch_count: int = 0
var wallet_balance: int = 0
var discovered_species_count: int = 0
-var total_experience: int = 0
-var player_level: int = 1
-var world_layout: StringName = &"generated_world"
-var world_seed: int = 0
var has_primary_file: bool = false
var message: String = ""
diff --git a/save/player_save_manager.gd b/save/player_save_manager.gd
index 56fd49c..26c8ed6 100644
--- a/save/player_save_manager.gd
+++ b/save/player_save_manager.gd
@@ -2,7 +2,6 @@ class_name PlayerSaveManager
extends Node
const FishCatchType = preload("res://fish/fish_catch.gd")
-const FishQualityType = preload("res://fish/fish_quality.gd")
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
const CollectionLogType = preload("res://collection/collection_log.gd")
const PlayerWalletType = preload("res://economy/player_wallet.gd")
@@ -12,72 +11,33 @@ const ItemCatalogType = preload("res://items/item_catalog.gd")
const OwnedItemType = preload("res://items/owned_item.gd")
const PlayerBagType = preload("res://inventory/player_bag.gd")
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
-const PlayerInventoryLayoutType = preload(
- "res://inventory/player_inventory_layout.gd"
-)
const PlayerFishingUpgradesType = preload(
"res://progression/player_fishing_upgrades.gd"
)
const PlayerCoolerCapacityType = preload(
"res://progression/player_cooler_capacity.gd"
)
-const PlayerArtUnlocksType = preload(
- "res://progression/player_art_unlocks.gd"
-)
-const PlayerExperienceType = preload(
- "res://progression/player_experience.gd"
-)
-const WorldTimeServiceType = preload("res://world/world_time_service.gd")
-const WorldWeatherServiceType = preload(
- "res://world/world_weather_service.gd"
-)
-const WorldLayoutType = preload("res://world/world_layout.gd")
-const PlayerJobServiceType = preload("res://jobs/player_job_service.gd")
-const PlayerType = preload("res://player/player.gd")
-const PlayerHomeStateType = preload("res://homes/player_home_state.gd")
-const SAVE_VERSION: int = 11
-const LEGACY_SAVE_FILENAME := "player_save.json"
-const ARCHIVE_EXTENSION := ".nfsave"
+const SAVE_VERSION: int = 4
const BASIC_ROD_ID: StringName = &"basic_fishing_rod"
+const SAVE_PATH: String = "user://player_save.json"
+const TEMP_PATH: String = "user://player_save.json.tmp"
+const BACKUP_PATH: String = "user://player_save.json.backup"
const MAX_SAFE_BALANCE: int = 1000000000000
-const DEFAULT_WORLD_SEED: int = 13001
-const MAX_WORLD_SEED: int = 2147483646
class LoadSnapshot:
extends RefCounted
var catches: Array[FishCatchType] = []
var discovered_ids: Array[StringName] = []
- var discovered_quality_masks: Dictionary[StringName, int] = {}
- var catch_counts: Dictionary[StringName, int] = {}
var wallet_balance: int = 0
var next_catch_sequence: int = 1
var bag_items: Array[OwnedItemType] = []
- var unlocked_bait_ids: Array[StringName] = []
- var active_bait_id: StringName
- var active_lure_id: StringName
var hotbar_slots: Array[StringName] = []
- var fish_hotbar_slots: Array[StringName] = []
var selected_hotbar_slot: int = 0
- var inventory_layout_data: Dictionary = {}
var reel_speed_level: int = 0
var barrier_power_level: int = 0
var cooler_capacity_level: int = 0
- var art_unlock_mask: int = 0
- var total_experience: int = 0
- var world_layout: StringName = WorldLayoutType.GENERATED
- var world_seed: int = DEFAULT_WORLD_SEED
- var world_time_hours: float = WorldTimeServiceType.DEFAULT_START_HOUR
- var has_world_weather_state: bool = false
- var world_weather: WorldWeatherServiceType.Weather = (
- WorldWeatherServiceType.DEFAULT_WEATHER
- )
- var world_weather_seconds_remaining: float = (
- WorldWeatherServiceType.SUNNY_DURATION_RANGE.x
- )
- var jobs_data: Dictionary = PlayerJobServiceType.default_save_data()
- var home_data: Dictionary = PlayerHomeStateType.default_save_data()
@export_range(0.05, 5.0, 0.05) var autosave_delay: float = 0.5
@@ -88,70 +48,15 @@ var _wallet: PlayerWalletType
var _catalog: FishPoolType
var _bag: PlayerBagType
var _hotbar: PlayerHotbarType
-var _inventory_layout: PlayerInventoryLayoutType
var _item_catalog: ItemCatalogType
var _fishing_upgrades: PlayerFishingUpgradesType
var _cooler_capacity: PlayerCoolerCapacityType
-var _art_unlocks: PlayerArtUnlocksType
-var _experience: PlayerExperienceType
-var _world_time: WorldTimeServiceType
-var _world_weather: WorldWeatherServiceType
-var _jobs: PlayerJobServiceType
-var _player: PlayerType
-var _home_state: PlayerHomeStateType
var _autosave_timer: Timer
var _is_configured: bool = false
var _is_restoring: bool = false
var _is_dirty: bool = false
var _automatic_saving_blocked: bool = false
var _autosave_enabled: bool = false
-var _save_path := ""
-var _expected_hash := ""
-var _data_root: PlayerDataRoot
-var _world_layout: StringName = WorldLayoutType.GENERATED
-var _world_seed: int = DEFAULT_WORLD_SEED
-
-
-func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
- if not select_storage(path, data_root):
- push_error("PlayerSaveManager could not configure progression storage.")
-
-
-func select_storage(path: String, data_root: PlayerDataRoot) -> bool:
- if path.is_empty() or data_root == null:
- return false
- if _autosave_enabled or _is_dirty:
- return false
- if _autosave_timer != null:
- _autosave_timer.stop()
- _save_path = path
- _data_root = data_root
- _expected_hash = ""
- _automatic_saving_blocked = false
- return true
-
-
-func _temp_path() -> String:
- return _save_path + ".tmp"
-
-
-func _backup_path() -> String:
- return _save_path + ".backup"
-
-
-func _codec_scratch_path() -> String:
- return _save_path + ".codec.tmp"
-
-
-func _legacy_save_path() -> String:
- return _save_path.get_base_dir().path_join(LEGACY_SAVE_FILENAME)
-
-
-func _read_path() -> String:
- if FileAccess.file_exists(_save_path):
- return _save_path
- var legacy: String = _legacy_save_path()
- return legacy if FileAccess.file_exists(legacy) else ""
func _ready() -> void:
@@ -168,17 +73,9 @@ func setup(
catalog: FishPoolType,
bag: PlayerBagType,
hotbar: PlayerHotbarType,
- inventory_layout: PlayerInventoryLayoutType,
item_catalog: ItemCatalogType,
fishing_upgrades: PlayerFishingUpgradesType,
cooler_capacity: PlayerCoolerCapacityType,
- art_unlocks: PlayerArtUnlocksType,
- experience: PlayerExperienceType,
- world_time: WorldTimeServiceType,
- world_weather: WorldWeatherServiceType,
- jobs: PlayerJobServiceType,
- player: PlayerType,
- home_state: PlayerHomeStateType,
) -> void:
_inventory = inventory
_collection_log = collection_log
@@ -186,17 +83,9 @@ func setup(
_catalog = catalog
_bag = bag
_hotbar = hotbar
- _inventory_layout = inventory_layout
_item_catalog = item_catalog
_fishing_upgrades = fishing_upgrades
_cooler_capacity = cooler_capacity
- _art_unlocks = art_unlocks
- _experience = experience
- _world_time = world_time
- _world_weather = world_weather
- _jobs = jobs
- _player = player
- _home_state = home_state
_is_configured = (
_inventory != null
and _collection_log != null
@@ -204,35 +93,25 @@ func setup(
and _catalog != null
and _bag != null
and _hotbar != null
- and _inventory_layout != null
and _item_catalog != null
and _fishing_upgrades != null
and _cooler_capacity != null
- and _art_unlocks != null
- and _experience != null
- and _world_time != null
- and _world_weather != null
- and _jobs != null
- and _player != null
- and _home_state != null
)
if not _is_configured:
push_error("PlayerSaveManager setup is missing required references.")
return
if not _inventory.catches_changed.is_connected(_mark_dirty):
_inventory.catches_changed.connect(_mark_dirty)
- if not _collection_log.collection_changed.is_connected(
- _on_collection_changed
+ if not _collection_log.fish_discovered.is_connected(
+ _on_fish_discovered
):
- _collection_log.collection_changed.connect(_on_collection_changed)
+ _collection_log.fish_discovered.connect(_on_fish_discovered)
if not _wallet.balance_changed.is_connected(_on_balance_changed):
_wallet.balance_changed.connect(_on_balance_changed)
if not _bag.contents_changed.is_connected(_mark_dirty):
_bag.contents_changed.connect(_mark_dirty)
if not _hotbar.slots_changed.is_connected(_mark_dirty):
_hotbar.slots_changed.connect(_mark_dirty)
- if not _inventory_layout.layout_changed.is_connected(_mark_dirty):
- _inventory_layout.layout_changed.connect(_mark_dirty)
if not _hotbar.selected_slot_changed.is_connected(
_on_selected_hotbar_slot_changed
):
@@ -249,20 +128,6 @@ func setup(
_cooler_capacity.capacity_changed.connect(
_on_cooler_capacity_changed
)
- if not _art_unlocks.unlocks_changed.is_connected(_on_art_unlocks_changed):
- _art_unlocks.unlocks_changed.connect(_on_art_unlocks_changed)
- if not _experience.experience_changed.is_connected(
- _on_experience_changed
- ):
- _experience.experience_changed.connect(_on_experience_changed)
- if not _jobs.changed.is_connected(_mark_dirty):
- _jobs.changed.connect(_mark_dirty)
- if not _player.active_bait_changed.is_connected(_on_active_tackle_changed):
- _player.active_bait_changed.connect(_on_active_tackle_changed)
- if not _player.active_lure_changed.is_connected(_on_active_tackle_changed):
- _player.active_lure_changed.connect(_on_active_tackle_changed)
- if not _home_state.changed.is_connected(_mark_dirty):
- _home_state.changed.connect(_mark_dirty)
func load_player_data() -> bool:
@@ -270,22 +135,23 @@ func load_player_data() -> bool:
return false
_automatic_saving_blocked = false
_recover_interrupted_write()
- var read_path: String = _read_path()
- if _save_path.is_empty() or read_path.is_empty():
+ if not FileAccess.file_exists(SAVE_PATH):
_is_dirty = false
return true
- var decoded: Dictionary = ProgressionSaveCodec.read_local_save(
- read_path,
- read_path == _legacy_save_path(),
- )
- if not bool(decoded.get("ok", false)):
- _handle_corrupt_save("Player save could not be decoded.", read_path)
+
+ var save_file := FileAccess.open(SAVE_PATH, FileAccess.READ)
+ if save_file == null:
+ _handle_corrupt_save("Unable to open player save.")
return false
- _expected_hash = (
- PortableFileGuard.hash_file(_save_path)
- if read_path == _save_path else ""
- )
- var save_data: Dictionary = decoded["data"]
+ var json_text: String = save_file.get_as_text()
+ save_file.close()
+
+ var json := JSON.new()
+ var parse_error: Error = json.parse(json_text)
+ if parse_error != OK or typeof(json.data) != TYPE_DICTIONARY:
+ _handle_corrupt_save("Player save contains malformed JSON.")
+ return false
+ var save_data: Dictionary = json.data
var version: int = _read_integer(save_data.get("save_version"), -1)
if version > SAVE_VERSION:
_automatic_saving_blocked = true
@@ -302,16 +168,13 @@ func load_player_data() -> bool:
save_data = _migrate_save(save_data, version)
if save_data.is_empty():
_handle_corrupt_save(
- "Player save version %d is unsupported." % version,
- read_path,
+ "Player save version %d is unsupported." % version
)
return false
var snapshot: LoadSnapshot = _build_load_snapshot(save_data)
if snapshot == null:
- _handle_corrupt_save(
- "Player save failed structural validation.", read_path
- )
+ _handle_corrupt_save("Player save failed structural validation.")
return false
_is_restoring = true
@@ -320,20 +183,16 @@ func load_player_data() -> bool:
snapshot.next_catch_sequence
)
var collection_restored: bool = (
- _collection_log.replace_collection_state(
- snapshot.discovered_ids,
- snapshot.discovered_quality_masks,
- snapshot.catch_counts,
- )
+ _collection_log.replace_discovered_ids(snapshot.discovered_ids)
)
var wallet_restored: bool = _wallet.restore_balance(
snapshot.wallet_balance
)
- var bag_restored: bool = (
- _bag.replace_all_items(snapshot.bag_items)
- and _bag.replace_unlocked_bait_ids(snapshot.unlocked_bait_ids)
+ var bag_restored: bool = _bag.replace_all_items(snapshot.bag_items)
+ var hotbar_restored: bool = _hotbar.replace_state(
+ snapshot.hotbar_slots,
+ snapshot.selected_hotbar_slot
)
- var tackle_restored: bool = _restore_tackle_selection(snapshot)
var upgrades_restored: bool = _fishing_upgrades.restore_levels(
snapshot.reel_speed_level,
snapshot.barrier_power_level
@@ -341,84 +200,20 @@ func load_player_data() -> bool:
var cooler_restored: bool = _cooler_capacity.restore_level(
snapshot.cooler_capacity_level
)
- var layout_restored: bool = _inventory_layout.restore_from_save_data(
- snapshot.inventory_layout_data
- )
- var hotbar_restored: bool = _hotbar.replace_state(
- snapshot.hotbar_slots,
- snapshot.selected_hotbar_slot,
- snapshot.fish_hotbar_slots,
- false,
- )
- var art_restored: bool = _art_unlocks.restore_mask(
- snapshot.art_unlock_mask
- )
- var experience_restored: bool = _experience.restore_total_experience(
- snapshot.total_experience
- )
- var world_time_restored: bool = (
- _world_time.restore_persistent_time_hours(snapshot.world_time_hours)
- )
- _world_layout = snapshot.world_layout
- _world_seed = snapshot.world_seed
- var world_weather_restored: bool = true
- if snapshot.has_world_weather_state:
- world_weather_restored = _world_weather.restore_persistent_state(
- snapshot.world_weather,
- snapshot.world_weather_seconds_remaining,
- )
- var jobs_restored: bool = _jobs.restore_from_save_data(snapshot.jobs_data)
- var home_restored: bool = _home_state.restore_from_save_data(
- snapshot.home_data
- )
_is_restoring = false
if (
not inventory_restored
or not collection_restored
or not wallet_restored
or not bag_restored
- or not tackle_restored
or not hotbar_restored
or not upgrades_restored
or not cooler_restored
- or not layout_restored
- or not art_restored
- or not experience_restored
- or not world_time_restored
- or not world_weather_restored
- or not jobs_restored
- or not home_restored
):
- push_error(
- (
- "Validated player save could not be restored: "
- + "inventory=%s collection=%s wallet=%s bag=%s tackle=%s hotbar=%s "
- + "upgrades=%s cooler=%s layout=%s art=%s experience=%s "
- + "time=%s weather=%s jobs=%s home=%s"
- )
- % [
- inventory_restored,
- collection_restored,
- wallet_restored,
- bag_restored,
- tackle_restored,
- hotbar_restored,
- upgrades_restored,
- cooler_restored,
- layout_restored,
- art_restored,
- experience_restored,
- world_time_restored,
- world_weather_restored,
- jobs_restored,
- home_restored,
- ]
- )
+ push_error("Validated player save could not be restored.")
return false
_is_dirty = false
- if read_path == _legacy_save_path():
- _migrate_legacy_plaintext_save(save_data, read_path)
print(
"Loaded player save version %d with %d catches."
% [SAVE_VERSION, snapshot.catches.size()]
@@ -427,36 +222,26 @@ func load_player_data() -> bool:
func inspect_save() -> SaveInspectionType:
- _recover_interrupted_write()
- var read_path: String = _read_path()
- return inspect_progression_at_path(
- read_path,
- read_path == _legacy_save_path(),
- )
-
-
-func inspect_progression_at_path(
- path: String,
- allow_legacy_plaintext: bool = false,
-) -> SaveInspectionType:
var result := SaveInspectionType.new()
- if not path.is_empty():
- _remove_if_present(path + ".codec.tmp")
- _recover_path_write(path)
- result.has_primary_file = not path.is_empty() and FileAccess.file_exists(path)
+ _recover_interrupted_write()
+ result.has_primary_file = FileAccess.file_exists(SAVE_PATH)
if not result.has_primary_file:
result.status = SaveInspectionType.Status.MISSING
result.message = "no save found."
return result
- var decoded: Dictionary = ProgressionSaveCodec.read_local_save(
- path,
- allow_legacy_plaintext,
- )
- if not bool(decoded.get("ok", false)):
+ var save_file := FileAccess.open(SAVE_PATH, FileAccess.READ)
+ if save_file == null:
result.status = SaveInspectionType.Status.IO_ERROR
result.message = "the save could not be read."
return result
- var save_data: Dictionary = decoded["data"]
+ var json := JSON.new()
+ var parse_error: Error = json.parse(save_file.get_as_text())
+ save_file.close()
+ if parse_error != OK or typeof(json.data) != TYPE_DICTIONARY:
+ result.status = SaveInspectionType.Status.MALFORMED
+ result.message = "the save is corrupt and was preserved."
+ return result
+ var save_data: Dictionary = json.data
result.detected_version = _read_integer(save_data.get("save_version"), -1)
if result.detected_version > SAVE_VERSION:
result.status = SaveInspectionType.Status.UNSUPPORTED_VERSION
@@ -481,244 +266,28 @@ func inspect_progression_at_path(
result.catch_count = snapshot.catches.size()
result.wallet_balance = snapshot.wallet_balance
result.discovered_species_count = snapshot.discovered_ids.size()
- result.total_experience = snapshot.total_experience
- result.player_level = PlayerExperienceType.level_for_total_experience(
- snapshot.total_experience
- )
- result.world_layout = snapshot.world_layout
- result.world_seed = snapshot.world_seed
result.message = "save ready."
return result
-func export_progression_archive(path: String) -> Dictionary:
- if not _is_configured or path.is_empty():
- return {"ok": false, "message": "progression export is unavailable."}
- if _is_dirty and not save_if_dirty():
- return {"ok": false, "message": "progression could not be saved first."}
- var source_path: String = _read_path()
- if source_path.is_empty():
- return {"ok": false, "message": "there is no progression to export."}
- return export_progression_archive_from_path(
- source_path,
- path,
- source_path == _legacy_save_path(),
- )
-
-
-func export_progression_archive_from_path(
- source_path: String,
- destination_path: String,
- allow_legacy_plaintext: bool = false,
-) -> Dictionary:
- if source_path.is_empty() or destination_path.is_empty():
- return {"ok": false, "message": "progression export is unavailable."}
- var decoded: Dictionary = ProgressionSaveCodec.read_local_save(
- source_path,
- allow_legacy_plaintext,
- )
- if not bool(decoded.get("ok", false)):
- return {"ok": false, "message": "the selected progression could not be read."}
- var prepared: Dictionary = _prepare_external_save_data(decoded["data"])
- if not bool(prepared.get("ok", false)):
- return prepared
- var bytes: PackedByteArray = ProgressionSaveCodec.encode_archive(
- prepared["data"],
- destination_path + ".codec.tmp",
- )
- if bytes.is_empty():
- return {"ok": false, "message": "the progression archive could not be encoded."}
- var result: Dictionary = PortableFileGuard.write_guarded(
- destination_path,
- bytes,
- PortableFileGuard.hash_file(destination_path),
- _data_root.conflict_directory(),
- _data_root.device_id,
- )
- if not bool(result.get("ok", false)):
- return {"ok": false, "message": "the progression archive could not be written."}
- var verified: Dictionary = inspect_progression_archive(destination_path)
- if not bool(verified.get("ok", false)):
- return {"ok": false, "message": "the progression archive could not be verified."}
- verified["message"] = "progression archive created."
- return verified
-
-
-func inspect_progression_archive(path: String) -> Dictionary:
- var decoded: Dictionary = ProgressionSaveCodec.read_archive(path)
- if not bool(decoded.get("ok", false)):
- return {
- "ok": false,
- "message": str(decoded.get("message", "could not open progression archive.")),
- }
- var prepared: Dictionary = _prepare_external_save_data(decoded["data"])
- if not bool(prepared.get("ok", false)):
- return prepared
- prepared["game_version"] = str(decoded.get("game_version", ""))
- prepared["created_at_unix"] = int(decoded.get("created_at_unix", 0))
- return prepared
-
-
-func import_progression_archive(path: String) -> Dictionary:
+func initialize_new_game() -> bool:
if not _is_configured:
- return {"ok": false, "message": "progression import is unavailable."}
- var inspected: Dictionary = inspect_progression_archive(path)
- if not bool(inspected.get("ok", false)):
- return inspected
- var current_path: String = _read_path()
- if not current_path.is_empty() and not _archive_replaced_save(current_path):
- return {"ok": false, "message": "the current progression could not be backed up."}
- var expected_hash: String = PortableFileGuard.hash_file(_save_path)
- var result: Dictionary = _write_current_save_data(
- inspected["data"], expected_hash
- )
- if not bool(result.get("ok", false)):
- return {"ok": false, "message": "the imported progression could not be installed."}
- _expected_hash = str(result.get("hash", ""))
- _remove_legacy_save_files()
- _is_dirty = false
- _automatic_saving_blocked = false
- return {
- "ok": true,
- "message": "progression imported.",
- "catch_count": inspected["catch_count"],
- "wallet_balance": inspected["wallet_balance"],
- "discovered_species_count": inspected["discovered_species_count"],
- }
-
-
-func install_progression_archive_at_path(
- archive_path: String,
- destination_path: String,
-) -> Dictionary:
- if not _is_configured or destination_path.is_empty():
- return {"ok": false, "message": "progression import is unavailable."}
- var inspected: Dictionary = inspect_progression_archive(archive_path)
- if not bool(inspected.get("ok", false)):
- return inspected
- var result: Dictionary = _write_save_data_at_path(
- inspected["data"],
- destination_path,
- PortableFileGuard.hash_file(destination_path),
- )
- if not bool(result.get("ok", false)):
- return {"ok": false, "message": "the imported progression could not be installed."}
- return {
- "ok": true,
- "message": "progression imported as a new save slot.",
- "catch_count": inspected["catch_count"],
- "wallet_balance": inspected["wallet_balance"],
- "discovered_species_count": inspected["discovered_species_count"],
- "world_layout": inspected["world_layout"],
- "world_seed": inspected["world_seed"],
- }
-
-
-func copy_progression_to_path(
- source_path: String,
- destination_path: String,
- allow_legacy_plaintext: bool,
- world_layout: StringName,
- world_seed: int,
-) -> Dictionary:
- if (
- not _is_configured
- or source_path.is_empty()
- or destination_path.is_empty()
- or not WorldLayoutType.is_valid(world_layout)
- or world_seed <= 0
- or world_seed > MAX_WORLD_SEED
- ):
- return {"ok": false, "message": "progression duplication is unavailable."}
- var decoded: Dictionary = ProgressionSaveCodec.read_local_save(
- source_path,
- allow_legacy_plaintext,
- )
- if not bool(decoded.get("ok", false)):
- return {"ok": false, "message": "the selected progression could not be read."}
- var prepared: Dictionary = _prepare_external_save_data(decoded["data"])
- if not bool(prepared.get("ok", false)):
- return prepared
- var save_data: Dictionary = (prepared["data"] as Dictionary).duplicate(true)
- var world_data: Dictionary = {}
- if typeof(save_data.get("world")) == TYPE_DICTIONARY:
- world_data = (save_data["world"] as Dictionary).duplicate(true)
- world_data["layout"] = String(world_layout)
- world_data["seed"] = world_seed
- save_data["world"] = world_data
- prepared = _prepare_external_save_data(save_data)
- if not bool(prepared.get("ok", false)):
- return prepared
- var result: Dictionary = _write_save_data_at_path(
- prepared["data"],
- destination_path,
- PortableFileGuard.hash_file(destination_path),
- )
- if not bool(result.get("ok", false)):
- return {"ok": false, "message": "the duplicated progression could not be written."}
- return {
- "ok": true,
- "message": "save slot duplicated.",
- "world_layout": String(world_layout),
- "world_seed": world_seed,
- }
-
-
-func initialize_new_game(
- world_seed: int = DEFAULT_WORLD_SEED,
- world_layout: StringName = WorldLayoutType.GENERATED,
-) -> bool:
- if not _is_configured:
- return false
- if (
- world_seed <= 0
- or world_seed > MAX_WORLD_SEED
- or not WorldLayoutType.is_valid(world_layout)
- ):
return false
_automatic_saving_blocked = false
_restore_defaults()
- _world_layout = world_layout
- _world_seed = world_seed
- _is_dirty = true
return true
-func get_world_layout() -> StringName:
- return _world_layout
-
-
-func get_world_seed() -> int:
- return _world_seed
-
-
-static func roll_world_seed() -> int:
- var random := RandomNumberGenerator.new()
- random.randomize()
- return random.randi_range(1, MAX_WORLD_SEED)
-
-
func delete_progression_save() -> bool:
- for primary_path: String in [_save_path, _legacy_save_path()]:
- if (
- FileAccess.file_exists(primary_path)
- and not _remove_if_present(primary_path)
- ):
- return false
- for path: String in [
- _temp_path(),
- _backup_path(),
- _codec_scratch_path(),
- _legacy_save_path() + ".tmp",
- _legacy_save_path() + ".backup",
- ]:
+ if FileAccess.file_exists(SAVE_PATH) and not _remove_if_present(SAVE_PATH):
+ return false
+ for path: String in [TEMP_PATH, BACKUP_PATH]:
if FileAccess.file_exists(path) and not _remove_if_present(path):
push_warning("Unable to remove stale player-save auxiliary file.")
if _autosave_timer != null:
_autosave_timer.stop()
_is_dirty = false
_automatic_saving_blocked = false
- _expected_hash = ""
return true
@@ -732,17 +301,6 @@ func save_if_dirty() -> bool:
return not _is_dirty or save_now()
-func save_world_time_checkpoint() -> bool:
- if (
- not _is_configured
- or _automatic_saving_blocked
- or not _autosave_enabled
- ):
- return false
- _is_dirty = true
- return save_now()
-
-
func is_dirty() -> bool:
return _is_dirty
@@ -763,18 +321,35 @@ func save_now() -> bool:
var save_data: Dictionary = _build_save_dictionary()
if save_data.is_empty():
return false
- var result: Dictionary = _write_current_save_data(
- save_data, _expected_hash
- )
- if bool(result.get("conflict", false)):
- _data_root.report_conflict(
- str(result.get("message", "")),
- str(result.get("conflict_path", "")),
- )
- if not bool(result.get("ok", false)):
- push_warning("Unable to safely write the player save.")
+ var json_text: String = JSON.stringify(save_data, "\t")
+ if json_text.is_empty():
return false
- _expected_hash = str(result["hash"])
+
+ var temp_file := FileAccess.open(TEMP_PATH, FileAccess.WRITE)
+ if temp_file == null:
+ push_warning("Unable to open temporary player save for writing.")
+ return false
+ temp_file.store_string(json_text)
+ temp_file.flush()
+ var write_error: Error = temp_file.get_error()
+ temp_file.close()
+ if write_error != OK:
+ _remove_if_present(TEMP_PATH)
+ push_warning("Unable to finish writing temporary player save.")
+ return false
+
+ var had_primary: bool = FileAccess.file_exists(SAVE_PATH)
+ _remove_if_present(BACKUP_PATH)
+ if had_primary and not _rename_file(SAVE_PATH, BACKUP_PATH):
+ _remove_if_present(TEMP_PATH)
+ push_warning("Unable to preserve the previous player save.")
+ return false
+ if not _rename_file(TEMP_PATH, SAVE_PATH):
+ if had_primary:
+ _rename_file(BACKUP_PATH, SAVE_PATH)
+ push_warning("Unable to promote the temporary player save.")
+ return false
+ _remove_if_present(BACKUP_PATH)
_is_dirty = false
return true
@@ -804,50 +379,14 @@ func _build_save_dictionary() -> Dictionary:
if fish_id.is_empty():
return {}
discovered_strings.append(String(fish_id))
- var serialized_quality_masks: Dictionary = {}
- var quality_masks: Dictionary[StringName, int] = (
- _collection_log.get_discovered_quality_masks()
- )
- for fish_id: StringName in quality_masks:
- var quality_mask: int = quality_masks[fish_id]
- if (
- fish_id.is_empty()
- or quality_mask <= 0
- or (quality_mask & ~FishQualityType.ALL_TIERS_MASK) != 0
- ):
- return {}
- serialized_quality_masks[String(fish_id)] = quality_mask
- var serialized_catch_counts: Dictionary = {}
- var catch_counts: Dictionary[StringName, int] = (
- _collection_log.get_catch_counts()
- )
- for fish_id: StringName in catch_counts:
- var catch_count: int = catch_counts[fish_id]
- if (
- fish_id.is_empty()
- or catch_count <= 0
- or catch_count > CollectionLogType.MAX_CATCH_COUNT
- or not _collection_log.has_discovered(fish_id)
- ):
- return {}
- serialized_catch_counts[String(fish_id)] = catch_count
var serialized_items: Array[Dictionary] = []
for owned: OwnedItemType in _bag.get_all_items():
if owned == null or not owned.is_valid():
return {}
serialized_items.append(owned.to_save_dict())
- var serialized_unlocked_baits: Array[String] = []
- for item_id: StringName in _bag.get_unlocked_bait_ids():
- var item_data = _item_catalog.get_item_by_id(item_id)
- if item_data == null or not item_data.is_valid() or not item_data.is_bait():
- return {}
- serialized_unlocked_baits.append(String(item_id))
var serialized_slots: Array[String] = []
for item_id: StringName in _hotbar.get_slots():
serialized_slots.append(String(item_id))
- var serialized_fish_slots: Array[String] = []
- for catch_id: StringName in _hotbar.get_fish_slots():
- serialized_fish_slots.append(String(catch_id))
if (
_wallet.get_balance() < 0
or _wallet.get_balance() > MAX_SAFE_BALANCE
@@ -858,17 +397,6 @@ func _build_save_dictionary() -> Dictionary:
)
):
return {}
- var active_bait_id: StringName = _player.active_bait_id
- var active_lure_id: StringName = _player.active_lure_id
- if (
- (not active_bait_id.is_empty() and not _valid_active_tackle(
- active_bait_id, true
- ))
- or (not active_lure_id.is_empty() and not _valid_active_tackle(
- active_lure_id, false
- ))
- ):
- return {}
return {
"save_version": SAVE_VERSION,
@@ -877,8 +405,6 @@ func _build_save_dictionary() -> Dictionary:
},
"collection": {
"discovered_fish_ids": discovered_strings,
- "discovered_quality_masks": serialized_quality_masks,
- "catch_counts": serialized_catch_counts,
},
"inventory": {
"next_catch_sequence": _inventory.get_next_catch_sequence(),
@@ -886,33 +412,13 @@ func _build_save_dictionary() -> Dictionary:
},
"bag": {
"items": serialized_items,
- "unlocked_bait_ids": serialized_unlocked_baits,
- },
- "tackle": {
- "active_bait_id": String(active_bait_id),
- "active_lure_id": String(active_lure_id),
},
"hotbar": {
"selected_slot": _hotbar.get_selected_slot(),
"slots": serialized_slots,
- "fish_slots": serialized_fish_slots,
},
- "inventory_layout": _inventory_layout.to_save_data(),
"upgrades": _fishing_upgrades.to_save_data(),
"cooler": _cooler_capacity.to_save_data(),
- "art": _art_unlocks.to_save_data(),
- "experience": _experience.to_save_data(),
- "world": {
- "layout": String(_world_layout),
- "seed": _world_seed,
- "time_hours": _world_time.get_persistent_time_hours(),
- "weather": int(_world_weather.get_persistent_weather()),
- "weather_seconds_remaining": (
- _world_weather.get_persistent_seconds_remaining()
- ),
- },
- "jobs": _jobs.to_save_data(),
- "home": _home_state.to_save_data(),
}
@@ -922,30 +428,16 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
or typeof(save_data.get("collection")) != TYPE_DICTIONARY
or typeof(save_data.get("inventory")) != TYPE_DICTIONARY
or typeof(save_data.get("bag")) != TYPE_DICTIONARY
- or typeof(save_data.get("tackle")) != TYPE_DICTIONARY
or typeof(save_data.get("hotbar")) != TYPE_DICTIONARY
- or typeof(save_data.get("inventory_layout")) != TYPE_DICTIONARY
- or typeof(save_data.get("experience")) != TYPE_DICTIONARY
- or typeof(save_data.get("jobs")) != TYPE_DICTIONARY
- or typeof(save_data.get("home")) != TYPE_DICTIONARY
):
return null
var wallet_data: Dictionary = save_data["wallet"]
var collection_data: Dictionary = save_data["collection"]
var inventory_data: Dictionary = save_data["inventory"]
var bag_data: Dictionary = save_data["bag"]
- var tackle_data: Dictionary = save_data["tackle"]
var hotbar_data: Dictionary = save_data["hotbar"]
- var inventory_layout_data: Dictionary = save_data["inventory_layout"]
- var experience_data: Dictionary = save_data["experience"]
- var jobs_data: Dictionary = save_data["jobs"]
- var home_data: Dictionary = save_data["home"]
- var world_data: Dictionary = {}
- if typeof(save_data.get("world")) == TYPE_DICTIONARY:
- world_data = save_data["world"]
var upgrades_data: Dictionary = {}
var cooler_data: Dictionary = {}
- var art_data: Dictionary = {}
if typeof(save_data.get("upgrades")) == TYPE_DICTIONARY:
upgrades_data = save_data["upgrades"]
else:
@@ -956,29 +448,14 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
cooler_data = save_data["cooler"]
else:
push_warning("Saved Cooler data was missing or invalid; using defaults.")
- if typeof(save_data.get("art")) == TYPE_DICTIONARY:
- art_data = save_data["art"]
if (
not wallet_data.has("balance")
or typeof(collection_data.get("discovered_fish_ids")) != TYPE_ARRAY
- or typeof(collection_data.get("discovered_quality_masks"))
- != TYPE_DICTIONARY
or typeof(inventory_data.get("catches")) != TYPE_ARRAY
or not inventory_data.has("next_catch_sequence")
or typeof(bag_data.get("items")) != TYPE_ARRAY
- or (
- bag_data.has("unlocked_bait_ids")
- and typeof(bag_data.get("unlocked_bait_ids")) != TYPE_ARRAY
- )
or typeof(hotbar_data.get("slots")) != TYPE_ARRAY
- or typeof(tackle_data.get("active_bait_id")) not in [
- TYPE_STRING, TYPE_STRING_NAME,
- ]
- or typeof(tackle_data.get("active_lure_id")) not in [
- TYPE_STRING, TYPE_STRING_NAME,
- ]
or not hotbar_data.has("selected_slot")
- or not experience_data.has("total_experience")
):
return null
@@ -996,69 +473,7 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
return null
var snapshot := LoadSnapshot.new()
- if world_data.has("layout"):
- if not WorldLayoutType.is_valid(world_data["layout"]):
- return null
- snapshot.world_layout = WorldLayoutType.normalized(
- world_data["layout"]
- )
- if world_data.has("seed"):
- snapshot.world_seed = _read_integer(
- world_data["seed"],
- -1,
- MAX_WORLD_SEED,
- )
- if snapshot.world_seed <= 0:
- return null
- snapshot.inventory_layout_data = inventory_layout_data.duplicate(true)
snapshot.wallet_balance = balance
- snapshot.total_experience = _read_integer(
- experience_data["total_experience"],
- -1,
- PlayerExperienceType.MAX_TOTAL_EXPERIENCE,
- )
- if snapshot.total_experience < 0:
- return null
- if not PlayerJobServiceType.validate_save_data(jobs_data):
- return null
- snapshot.jobs_data = jobs_data.duplicate(true)
- var sanitized_home: Dictionary = PlayerHomeStateType.sanitize_save_data(
- home_data
- )
- if sanitized_home.is_empty():
- return null
- snapshot.home_data = sanitized_home
- if world_data.has("time_hours"):
- snapshot.world_time_hours = _read_world_time_hours(
- world_data["time_hours"]
- )
- if snapshot.world_time_hours < 0.0:
- return null
- var has_weather: bool = world_data.has("weather")
- var has_weather_seconds: bool = world_data.has(
- "weather_seconds_remaining"
- )
- if has_weather != has_weather_seconds:
- return null
- if has_weather:
- var weather_value: int = _read_integer(
- world_data["weather"],
- -1,
- WorldWeatherServiceType.Weather.FOGGY,
- )
- var weather_seconds: float = _read_world_weather_seconds(
- world_data["weather_seconds_remaining"]
- )
- if (
- not WorldWeatherServiceType.is_valid_weather(weather_value)
- or weather_seconds < 0.0
- ):
- return null
- snapshot.has_world_weather_state = true
- snapshot.world_weather = (
- weather_value as WorldWeatherServiceType.Weather
- )
- snapshot.world_weather_seconds_remaining = weather_seconds
var discovered_values: Array = collection_data["discovered_fish_ids"]
var seen_discoveries: Dictionary[StringName, bool] = {}
for value: Variant in discovered_values:
@@ -1069,50 +484,6 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
continue
seen_discoveries[fish_id] = true
snapshot.discovered_ids.append(fish_id)
- var quality_mask_values: Dictionary = (
- collection_data["discovered_quality_masks"]
- )
- for key: Variant in quality_mask_values:
- if typeof(key) not in [TYPE_STRING, TYPE_STRING_NAME]:
- return null
- var fish_id: StringName = StringName(str(key))
- var mask: int = _read_integer(
- quality_mask_values[key],
- -1,
- FishQualityType.ALL_TIERS_MASK,
- )
- if (
- fish_id.is_empty()
- or not seen_discoveries.has(fish_id)
- or mask <= 0
- or (mask & ~FishQualityType.ALL_TIERS_MASK) != 0
- ):
- return null
- snapshot.discovered_quality_masks[fish_id] = mask
- var has_saved_catch_counts: bool = collection_data.has("catch_counts")
- if (
- has_saved_catch_counts
- and typeof(collection_data.get("catch_counts")) != TYPE_DICTIONARY
- ):
- return null
- if has_saved_catch_counts:
- var saved_catch_counts: Dictionary = collection_data["catch_counts"]
- for key: Variant in saved_catch_counts:
- if typeof(key) not in [TYPE_STRING, TYPE_STRING_NAME]:
- return null
- var fish_id := StringName(str(key))
- var catch_count: int = _read_integer(
- saved_catch_counts[key],
- -1,
- CollectionLogType.MAX_CATCH_COUNT,
- )
- if (
- fish_id.is_empty()
- or not seen_discoveries.has(fish_id)
- or catch_count <= 0
- ):
- return null
- snapshot.catch_counts[fish_id] = catch_count
var seen_ids: Dictionary[StringName, bool] = {}
var seen_sequences: Dictionary[int, bool] = {}
@@ -1163,13 +534,6 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
fish_catch.catch_sequence
)
snapshot.catches.append(fish_catch)
- if (
- not has_saved_catch_counts
- and seen_discoveries.has(fish_catch.fish_id)
- ):
- snapshot.catch_counts[fish_catch.fish_id] = (
- int(snapshot.catch_counts.get(fish_catch.fish_id, 0)) + 1
- )
snapshot.next_catch_sequence = maxi(
requested_next_sequence,
@@ -1217,52 +581,9 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
var owned := OwnedItemType.new()
owned.item_id = item_id
owned.quantity = quantity
- owned.storage_slot = _read_integer(
- item_record.get("storage_slot", -1),
- -1,
- PlayerBagType.MAX_STORAGE_SLOT_INDEX,
- )
seen_items[item_id] = true
snapshot.bag_items.append(owned)
- var seen_unlocked_baits: Dictionary[StringName, bool] = {}
- if bag_data.has("unlocked_bait_ids"):
- var unlocked_bait_values: Array = bag_data["unlocked_bait_ids"]
- for value: Variant in unlocked_bait_values:
- if typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME]:
- continue
- var bait_id: StringName = StringName(str(value))
- var bait_data = _item_catalog.get_item_by_id(bait_id)
- if (
- bait_id.is_empty()
- or seen_unlocked_baits.has(bait_id)
- or bait_data == null
- or not bait_data.is_valid()
- or not bait_data.is_bait()
- ):
- continue
- seen_unlocked_baits[bait_id] = true
- snapshot.unlocked_bait_ids.append(bait_id)
- for owned: OwnedItemType in snapshot.bag_items:
- var owned_item_data = _item_catalog.get_item_by_id(owned.item_id)
- if (
- owned_item_data != null
- and owned_item_data.is_bait()
- and not seen_unlocked_baits.has(owned.item_id)
- ):
- seen_unlocked_baits[owned.item_id] = true
- snapshot.unlocked_bait_ids.append(owned.item_id)
- snapshot.active_bait_id = _validated_tackle_id(
- StringName(str(tackle_data.get("active_bait_id", ""))),
- true,
- seen_items,
- )
- snapshot.active_lure_id = _validated_tackle_id(
- StringName(str(tackle_data.get("active_lure_id", ""))),
- false,
- seen_items,
- )
-
var slot_values: Array = hotbar_data["slots"]
snapshot.hotbar_slots.resize(PlayerHotbarType.SLOT_COUNT)
snapshot.hotbar_slots.fill(StringName())
@@ -1283,22 +604,6 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
and seen_items.has(slot_item_id)
):
snapshot.hotbar_slots[slot_index] = slot_item_id
- var fish_slot_values: Array = []
- if typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY:
- fish_slot_values = hotbar_data["fish_slots"]
- snapshot.fish_hotbar_slots.resize(PlayerHotbarType.SLOT_COUNT)
- snapshot.fish_hotbar_slots.fill(StringName())
- for slot_index: int in range(
- mini(fish_slot_values.size(), PlayerHotbarType.SLOT_COUNT)
- ):
- if not snapshot.hotbar_slots[slot_index].is_empty():
- continue
- var slot_value: Variant = fish_slot_values[slot_index]
- if typeof(slot_value) not in [TYPE_STRING, TYPE_STRING_NAME]:
- continue
- var catch_id: StringName = StringName(str(slot_value))
- if not catch_id.is_empty() and seen_ids.has(catch_id):
- snapshot.fish_hotbar_slots[slot_index] = catch_id
var selected_slot: int = _read_integer(
hotbar_data["selected_slot"],
0,
@@ -1324,11 +629,6 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
PlayerCoolerCapacityType.MAX_LEVEL,
"cooler.capacity_level"
)
- snapshot.art_unlock_mask = _read_integer(
- art_data.get("unlock_mask"),
- 0,
- PlayerArtUnlocksType.ALL_UNLOCK_MASK,
- )
return snapshot
@@ -1350,20 +650,6 @@ func _migrate_save(
migrated = _migrate_version_2_to_3(migrated)
3:
migrated = _migrate_version_3_to_4(migrated)
- 4:
- migrated = _migrate_version_4_to_5(migrated)
- 5:
- migrated = _migrate_version_5_to_6(migrated)
- 6:
- migrated = _migrate_version_6_to_7(migrated)
- 7:
- migrated = _migrate_version_7_to_8(migrated)
- 8:
- migrated = _migrate_version_8_to_9(migrated)
- 9:
- migrated = _migrate_version_9_to_10(migrated)
- 10:
- migrated = _migrate_version_10_to_11(migrated)
_:
return {}
if migrated.is_empty():
@@ -1411,223 +697,6 @@ func _migrate_version_3_to_4(data: Dictionary) -> Dictionary:
return migrated
-func _migrate_version_4_to_5(data: Dictionary) -> Dictionary:
- var migrated: Dictionary = data.duplicate(true)
- if (
- typeof(migrated.get("inventory")) != TYPE_DICTIONARY
- or typeof(migrated.get("collection")) != TYPE_DICTIONARY
- ):
- return {}
- var inventory_data: Dictionary = migrated["inventory"]
- var collection_data: Dictionary = migrated["collection"]
- if (
- typeof(inventory_data.get("catches")) != TYPE_ARRAY
- or typeof(collection_data.get("discovered_fish_ids")) != TYPE_ARRAY
- ):
- return {}
- var catches: Array = inventory_data["catches"]
- var discovered_values: Array = collection_data["discovered_fish_ids"]
- var discovered_lookup: Dictionary[String, bool] = {}
- for value: Variant in discovered_values:
- if typeof(value) in [TYPE_STRING, TYPE_STRING_NAME]:
- var existing_id: String = str(value)
- if not existing_id.is_empty():
- discovered_lookup[existing_id] = true
- for index: int in catches.size():
- if typeof(catches[index]) != TYPE_DICTIONARY:
- continue
- var catch_data: Dictionary = catches[index]
- catch_data["quality"] = FishQualityType.Tier.BORING
- var catch_fish_id: String = str(catch_data.get("fish_id", ""))
- if not catch_fish_id.is_empty() and not discovered_lookup.has(
- catch_fish_id
- ):
- discovered_values.append(catch_fish_id)
- discovered_lookup[catch_fish_id] = true
- catches[index] = catch_data
- inventory_data["catches"] = catches
- migrated["inventory"] = inventory_data
- var quality_masks: Dictionary = {}
- for value: Variant in discovered_values:
- if typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME]:
- continue
- var fish_id: String = str(value)
- if not fish_id.is_empty():
- quality_masks[fish_id] = FishQualityType.bit_for(
- FishQualityType.Tier.BORING
- )
- collection_data["discovered_quality_masks"] = quality_masks
- collection_data["discovered_fish_ids"] = discovered_values
- migrated["collection"] = collection_data
- migrated["save_version"] = 5
- return migrated
-
-
-func _migrate_version_5_to_6(data: Dictionary) -> Dictionary:
- var migrated: Dictionary = data.duplicate(true)
- migrated["save_version"] = 6
- migrated["experience"] = {"total_experience": 0}
- migrated["world"] = {
- "time_hours": WorldTimeServiceType.DEFAULT_START_HOUR,
- }
- return migrated
-
-
-func _migrate_version_6_to_7(data: Dictionary) -> Dictionary:
- var migrated: Dictionary = data.duplicate(true)
- migrated["save_version"] = 7
- # Historical catch and sale totals cannot be reconstructed honestly from
- # current inventory. New cumulative counters begin at feature introduction.
- migrated["jobs"] = PlayerJobServiceType.default_save_data()
- return migrated
-
-
-func _migrate_version_7_to_8(data: Dictionary) -> Dictionary:
- var migrated: Dictionary = data.duplicate(true)
- if (
- typeof(migrated.get("bag")) != TYPE_DICTIONARY
- or typeof(migrated.get("inventory")) != TYPE_DICTIONARY
- or typeof(migrated.get("hotbar")) != TYPE_DICTIONARY
- ):
- return {}
- var bag_data: Dictionary = migrated["bag"]
- var inventory_data: Dictionary = migrated["inventory"]
- var hotbar_data: Dictionary = migrated["hotbar"]
- if (
- typeof(bag_data.get("items")) != TYPE_ARRAY
- or typeof(inventory_data.get("catches")) != TYPE_ARRAY
- or typeof(hotbar_data.get("slots")) != TYPE_ARRAY
- ):
- return {}
-
- var placements: Array[Dictionary] = []
- var assigned: Dictionary[String, bool] = {}
- var item_slots: Array = hotbar_data["slots"]
- var fish_slots: Array = []
- if typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY:
- fish_slots = hotbar_data["fish_slots"]
- for slot: int in PlayerHotbarType.SLOT_COUNT:
- var item_id := StringName(
- str(item_slots[slot]) if slot < item_slots.size() else ""
- )
- if not item_id.is_empty():
- var item = _item_catalog.get_item_by_id(item_id)
- if item != null and not item.is_bait() and not item.is_lure():
- var key := PlayerInventoryLayoutType.item_key(item_id)
- if not assigned.has(key):
- placements.append({
- "kind": PlayerInventoryLayoutType.EntryKind.ITEM,
- "identity": String(item_id),
- "container": PlayerInventoryLayoutType.InventoryContainer.HOTBAR,
- "slot": slot,
- })
- assigned[key] = true
- continue
- var catch_id := StringName(
- str(fish_slots[slot]) if slot < fish_slots.size() else ""
- )
- if catch_id.is_empty():
- continue
- var catch_key := PlayerInventoryLayoutType.catch_key(catch_id)
- if assigned.has(catch_key):
- continue
- placements.append({
- "kind": PlayerInventoryLayoutType.EntryKind.CATCH,
- "identity": String(catch_id),
- "container": PlayerInventoryLayoutType.InventoryContainer.HOTBAR,
- "slot": slot,
- })
- assigned[catch_key] = true
-
- var next_inventory_slot: int = 0
- var next_storage_slot: int = 0
- for value: Variant in bag_data["items"] as Array:
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var item_id := StringName(str((value as Dictionary).get("item_id", "")))
- var item = _item_catalog.get_item_by_id(item_id)
- if item == null or item.is_bait() or item.is_lure():
- continue
- var key := PlayerInventoryLayoutType.item_key(item_id)
- if assigned.has(key):
- continue
- var target := PlayerInventoryLayoutType.InventoryContainer.INVENTORY
- var target_slot := next_inventory_slot
- if next_inventory_slot < PlayerInventoryLayoutType.INVENTORY_CAPACITIES[0]:
- next_inventory_slot += 1
- else:
- target = PlayerInventoryLayoutType.InventoryContainer.STORAGE
- target_slot = next_storage_slot
- next_storage_slot += 1
- placements.append({
- "kind": PlayerInventoryLayoutType.EntryKind.ITEM,
- "identity": String(item_id),
- "container": target,
- "slot": target_slot,
- })
- assigned[key] = true
- for value: Variant in inventory_data["catches"] as Array:
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var catch_id := StringName(
- str((value as Dictionary).get("catch_id", ""))
- )
- if catch_id.is_empty():
- continue
- var key := PlayerInventoryLayoutType.catch_key(catch_id)
- if assigned.has(key):
- continue
- var target := PlayerInventoryLayoutType.InventoryContainer.INVENTORY
- var target_slot := next_inventory_slot
- if next_inventory_slot < PlayerInventoryLayoutType.INVENTORY_CAPACITIES[0]:
- next_inventory_slot += 1
- else:
- target = PlayerInventoryLayoutType.InventoryContainer.STORAGE
- target_slot = next_storage_slot
- next_storage_slot += 1
- placements.append({
- "kind": PlayerInventoryLayoutType.EntryKind.CATCH,
- "identity": String(catch_id),
- "container": target,
- "slot": target_slot,
- })
- assigned[key] = true
- migrated["inventory_layout"] = {
- "backpack_level": 0,
- "placements": placements,
- }
- migrated["save_version"] = 8
- return migrated
-
-
-func _migrate_version_8_to_9(data: Dictionary) -> Dictionary:
- var migrated: Dictionary = data.duplicate(true)
- var world_data: Dictionary = {}
- if typeof(migrated.get("world")) == TYPE_DICTIONARY:
- world_data = (migrated["world"] as Dictionary).duplicate(true)
- world_data["seed"] = DEFAULT_WORLD_SEED
- migrated["world"] = world_data
- migrated["save_version"] = 9
- return migrated
-
-
-func _migrate_version_9_to_10(data: Dictionary) -> Dictionary:
- var migrated: Dictionary = data.duplicate(true)
- migrated["tackle"] = {
- "active_bait_id": "",
- "active_lure_id": "",
- }
- migrated["save_version"] = 10
- return migrated
-
-
-func _migrate_version_10_to_11(data: Dictionary) -> Dictionary:
- var migrated: Dictionary = data.duplicate(true)
- migrated["home"] = PlayerHomeStateType.default_save_data()
- migrated["save_version"] = 11
- return migrated
-
-
func _mark_dirty() -> void:
if (
_is_restoring
@@ -1640,7 +709,7 @@ func _mark_dirty() -> void:
_autosave_timer.start(maxf(autosave_delay, 0.05))
-func _on_collection_changed() -> void:
+func _on_fish_discovered(_fish_id: StringName) -> void:
_mark_dirty()
@@ -1669,185 +738,33 @@ func _on_cooler_capacity_changed(
_mark_dirty()
-func _on_art_unlocks_changed(_unlock_mask: int) -> void:
- _mark_dirty()
-
-
-func _on_experience_changed(_total_experience: int, _level: int) -> void:
- _mark_dirty()
-
-
-func _on_active_tackle_changed(_item_id: StringName) -> void:
- _mark_dirty()
-
-
func _on_autosave_timeout() -> void:
if _is_dirty:
save_now()
func _recover_interrupted_write() -> void:
- _remove_if_present(_codec_scratch_path())
- _recover_path_write(_save_path)
- _recover_path_write(_legacy_save_path())
-
-
-func _recover_path_write(path: String) -> void:
- var temporary: String = path + ".tmp"
- var backup: String = path + ".backup"
- if FileAccess.file_exists(path):
- _remove_if_present(temporary)
- _remove_if_present(backup)
+ if FileAccess.file_exists(SAVE_PATH):
+ _remove_if_present(TEMP_PATH)
+ _remove_if_present(BACKUP_PATH)
return
- if FileAccess.file_exists(backup):
- _rename_file(backup, path)
- _remove_if_present(temporary)
+ if FileAccess.file_exists(BACKUP_PATH):
+ _rename_file(BACKUP_PATH, SAVE_PATH)
+ _remove_if_present(TEMP_PATH)
-func _write_current_save_data(
- save_data: Dictionary,
- expected_hash: String,
-) -> Dictionary:
- return _write_save_data_at_path(save_data, _save_path, expected_hash)
-
-
-func _write_save_data_at_path(
- save_data: Dictionary,
- destination_path: String,
- expected_hash: String,
-) -> Dictionary:
- var bytes: PackedByteArray = ProgressionSaveCodec.encode_local_save(
- save_data,
- destination_path + ".codec.tmp",
- )
- if bytes.is_empty():
- return {"ok": false}
- return PortableFileGuard.write_guarded(
- destination_path,
- bytes,
- expected_hash,
- _data_root.conflict_directory(),
- _data_root.device_id,
- )
-
-
-func _prepare_external_save_data(value: Variant) -> Dictionary:
- if typeof(value) != TYPE_DICTIONARY:
- return {"ok": false, "message": "progression data is malformed."}
- var save_data: Dictionary = (value as Dictionary).duplicate(true)
- var version: int = _read_integer(save_data.get("save_version"), -1)
- if version > SAVE_VERSION:
- return {
- "ok": false,
- "message": "this progression belongs to a newer game version.",
- }
- if version < 1:
- return {"ok": false, "message": "the progression version is unsupported."}
- if version != SAVE_VERSION:
- save_data = _migrate_save(save_data, version)
- if save_data.is_empty():
- return {
- "ok": false,
- "message": "the progression version is unsupported.",
- }
- var snapshot: LoadSnapshot = _build_load_snapshot(save_data)
- if snapshot == null:
- return {"ok": false, "message": "the progression is structurally invalid."}
- return {
- "ok": true,
- "data": save_data,
- "catch_count": snapshot.catches.size(),
- "wallet_balance": snapshot.wallet_balance,
- "discovered_species_count": snapshot.discovered_ids.size(),
- "world_layout": String(snapshot.world_layout),
- "world_seed": snapshot.world_seed,
- }
-
-
-func _migrate_legacy_plaintext_save(
- save_data: Dictionary,
- legacy_path: String,
-) -> void:
- var result: Dictionary = _write_current_save_data(save_data, "")
- if not bool(result.get("ok", false)):
- push_warning("Legacy progression loaded but could not be encrypted yet.")
- return
- _expected_hash = str(result.get("hash", ""))
- if not _archive_replaced_save(legacy_path, "plaintext-migration"):
- push_warning(
- "Legacy progression was encrypted, but its migration backup failed."
- )
- return
- _remove_legacy_save_files()
- print("Upgraded legacy plaintext progression to the opaque save format.")
-
-
-func _archive_replaced_save(
- source_path: String,
- reason: String = "before-import",
-) -> bool:
- if source_path.is_empty() or not FileAccess.file_exists(source_path):
- return true
- var timestamp: String = Time.get_datetime_string_from_system().replace(
- ":", "-"
- )
- var destination: String = _data_root.root_path.path_join(
- "backups/saves/player-save-%s-%s%s"
- % [reason, timestamp, ARCHIVE_EXTENSION]
- )
- if FileAccess.file_exists(destination):
- destination = destination.trim_suffix(ARCHIVE_EXTENSION) + (
- "-%s%s" % [Time.get_ticks_usec(), ARCHIVE_EXTENSION]
- )
- var decoded: Dictionary = ProgressionSaveCodec.read_local_save(
- source_path,
- source_path == _legacy_save_path(),
- )
- var bytes: PackedByteArray
- if bool(decoded.get("ok", false)):
- var prepared: Dictionary = _prepare_external_save_data(decoded["data"])
- if bool(prepared.get("ok", false)):
- bytes = ProgressionSaveCodec.encode_archive(
- prepared["data"], destination + ".codec.tmp"
- )
- if bytes.is_empty():
- bytes = PortableFileGuard.read_bytes(source_path)
- destination = (
- destination.trim_suffix(ARCHIVE_EXTENSION) + ".preserved"
- )
- if bytes.is_empty():
- return false
- var result: Dictionary = PortableFileGuard.write_guarded(
- destination,
- bytes,
- "",
- _data_root.conflict_directory(),
- _data_root.device_id,
- )
- return bool(result.get("ok", false))
-
-
-func _remove_legacy_save_files() -> void:
- for path: String in [
- _legacy_save_path(),
- _legacy_save_path() + ".tmp",
- _legacy_save_path() + ".backup",
- ]:
- _remove_if_present(path)
-
-
-func _handle_corrupt_save(message: String, source_path: String) -> void:
+func _handle_corrupt_save(message: String) -> void:
push_warning(message)
_restore_defaults()
- if not FileAccess.file_exists(source_path):
+ if not FileAccess.file_exists(SAVE_PATH):
return
var timestamp: int = int(Time.get_unix_time_from_system())
var corrupt_path: String = (
- "%s.corrupt-%d" % [source_path, timestamp]
+ "%s.corrupt-%d" % [SAVE_PATH, timestamp]
)
if FileAccess.file_exists(corrupt_path):
corrupt_path += "-%d" % Time.get_ticks_usec()
- if not _rename_file(source_path, corrupt_path):
+ if not _rename_file(SAVE_PATH, corrupt_path):
_automatic_saving_blocked = true
push_warning(
"Corrupt player save was left in place; automatic saving is "
@@ -1861,8 +778,6 @@ func _restore_defaults() -> void:
_is_restoring = true
var empty_catches: Array[FishCatchType] = []
var empty_discoveries: Array[StringName] = []
- var empty_quality_masks: Dictionary[StringName, int] = {}
- var empty_catch_counts: Dictionary[StringName, int] = {}
var default_items: Array[OwnedItemType] = []
var basic_rod := OwnedItemType.new()
basic_rod.item_id = BASIC_ROD_ID
@@ -1873,82 +788,16 @@ func _restore_defaults() -> void:
default_slots.fill(StringName())
default_slots[0] = BASIC_ROD_ID
_inventory.replace_all_catches(empty_catches, 1)
- _collection_log.replace_collection_state(
- empty_discoveries,
- empty_quality_masks,
- empty_catch_counts,
- )
+ _collection_log.replace_discovered_ids(empty_discoveries)
_wallet.restore_balance(0)
_bag.replace_all_items(default_items)
- var default_unlocked_baits: Array[StringName] = []
- _bag.replace_unlocked_bait_ids(default_unlocked_baits)
- _player.unequip_bait()
- _player.unequip_lure()
+ _hotbar.replace_state(default_slots, 0)
_fishing_upgrades.reset_to_defaults()
_cooler_capacity.reset_to_defaults()
- _inventory_layout.reset_to_defaults()
- _hotbar.replace_state(default_slots, 0)
- _art_unlocks.reset_to_defaults()
- _experience.reset_to_defaults()
- _world_time.restore_persistent_time_hours(
- WorldTimeServiceType.DEFAULT_START_HOUR
- )
- _world_weather.reset_persistent_state()
- _world_layout = WorldLayoutType.GENERATED
- _world_seed = DEFAULT_WORLD_SEED
- _jobs.reset_to_defaults()
- _home_state.reset_to_defaults()
_is_restoring = false
_is_dirty = false
-func _valid_active_tackle(item_id: StringName, bait: bool) -> bool:
- var item = _item_catalog.get_item_by_id(item_id)
- return (
- item != null
- and item.is_available()
- and (item.is_bait() if bait else item.is_lure())
- and _bag.owns_item(item_id)
- )
-
-
-func _validated_tackle_id(
- item_id: StringName,
- bait: bool,
- seen_items: Dictionary[StringName, bool],
-) -> StringName:
- if item_id.is_empty():
- return StringName()
- var item = _item_catalog.get_item_by_id(item_id)
- if (
- item == null
- or not item.is_available()
- or not seen_items.has(item_id)
- or not (item.is_bait() if bait else item.is_lure())
- ):
- push_warning("Skipped invalid saved active tackle '%s'." % String(item_id))
- return StringName()
- return item_id
-
-
-func _restore_tackle_selection(snapshot: LoadSnapshot) -> bool:
- _player.unequip_bait()
- _player.unequip_lure()
- var bait_restored: bool = (
- snapshot.active_bait_id.is_empty()
- or _player.equip_bait(
- _item_catalog.get_item_by_id(snapshot.active_bait_id)
- )
- )
- var lure_restored: bool = (
- snapshot.active_lure_id.is_empty()
- or _player.equip_lure(
- _item_catalog.get_item_by_id(snapshot.active_lure_id)
- )
- )
- return bait_restored and lure_restored
-
-
func _read_integer(
value: Variant,
invalid_value: int,
@@ -1970,32 +819,6 @@ func _read_integer(
return invalid_value
-func _read_world_time_hours(value: Variant) -> float:
- if typeof(value) not in [TYPE_FLOAT, TYPE_INT]:
- return -1.0
- var time_hours: float = float(value)
- if (
- not is_finite(time_hours)
- or time_hours < 0.0
- or time_hours >= WorldTimeServiceType.HOURS_PER_DAY
- ):
- return -1.0
- return time_hours
-
-
-func _read_world_weather_seconds(value: Variant) -> float:
- if typeof(value) not in [TYPE_FLOAT, TYPE_INT]:
- return -1.0
- var seconds_remaining: float = float(value)
- if (
- not is_finite(seconds_remaining)
- or seconds_remaining < 0.0
- or seconds_remaining > WorldWeatherServiceType.MAX_PERSISTED_SECONDS
- ):
- return -1.0
- return seconds_remaining
-
-
func _read_upgrade_level(
value: Variant,
maximum_level: int,
diff --git a/save/player_save_slot_catalog.gd b/save/player_save_slot_catalog.gd
deleted file mode 100644
index d3b9d23..0000000
--- a/save/player_save_slot_catalog.gd
+++ /dev/null
@@ -1,591 +0,0 @@
-class_name PlayerSaveSlotCatalog
-extends RefCounted
-
-const FORMAT_VERSION: int = 1
-const MAX_SLOTS: int = 32
-const MAX_NAME_LENGTH: int = 32
-const LEGACY_SAVE_ID: StringName = &"player_save"
-
-signal slots_changed
-
-var _data_root: PlayerDataRoot
-var _save_manager: PlayerSaveManager
-var _manifest_path := ""
-var _slots_directory := ""
-var _slots: Array[Dictionary] = []
-var _active_slot_id := ""
-var _expected_hash := ""
-var _error_message := ""
-
-
-func configure(
- data_root: PlayerDataRoot,
- save_manager: PlayerSaveManager,
-) -> bool:
- if data_root == null or save_manager == null or data_root.root_path.is_empty():
- return false
- _data_root = data_root
- _save_manager = save_manager
- _manifest_path = data_root.path_for(&"save_slots")
- _slots_directory = data_root.root_path.path_join("player/saves")
- _slots.clear()
- _active_slot_id = ""
- _expected_hash = ""
- _error_message = ""
- if DirAccess.make_dir_recursive_absolute(_slots_directory) != OK:
- _error_message = "the save-slot directory could not be created."
- return false
- _recover_interrupted_manifest_write()
- var loaded: bool = _load_manifest()
- if not loaded and FileAccess.file_exists(_manifest_path):
- if not _preserve_invalid_manifest():
- _error_message = "the invalid save-slot catalog could not be preserved."
- return false
- var slot_count_before_discovery: int = _slots.size()
- var active_before_discovery: String = _active_slot_id
- _discover_untracked_saves()
- if _active_slot_id.is_empty() and not _slots.is_empty():
- _active_slot_id = str(_slots.front().get("slot_id", ""))
- if (
- not loaded
- or _slots.size() != slot_count_before_discovery
- or _active_slot_id != active_before_discovery
- ):
- if not _save_manifest():
- return false
- return _select_configured_storage()
-
-
-func get_error_message() -> String:
- return _error_message
-
-
-func list_slots() -> Array[Dictionary]:
- var result: Array[Dictionary] = []
- for entry: Dictionary in _slots:
- result.append(_build_slot_summary(entry))
- result.sort_custom(
- func(a: Dictionary, b: Dictionary) -> bool:
- var a_played: int = int(a.get("last_played_at_unix", 0))
- var b_played: int = int(b.get("last_played_at_unix", 0))
- if a_played == b_played:
- return int(a.get("created_at_unix", 0)) > int(
- b.get("created_at_unix", 0)
- )
- return a_played > b_played
- )
- return result
-
-
-func get_slot(slot_id: String) -> Dictionary:
- var entry: Dictionary = _find_slot(slot_id)
- return _build_slot_summary(entry) if not entry.is_empty() else {}
-
-
-func get_active_slot_id() -> String:
- return _active_slot_id
-
-
-func has_slots() -> bool:
- return not _slots.is_empty()
-
-
-func ensure_active_slot() -> Dictionary:
- if not _active_slot_id.is_empty() and not _find_slot(_active_slot_id).is_empty():
- return {"ok": true, "slot_id": _active_slot_id}
- var created: Dictionary = create_empty_slot(_next_default_name())
- if not bool(created.get("ok", false)):
- return created
- var slot_id: String = str(created.get("slot_id", ""))
- if not activate_slot(slot_id):
- return {"ok": false, "message": "the new save slot could not be selected."}
- return {"ok": true, "slot_id": slot_id}
-
-
-func create_empty_slot(display_name: String) -> Dictionary:
- if _slots.size() >= MAX_SLOTS:
- return {"ok": false, "message": "the maximum number of save slots has been reached."}
- var clean_name: String = normalized_name(display_name)
- if clean_name.is_empty():
- return {"ok": false, "message": "enter a name for this save slot."}
- var slot_id: String = _generate_slot_id()
- var now: int = int(Time.get_unix_time_from_system())
- _slots.append({
- "slot_id": slot_id,
- "display_name": clean_name,
- "created_at_unix": now,
- "last_played_at_unix": 0,
- "legacy": false,
- })
- if not _save_manifest():
- _slots.pop_back()
- return {"ok": false, "message": "the save-slot catalog could not be updated."}
- slots_changed.emit()
- return {"ok": true, "slot_id": slot_id}
-
-
-func duplicate_slot(
- source_slot_id: String,
- display_name: String,
- world_layout: StringName,
- world_seed: int,
-) -> Dictionary:
- if _slots.size() >= MAX_SLOTS:
- return {"ok": false, "message": "the maximum number of save slots has been reached."}
- var source: Dictionary = _find_slot(source_slot_id)
- var clean_name: String = normalized_name(display_name)
- if source.is_empty() or clean_name.is_empty():
- return {"ok": false, "message": "the selected save slot cannot be duplicated."}
- var source_path: String = _existing_slot_path(source)
- if source_path.is_empty():
- return {"ok": false, "message": "the selected save slot has no progression to duplicate."}
- var slot_id: String = _generate_slot_id()
- var destination: String = _slots_directory.path_join(slot_id + ".nfsave")
- var copied: Dictionary = _save_manager.copy_progression_to_path(
- source_path,
- destination,
- source_path.ends_with(".json"),
- world_layout,
- world_seed,
- )
- if not bool(copied.get("ok", false)):
- return copied
- var now: int = int(Time.get_unix_time_from_system())
- _slots.append({
- "slot_id": slot_id,
- "display_name": clean_name,
- "created_at_unix": now,
- "last_played_at_unix": 0,
- "legacy": false,
- })
- if not _save_manifest():
- _slots.pop_back()
- _remove_if_present(destination)
- return {"ok": false, "message": "the duplicated slot could not be recorded."}
- slots_changed.emit()
- return {"ok": true, "slot_id": slot_id}
-
-
-func import_slot(archive_path: String, display_name: String) -> Dictionary:
- if _slots.size() >= MAX_SLOTS:
- return {"ok": false, "message": "the maximum number of save slots has been reached."}
- var clean_name: String = normalized_name(display_name)
- if clean_name.is_empty():
- return {"ok": false, "message": "the imported save needs a slot name."}
- var slot_id: String = _generate_slot_id()
- var destination: String = _slots_directory.path_join(slot_id + ".nfsave")
- var installed: Dictionary = _save_manager.install_progression_archive_at_path(
- archive_path,
- destination,
- )
- if not bool(installed.get("ok", false)):
- return installed
- var now: int = int(Time.get_unix_time_from_system())
- _slots.append({
- "slot_id": slot_id,
- "display_name": clean_name,
- "created_at_unix": now,
- "last_played_at_unix": 0,
- "legacy": false,
- })
- if not _save_manifest():
- _slots.pop_back()
- _remove_if_present(destination)
- return {"ok": false, "message": "the imported slot could not be recorded."}
- slots_changed.emit()
- installed["slot_id"] = slot_id
- return installed
-
-
-func export_slot(slot_id: String, destination_path: String) -> Dictionary:
- var entry: Dictionary = _find_slot(slot_id)
- var source_path: String = _existing_slot_path(entry)
- if entry.is_empty() or source_path.is_empty():
- return {"ok": false, "message": "the selected save slot cannot be exported."}
- return _save_manager.export_progression_archive_from_path(
- source_path,
- destination_path,
- source_path.ends_with(".json"),
- )
-
-
-func activate_slot(slot_id: String) -> bool:
- var entry: Dictionary = _find_slot(slot_id)
- if entry.is_empty():
- return false
- var previous_id: String = _active_slot_id
- var previous_path: String = _configured_storage_path(previous_id)
- var next_path: String = _slot_primary_path(entry)
- if not _save_manager.select_storage(next_path, _data_root):
- return false
- _active_slot_id = slot_id
- if _save_manifest():
- slots_changed.emit()
- return true
- _active_slot_id = previous_id
- _save_manager.select_storage(previous_path, _data_root)
- return false
-
-
-func mark_played(slot_id: String) -> bool:
- var index: int = _find_slot_index(slot_id)
- if index < 0:
- return false
- var previous_played_at: int = int(
- _slots[index].get("last_played_at_unix", 0)
- )
- var previous_active: String = _active_slot_id
- _slots[index]["last_played_at_unix"] = int(Time.get_unix_time_from_system())
- _active_slot_id = slot_id
- if not _save_manifest():
- _slots[index]["last_played_at_unix"] = previous_played_at
- _active_slot_id = previous_active
- return false
- slots_changed.emit()
- return true
-
-
-func rename_slot(slot_id: String, display_name: String) -> bool:
- var index: int = _find_slot_index(slot_id)
- var clean_name: String = normalized_name(display_name)
- if index < 0 or clean_name.is_empty():
- return false
- var previous: String = str(_slots[index].get("display_name", ""))
- _slots[index]["display_name"] = clean_name
- if not _save_manifest():
- _slots[index]["display_name"] = previous
- return false
- slots_changed.emit()
- return true
-
-
-func delete_slot(slot_id: String) -> bool:
- var index: int = _find_slot_index(slot_id)
- if index < 0:
- return false
- var previous_slots: Array[Dictionary] = []
- for previous_entry: Dictionary in _slots:
- previous_slots.append(previous_entry.duplicate(true))
- var previous_active: String = _active_slot_id
- var entry: Dictionary = _slots[index]
- var source_path: String = _existing_slot_path(entry)
- var preserved_path := ""
- if not source_path.is_empty():
- preserved_path = _deleted_backup_path(entry, source_path)
- if not _rename_file(source_path, preserved_path):
- return false
- _slots.remove_at(index)
- if _active_slot_id == slot_id:
- _active_slot_id = (
- str(_slots.front().get("slot_id", ""))
- if not _slots.is_empty()
- else ""
- )
- if not _select_configured_storage() or not _save_manifest():
- _slots = previous_slots
- _active_slot_id = previous_active
- _select_configured_storage()
- if not preserved_path.is_empty():
- _rename_file(preserved_path, source_path)
- return false
- for auxiliary: String in _slot_auxiliary_paths(entry):
- _remove_if_present(auxiliary)
- slots_changed.emit()
- return true
-
-
-static func normalized_name(value: String) -> String:
- var clean_name: String = value.strip_edges().left(MAX_NAME_LENGTH)
- if clean_name.is_empty():
- return ""
- for index: int in clean_name.length():
- var codepoint: int = clean_name.unicode_at(index)
- if codepoint < 32 or codepoint == 127:
- return ""
- return clean_name
-
-
-func _build_slot_summary(entry: Dictionary) -> Dictionary:
- if entry.is_empty():
- return {}
- var summary: Dictionary = entry.duplicate(true)
- var path: String = _existing_slot_path(entry)
- var inspection: PlayerSaveInspection = _save_manager.inspect_progression_at_path(
- path,
- path.ends_with(".json"),
- )
- summary["active"] = str(entry.get("slot_id", "")) == _active_slot_id
- summary["has_save"] = inspection.can_continue()
- summary["save_status"] = inspection.status
- summary["status_message"] = inspection.message
- summary["catch_count"] = inspection.catch_count
- summary["wallet_balance"] = inspection.wallet_balance
- summary["discovered_species_count"] = inspection.discovered_species_count
- summary["player_level"] = inspection.player_level
- summary["world_layout"] = String(inspection.world_layout)
- summary["world_seed"] = inspection.world_seed
- return summary
-
-
-func _load_manifest() -> bool:
- if not FileAccess.file_exists(_manifest_path):
- return false
- var file := FileAccess.open(_manifest_path, FileAccess.READ)
- if file == null:
- return false
- var parser := JSON.new()
- var error: Error = parser.parse(file.get_as_text())
- file.close()
- if error != OK or typeof(parser.data) != TYPE_DICTIONARY:
- return false
- var data: Dictionary = parser.data
- if int(data.get("format_version", -1)) != FORMAT_VERSION:
- return false
- if typeof(data.get("slots")) != TYPE_ARRAY:
- return false
- var loaded_slots: Array[Dictionary] = []
- var seen_ids: Dictionary[String, bool] = {}
- for value: Variant in data["slots"]:
- if typeof(value) != TYPE_DICTIONARY:
- return false
- var entry: Dictionary = (value as Dictionary).duplicate(true)
- if not _valid_slot_entry(entry):
- return false
- var slot_id: String = str(entry["slot_id"])
- if seen_ids.has(slot_id):
- return false
- seen_ids[slot_id] = true
- loaded_slots.append(entry)
- _slots = loaded_slots
- _active_slot_id = str(data.get("active_slot_id", ""))
- if not _active_slot_id.is_empty() and not seen_ids.has(_active_slot_id):
- _active_slot_id = ""
- _expected_hash = PortableFileGuard.hash_file(_manifest_path)
- return true
-
-
-func _save_manifest() -> bool:
- var data: Dictionary = {
- "format_version": FORMAT_VERSION,
- "active_slot_id": _active_slot_id,
- "slots": _slots,
- }
- var result: Dictionary = PortableFileGuard.write_guarded(
- _manifest_path,
- JSON.stringify(data, "\t").to_utf8_buffer(),
- _expected_hash,
- _data_root.conflict_directory(),
- _data_root.device_id,
- )
- if bool(result.get("conflict", false)):
- _data_root.report_conflict(
- str(result.get("message", "")),
- str(result.get("conflict_path", "")),
- )
- if bool(result.get("ok", false)):
- _expected_hash = str(result.get("hash", ""))
- return true
- _error_message = "the save-slot catalog could not be written safely."
- return false
-
-
-func _discover_untracked_saves() -> void:
- var known_ids: Dictionary[String, bool] = {}
- var has_legacy: bool = false
- for entry: Dictionary in _slots:
- known_ids[str(entry.get("slot_id", ""))] = true
- has_legacy = has_legacy or bool(entry.get("legacy", false))
- var legacy_primary: String = _data_root.path_for(LEGACY_SAVE_ID)
- var legacy_plaintext: String = legacy_primary.get_base_dir().path_join(
- PlayerSaveManager.LEGACY_SAVE_FILENAME
- )
- if (
- not has_legacy
- and (
- FileAccess.file_exists(legacy_primary)
- or FileAccess.file_exists(legacy_plaintext)
- )
- ):
- var legacy_id: String = _generate_slot_id()
- _slots.append({
- "slot_id": legacy_id,
- "display_name": _next_default_name(),
- "created_at_unix": int(Time.get_unix_time_from_system()),
- "last_played_at_unix": 0,
- "legacy": true,
- })
- known_ids[legacy_id] = true
- var directory := DirAccess.open(_slots_directory)
- if directory == null:
- return
- directory.list_dir_begin()
- var filename: String = directory.get_next()
- while not filename.is_empty():
- if not directory.current_is_dir() and filename.ends_with(".nfsave"):
- var slot_id: String = filename.trim_suffix(".nfsave")
- if _valid_slot_id(slot_id) and not known_ids.has(slot_id):
- _slots.append({
- "slot_id": slot_id,
- "display_name": _next_default_name(),
- "created_at_unix": int(Time.get_unix_time_from_system()),
- "last_played_at_unix": 0,
- "legacy": false,
- })
- known_ids[slot_id] = true
- filename = directory.get_next()
- directory.list_dir_end()
-
-
-func _select_configured_storage() -> bool:
- return _save_manager.select_storage(
- _configured_storage_path(_active_slot_id),
- _data_root,
- )
-
-
-func _configured_storage_path(slot_id: String) -> String:
- var entry: Dictionary = _find_slot(slot_id)
- if not entry.is_empty():
- return _slot_primary_path(entry)
- return _data_root.path_for(LEGACY_SAVE_ID)
-
-
-func _slot_primary_path(entry: Dictionary) -> String:
- if bool(entry.get("legacy", false)):
- return _data_root.path_for(LEGACY_SAVE_ID)
- return _slots_directory.path_join(str(entry.get("slot_id", "")) + ".nfsave")
-
-
-func _existing_slot_path(entry: Dictionary) -> String:
- if entry.is_empty():
- return ""
- var primary: String = _slot_primary_path(entry)
- if FileAccess.file_exists(primary):
- return primary
- if bool(entry.get("legacy", false)):
- var plaintext: String = primary.get_base_dir().path_join(
- PlayerSaveManager.LEGACY_SAVE_FILENAME
- )
- if FileAccess.file_exists(plaintext):
- return plaintext
- return ""
-
-
-func _slot_auxiliary_paths(entry: Dictionary) -> Array[String]:
- var primary: String = _slot_primary_path(entry)
- var paths: Array[String] = [
- primary + ".tmp",
- primary + ".backup",
- primary + ".codec.tmp",
- ]
- if bool(entry.get("legacy", false)):
- var plaintext: String = primary.get_base_dir().path_join(
- PlayerSaveManager.LEGACY_SAVE_FILENAME
- )
- paths.append_array([
- plaintext + ".tmp",
- plaintext + ".backup",
- ])
- return paths
-
-
-func _deleted_backup_path(entry: Dictionary, source_path: String) -> String:
- var timestamp: String = Time.get_datetime_string_from_system().replace(":", "-")
- var extension: String = ".json" if source_path.ends_with(".json") else ".nfsave"
- var destination: String = _data_root.root_path.path_join(
- "backups/saves/deleted-slot-%s-%s%s"
- % [str(entry.get("slot_id", "")).left(8), timestamp, extension]
- )
- if FileAccess.file_exists(destination):
- destination = destination.trim_suffix(extension) + (
- "-%d%s" % [Time.get_ticks_usec(), extension]
- )
- return destination
-
-
-func _find_slot(slot_id: String) -> Dictionary:
- var index: int = _find_slot_index(slot_id)
- return _slots[index] if index >= 0 else {}
-
-
-func _find_slot_index(slot_id: String) -> int:
- for index: int in _slots.size():
- if str(_slots[index].get("slot_id", "")) == slot_id:
- return index
- return -1
-
-
-func _next_default_name() -> String:
- var used: Dictionary[String, bool] = {}
- for entry: Dictionary in _slots:
- used[str(entry.get("display_name", "")).to_lower()] = true
- var number: int = 1
- while used.has("save %d" % number):
- number += 1
- return "save %d" % number
-
-
-func _generate_slot_id() -> String:
- var slot_id: String = Crypto.new().generate_random_bytes(16).hex_encode()
- while _find_slot_index(slot_id) >= 0:
- slot_id = Crypto.new().generate_random_bytes(16).hex_encode()
- return slot_id
-
-
-func _valid_slot_entry(entry: Dictionary) -> bool:
- var display_name: String = str(entry.get("display_name", ""))
- return (
- _valid_slot_id(str(entry.get("slot_id", "")))
- and display_name.length() <= MAX_NAME_LENGTH
- and not normalized_name(display_name).is_empty()
- and typeof(entry.get("created_at_unix")) in [TYPE_INT, TYPE_FLOAT]
- and typeof(entry.get("last_played_at_unix")) in [TYPE_INT, TYPE_FLOAT]
- and typeof(entry.get("legacy")) == TYPE_BOOL
- )
-
-
-func _valid_slot_id(slot_id: String) -> bool:
- if slot_id.length() != 32:
- return false
- for index: int in slot_id.length():
- var character: String = slot_id.substr(index, 1).to_lower()
- if character not in "0123456789abcdef":
- return false
- return true
-
-
-func _recover_interrupted_manifest_write() -> void:
- var temporary: String = _manifest_path + ".tmp"
- var backup: String = _manifest_path + ".backup"
- if FileAccess.file_exists(_manifest_path):
- _remove_if_present(temporary)
- _remove_if_present(backup)
- return
- if FileAccess.file_exists(backup):
- _rename_file(backup, _manifest_path)
- _remove_if_present(temporary)
-
-
-func _preserve_invalid_manifest() -> bool:
- var destination: String = _data_root.migration_backup_directory().path_join(
- "invalid-save-slots-%s.json"
- % Time.get_datetime_string_from_system().replace(":", "-")
- )
- if not _rename_file(_manifest_path, destination):
- return false
- _expected_hash = ""
- _slots.clear()
- _active_slot_id = ""
- return true
-
-
-func _rename_file(source: String, destination: String) -> bool:
- if source.is_empty() or destination.is_empty():
- return false
- if DirAccess.make_dir_recursive_absolute(destination.get_base_dir()) != OK:
- return false
- return DirAccess.rename_absolute(source, destination) == OK
-
-
-func _remove_if_present(path: String) -> bool:
- return not FileAccess.file_exists(path) or DirAccess.remove_absolute(path) == OK
diff --git a/save/player_save_slot_catalog.gd.uid b/save/player_save_slot_catalog.gd.uid
deleted file mode 100644
index 8906f5a..0000000
--- a/save/player_save_slot_catalog.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://djfeifv7xrqdt
diff --git a/save/progression_save_codec.gd b/save/progression_save_codec.gd
deleted file mode 100644
index 4d0ba33..0000000
--- a/save/progression_save_codec.gd
+++ /dev/null
@@ -1,191 +0,0 @@
-class_name ProgressionSaveCodec
-extends RefCounted
-
-const MAGIC := "straywild_PROGRESSION_CONTAINER"
-const LEGACY_MAGIC := "NETFISHING_PROGRESSION_CONTAINER"
-const FORMAT_VERSION := 1
-const LOCAL_KIND := "local_save"
-const ARCHIVE_KIND := "portable_archive"
-const MAX_CONTAINER_BYTES := 16 * 1024 * 1024
-
-# This key keeps routine progression files opaque to casual editing. Since the
-# client is open source, it is intentionally not treated as a security secret.
-# Its original value is a stable file-format detail so released saves remain
-# readable after the Straywild rebrand.
-const CONTAINER_PASSPHRASE := (
- "NETfishing progression container v1 / not an identity credential"
-)
-
-
-static func encode_local_save(
- save_data: Dictionary,
- scratch_path: String,
-) -> PackedByteArray:
- return _encode_container(LOCAL_KIND, save_data, scratch_path)
-
-
-static func encode_archive(
- save_data: Dictionary,
- scratch_path: String,
-) -> PackedByteArray:
- return _encode_container(ARCHIVE_KIND, save_data, scratch_path)
-
-
-static func read_local_save(
- path: String,
- allow_legacy_plaintext: bool = false,
-) -> Dictionary:
- if allow_legacy_plaintext and not _looks_encrypted(path):
- var legacy: Dictionary = _read_plaintext_dictionary(path)
- if bool(legacy.get("ok", false)):
- legacy["legacy_plaintext"] = true
- return legacy
- return _read_container(path, LOCAL_KIND)
-
-
-static func read_archive(path: String) -> Dictionary:
- if not _looks_encrypted(path):
- return {"ok": false, "message": "progression archive is not recognized."}
- return _read_container(path, ARCHIVE_KIND)
-
-
-static func _encode_container(
- kind: String,
- save_data: Dictionary,
- scratch_path: String,
-) -> PackedByteArray:
- if save_data.is_empty() or scratch_path.is_empty():
- return PackedByteArray()
- var payload_json: String = JSON.stringify(save_data)
- if payload_json.is_empty():
- return PackedByteArray()
- var payload_bytes: PackedByteArray = payload_json.to_utf8_buffer()
- var envelope: Dictionary = {
- "magic": MAGIC,
- "format_version": FORMAT_VERSION,
- "kind": kind,
- "game_version": str(
- ProjectSettings.get_setting("application/config/version", "")
- ),
- "created_at_unix": int(Time.get_unix_time_from_system()),
- "payload_sha256": PortableFileGuard.hash_bytes(payload_bytes),
- "payload_json": payload_json,
- }
- if DirAccess.make_dir_recursive_absolute(
- scratch_path.get_base_dir()
- ) != OK:
- return PackedByteArray()
- _remove_if_present(scratch_path)
- var file: FileAccess = FileAccess.open_encrypted_with_pass(
- scratch_path,
- FileAccess.WRITE,
- CONTAINER_PASSPHRASE,
- )
- if file == null:
- return PackedByteArray()
- file.store_string(JSON.stringify(envelope))
- file.flush()
- var write_ok: bool = file.get_error() == OK
- file.close()
- if not write_ok:
- _remove_if_present(scratch_path)
- return PackedByteArray()
- var encoded: PackedByteArray = PortableFileGuard.read_bytes(
- scratch_path,
- MAX_CONTAINER_BYTES,
- )
- _remove_if_present(scratch_path)
- return encoded
-
-
-static func _read_container(path: String, expected_kind: String) -> Dictionary:
- if (
- path.is_empty()
- or not FileAccess.file_exists(path)
- or FileAccess.get_size(path) > MAX_CONTAINER_BYTES
- ):
- return {"ok": false, "message": "progression file is unavailable."}
- var envelope_text: String = _read_encrypted_text(path)
- if envelope_text.is_empty():
- return {"ok": false, "message": "progression file could not be opened."}
- var envelope_json := JSON.new()
- if (
- envelope_json.parse(envelope_text) != OK
- or typeof(envelope_json.data) != TYPE_DICTIONARY
- ):
- return {"ok": false, "message": "progression container is malformed."}
- var envelope: Dictionary = envelope_json.data
- var legacy_container: bool = envelope.get("magic") == LEGACY_MAGIC
- if (
- envelope.get("magic") not in [MAGIC, LEGACY_MAGIC]
- or int(envelope.get("format_version", -1)) != FORMAT_VERSION
- or str(envelope.get("kind", "")) != expected_kind
- ):
- return {"ok": false, "message": "progression container is unsupported."}
- var payload_json: String = str(envelope.get("payload_json", ""))
- var payload_bytes: PackedByteArray = payload_json.to_utf8_buffer()
- if (
- payload_json.is_empty()
- or str(envelope.get("payload_sha256", ""))
- != PortableFileGuard.hash_bytes(payload_bytes)
- ):
- return {"ok": false, "message": "progression container failed validation."}
- var payload_parser := JSON.new()
- if (
- payload_parser.parse(payload_json) != OK
- or typeof(payload_parser.data) != TYPE_DICTIONARY
- ):
- return {"ok": false, "message": "progression payload is malformed."}
- return {
- "ok": true,
- "data": payload_parser.data,
- "game_version": str(envelope.get("game_version", "")),
- "created_at_unix": int(envelope.get("created_at_unix", 0)),
- "legacy_plaintext": false,
- "legacy_container": legacy_container,
- }
-
-
-static func _read_encrypted_text(path: String) -> String:
- var file: FileAccess = FileAccess.open_encrypted_with_pass(
- path,
- FileAccess.READ,
- CONTAINER_PASSPHRASE,
- )
- if file == null:
- return ""
- var text: String = file.get_as_text()
- file.close()
- return text
-
-
-static func _read_plaintext_dictionary(path: String) -> Dictionary:
- var bytes: PackedByteArray = PortableFileGuard.read_bytes(
- path,
- MAX_CONTAINER_BYTES,
- )
- if bytes.is_empty():
- return {"ok": false}
- var text: String = bytes.get_string_from_utf8()
- if not text.strip_edges().begins_with("{"):
- return {"ok": false}
- var parser := JSON.new()
- if parser.parse(text) != OK or typeof(parser.data) != TYPE_DICTIONARY:
- return {"ok": false}
- return {"ok": true, "data": parser.data}
-
-
-static func _looks_encrypted(path: String) -> bool:
- if not FileAccess.file_exists(path) or FileAccess.get_size(path) < 4:
- return false
- var file := FileAccess.open(path, FileAccess.READ)
- if file == null:
- return false
- var header: PackedByteArray = file.get_buffer(4)
- file.close()
- return header == PackedByteArray([0x47, 0x44, 0x45, 0x43])
-
-
-static func _remove_if_present(path: String) -> void:
- if FileAccess.file_exists(path):
- DirAccess.remove_absolute(path)
diff --git a/save/progression_save_codec.gd.uid b/save/progression_save_codec.gd.uid
deleted file mode 100644
index 6db5823..0000000
--- a/save/progression_save_codec.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://lad8mvedktww
diff --git a/scripts/bake_shoreline_ribbons.gd b/scripts/bake_shoreline_ribbons.gd
deleted file mode 100644
index c030579..0000000
--- a/scripts/bake_shoreline_ribbons.gd
+++ /dev/null
@@ -1,42 +0,0 @@
-extends SceneTree
-
-const REGION_SCENE := preload("res://world/regions/starter_island_region.tscn")
-func _initialize() -> void:
- call_deferred("_bake")
-
-
-func _bake() -> void:
- var started := Time.get_ticks_usec()
- var region := REGION_SCENE.instantiate()
- root.add_child(region)
- var baker := region.get_node_or_null("ShorelineRibbonBaker") as ShorelineRibbonBaker
- if baker == null:
- push_error("The map does not contain a configured ShorelineRibbonBaker.")
- quit(1)
- return
- var results := baker.rebuild_all()
- if results.size() != baker.water_bodies.size():
- quit(1)
- return
- for result: Dictionary in results:
- if result.get("skipped", false):
- print(
- "Shoreline %s: type=%s skipped intentionally"
- % [result["output_path"], WaterType.label(result["water_type"])]
- )
- continue
- print(
- "Shoreline %s: type=%s segments=%d loops=%d raw=%d simplified=%d smoothed=%d triangles=%d"
- % [
- result["output_path"],
- WaterType.label(result["water_type"]),
- result["segment_count"],
- result["loop_count"],
- result["raw_point_count"],
- result["simplified_point_count"],
- result["smoothed_point_count"],
- result["triangle_count"],
- ]
- )
- print("Shoreline bake completed in %.2f ms" % ((Time.get_ticks_usec() - started) / 1000.0))
- quit()
diff --git a/scripts/bake_shoreline_ribbons.gd.uid b/scripts/bake_shoreline_ribbons.gd.uid
deleted file mode 100644
index 7b789be..0000000
--- a/scripts/bake_shoreline_ribbons.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://eosavaa6wu1e
diff --git a/scripts/build_android_debug.sh b/scripts/build_android_debug.sh
deleted file mode 100755
index f68eb32..0000000
--- a/scripts/build_android_debug.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
-readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
-readonly OUTPUT_PATH="${PROJECT_ROOT}/builds/android/straywild-debug.apk"
-readonly GODOT_BIN="${GODOT_BIN:-godot}"
-
-mkdir -p -- "$(dirname -- "${OUTPUT_PATH}")"
-
-"${GODOT_BIN}" \
- --headless \
- --path "${PROJECT_ROOT}" \
- --export-debug "Android" \
- "${OUTPUT_PATH}"
-
-test -s "${OUTPUT_PATH}"
-sha256sum "${OUTPUT_PATH}"
diff --git a/scripts/build_playtest.sh b/scripts/build_playtest.sh
index d54ba70..3cf206e 100755
--- a/scripts/build_playtest.sh
+++ b/scripts/build_playtest.sh
@@ -4,14 +4,12 @@ set -euo pipefail
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
-readonly BUILD_ROOT="${PROJECT_ROOT}/builds/v0.20.2-alpha"
+readonly BUILD_ROOT="${PROJECT_ROOT}/builds/playtest-0.1"
readonly WINDOWS_DIR="${BUILD_ROOT}/windows-x86_64"
readonly LINUX_DIR="${BUILD_ROOT}/linux-x86_64"
-readonly README_SOURCE="${PROJECT_ROOT}/docs/README-PLAYTEST.txt"
-readonly SOURCE_COMMIT="$(git -C "${PROJECT_ROOT}" rev-parse HEAD)"
-readonly SOURCE_URL="https://forge.makearmy.io/woofmeow/straywild"
-readonly WINDOWS_ZIP="${BUILD_ROOT}/straywild-v0.20.2-alpha-windows-x86_64.zip"
-readonly LINUX_ZIP="${BUILD_ROOT}/straywild-v0.20.2-alpha-linux-x86_64.zip"
+readonly README_SOURCE="${PROJECT_ROOT}/playtest/README-PLAYTEST.txt"
+readonly WINDOWS_ZIP="${BUILD_ROOT}/netfishing-prealpha-0.1-windows-x86_64.zip"
+readonly LINUX_ZIP="${BUILD_ROOT}/netfishing-prealpha-0.1-linux-x86_64.zip"
readonly GODOT_BIN="${GODOT_BIN:-godot}"
if [[ ! -f "${PROJECT_ROOT}/project.godot" ]]; then
@@ -31,64 +29,29 @@ mkdir -p -- "${WINDOWS_DIR}" "${LINUX_DIR}"
--headless \
--path "${PROJECT_ROOT}" \
--export-release "Windows Desktop" \
- "${WINDOWS_DIR}/straywild.exe"
+ "${WINDOWS_DIR}/netfishing.exe"
"${GODOT_BIN}" \
--headless \
--path "${PROJECT_ROOT}" \
--export-release "Linux Desktop" \
- "${LINUX_DIR}/straywild.x86_64"
+ "${LINUX_DIR}/netfishing"
for required_file in \
- "${WINDOWS_DIR}/straywild.exe" \
- "${WINDOWS_DIR}/straywild.pck" \
- "${LINUX_DIR}/straywild.x86_64" \
- "${LINUX_DIR}/straywild.pck"; do
+ "${WINDOWS_DIR}/netfishing.exe" \
+ "${WINDOWS_DIR}/netfishing.pck" \
+ "${LINUX_DIR}/netfishing" \
+ "${LINUX_DIR}/netfishing.pck"; do
if [[ ! -s "${required_file}" ]]; then
echo "error: expected export output is missing: ${required_file}" >&2
exit 1
fi
done
-chmod +x -- "${LINUX_DIR}/straywild.x86_64"
+chmod +x -- "${LINUX_DIR}/netfishing"
cp -- "${README_SOURCE}" "${WINDOWS_DIR}/README-PLAYTEST.txt"
cp -- "${README_SOURCE}" "${LINUX_DIR}/README-PLAYTEST.txt"
-stage_licenses() {
- local destination="$1/licenses"
- mkdir -p -- "${destination}"
- install -m 0644 "${PROJECT_ROOT}/LICENSE" \
- "${destination}/GPL-3.0-or-later.txt"
- install -m 0644 "${PROJECT_ROOT}/ASSET-LICENSE.md" \
- "${destination}/straywild-ASSET-LICENSE.md"
- install -m 0644 "${PROJECT_ROOT}/docs/ATTRIBUTION.md" \
- "${destination}/ATTRIBUTION.md"
- install -m 0644 "${PROJECT_ROOT}/TRADEMARKS.md" \
- "${destination}/TRADEMARKS.md"
- install -m 0644 "${PROJECT_ROOT}/ui/fonts/Tuffy-LICENSE.txt" \
- "${destination}/Tuffy-LICENSE.txt"
- install -m 0644 "${PROJECT_ROOT}/ui/fonts/Seattle-Avenue-LICENSE.txt" \
- "${destination}/Seattle-Avenue-LICENSE.txt"
- cat >"${destination}/SOURCE-CODE.md" <&2
- exit 2
- ;;
- esac
- shift
-done
-if [[ ${HOTFIX} -eq 1 && ${PACKAGE_ONLY} -ne 1 ]]; then
- echo "PortMaster hotfixes must use an existing pinned ARM64 export." >&2
- echo "Run this script with both --package-only and --hotfix." >&2
- exit 2
-fi
-
-readonly PROJECT_VERSION="$(
- sed -n 's/^config\/version="\([^"]*\)"/\1/p' "${PROJECT_ROOT}/project.godot"
-)"
-if [[ -z "${PROJECT_VERSION}" ]]; then
- echo "Could not read application/config/version from project.godot." >&2
- exit 1
-fi
-
-readonly RELEASE_TAG="v${PROJECT_VERSION}"
-readonly RELEASE_ROOT="${PROJECT_ROOT}/builds/${RELEASE_TAG}"
-readonly ARM64_ROOT="${RELEASE_ROOT}/linux-arm64"
-readonly ARM64_EXECUTABLE="${ARM64_ROOT}/straywild.arm64"
-readonly ARM64_PCK="${ARM64_ROOT}/straywild.pck"
-readonly STAGE_ROOT="${RELEASE_ROOT}/portmaster-stage"
-readonly GAME_ROOT="${STAGE_ROOT}/straywild"
-readonly ARCHIVE="${RELEASE_ROOT}/straywild.zip"
-readonly OBSOLETE_VERSIONED_ARCHIVE="${RELEASE_ROOT}/straywild-${RELEASE_TAG}-portmaster-arm64.zip"
-readonly OBSOLETE_LOWERCASE_ARCHIVE="${RELEASE_ROOT}/straywild-${RELEASE_TAG}-portmaster-arm64.zip"
-
-readonly HEAD_COMMIT="$(git -C "${PROJECT_ROOT}" rev-parse HEAD)"
-readonly TAG_COMMIT="$(git -C "${PROJECT_ROOT}" rev-parse "${RELEASE_TAG}^{commit}")"
-if [[ ${HOTFIX} -eq 1 ]]; then
- if ! git -C "${PROJECT_ROOT}" merge-base --is-ancestor \
- "${TAG_COMMIT}" "${HEAD_COMMIT}"; then
- echo "${RELEASE_TAG} is not an ancestor of HEAD. Refusing hotfix package." >&2
- exit 1
- fi
-
- while IFS= read -r changed_path; do
- case "${changed_path}" in
- docs/PORTMASTER.md|scripts/build_portmaster.sh|scripts/portmaster/*)
- ;;
- *)
- echo "Hotfix contains a non-PortMaster change: ${changed_path}" >&2
- exit 1
- ;;
- esac
- done < <(git -C "${PROJECT_ROOT}" diff --name-only \
- "${TAG_COMMIT}..${HEAD_COMMIT}")
-elif [[ "${HEAD_COMMIT}" != "${TAG_COMMIT}" ]]; then
- echo "HEAD does not match ${RELEASE_TAG}. Refusing to package mutable source." >&2
- exit 1
-fi
-if [[ "${straywild_ALLOW_DIRTY_RELEASE:-0}" != "1" ]]; then
- dirty_tree=0
- git -C "${PROJECT_ROOT}" diff --quiet || dirty_tree=1
- git -C "${PROJECT_ROOT}" diff --cached --quiet || dirty_tree=1
- if [[ -n "$(git -C "${PROJECT_ROOT}" status --porcelain --untracked-files=normal)" ]]; then
- dirty_tree=1
- fi
- if [[ ${dirty_tree} -ne 0 ]]; then
- echo "The repository is not clean. Refusing to build a release package." >&2
- exit 1
- fi
-fi
-
-for command_name in git file zip unzip sha256sum rsync; do
- command -v "${command_name}" >/dev/null 2>&1 || {
- echo "Required command not found: ${command_name}" >&2
- exit 1
- }
-done
-
-mkdir -p -- "${ARM64_ROOT}" "${RELEASE_ROOT}"
-if [[ ${PACKAGE_ONLY} -eq 0 ]]; then
- readonly EXPORT_PROJECT_ROOT="$(
- mktemp -d -t straywild-portmaster-export.XXXXXX
- )"
- cleanup_export_project() {
- rm -rf -- "${EXPORT_PROJECT_ROOT}"
- }
- trap cleanup_export_project EXIT INT TERM
- rm -f -- "${ARM64_EXECUTABLE}" "${ARM64_PCK}"
- rsync -a \
- --exclude '/.git/' \
- --exclude '/.godot/' \
- --exclude '/art/source/' \
- --exclude '/art/exported/system_icons/straywild.ico' \
- --exclude '/art/exported/system_icons/straywild_1024.png' \
- --exclude '/art/exported/system_icons/straywild_1024.png.import' \
- --exclude '/audio/ambience/' \
- --exclude '/builds/' \
- --exclude '/docs/design/' \
- --exclude '/tests/' \
- "${PROJECT_ROOT}/" "${EXPORT_PROJECT_ROOT}/"
- "${ASSET_PROFILE_SCRIPT}" "${EXPORT_PROJECT_ROOT}"
- "${GODOT_BIN}" \
- --headless \
- --path "${EXPORT_PROJECT_ROOT}" \
- --import
- "${GODOT_BIN}" \
- --headless \
- --path "${EXPORT_PROJECT_ROOT}" \
- --export-release "Linux ARM64" \
- "${ARM64_EXECUTABLE}"
- cleanup_export_project
- trap - EXIT INT TERM
-fi
-
-test -s "${ARM64_EXECUTABLE}"
-test -s "${ARM64_PCK}"
-file "${ARM64_EXECUTABLE}" | grep -q "ARM aarch64"
-
-rm -rf -- "${STAGE_ROOT}"
-rm -f -- \
- "${ARCHIVE}" \
- "${OBSOLETE_VERSIONED_ARCHIVE}" \
- "${OBSOLETE_LOWERCASE_ARCHIVE}"
-mkdir -p -- "${GAME_ROOT}/licenses"
-
-install -m 0755 "${TEMPLATE_ROOT}/straywild.sh" "${STAGE_ROOT}/straywild.sh"
-install -m 0644 "${TEMPLATE_ROOT}/port.json" "${GAME_ROOT}/port.json"
-install -m 0644 "${TEMPLATE_ROOT}/straywild.gptk" \
- "${GAME_ROOT}/straywild.gptk"
-install -m 0755 "${TEMPLATE_ROOT}/launch-straywild.sh" \
- "${GAME_ROOT}/launch-straywild.sh"
-install -m 0755 "${ARM64_EXECUTABLE}" "${GAME_ROOT}/straywild.aarch64"
-install -m 0644 "${ARM64_PCK}" "${GAME_ROOT}/straywild.pck"
-install -m 0644 "${TEMPLATE_ROOT}/gameinfo.xml" "${GAME_ROOT}/gameinfo.xml"
-install -m 0644 "${TEMPLATE_ROOT}/screenshot.png" "${GAME_ROOT}/screenshot.png"
-install -m 0644 \
- "${PROJECT_ROOT}/docs/ATTRIBUTION.md" \
- "${GAME_ROOT}/licenses/ATTRIBUTION.md"
-install -m 0644 \
- "${PROJECT_ROOT}/LICENSE" \
- "${GAME_ROOT}/licenses/GPL-3.0-or-later.txt"
-install -m 0644 \
- "${PROJECT_ROOT}/ASSET-LICENSE.md" \
- "${GAME_ROOT}/licenses/straywild-ASSET-LICENSE.md"
-install -m 0644 \
- "${PROJECT_ROOT}/TRADEMARKS.md" \
- "${GAME_ROOT}/licenses/TRADEMARKS.md"
-install -m 0644 \
- "${PROJECT_ROOT}/ui/fonts/Tuffy-LICENSE.txt" \
- "${GAME_ROOT}/licenses/Tuffy-LICENSE.txt"
-install -m 0644 \
- "${PROJECT_ROOT}/ui/fonts/Seattle-Avenue-LICENSE.txt" \
- "${GAME_ROOT}/licenses/Seattle-Avenue-LICENSE.txt"
-
-cat >"${GAME_ROOT}/BUILD-INFO.txt" <"${GAME_ROOT}/licenses/SOURCE-CODE.md" <"${GAME_ROOT}/README.md" </dev/null
-readonly TOP_LEVELS="$(
- unzip -Z1 "${ARCHIVE}" | cut -d/ -f1 | sort -u
-)"
-readonly EXPECTED_TOP_LEVELS="$(printf '%s\n' straywild straywild.sh)"
-if [[ "${TOP_LEVELS}" != "${EXPECTED_TOP_LEVELS}" ]]; then
- echo "Unexpected PortMaster archive roots:" >&2
- printf '%s\n' "${TOP_LEVELS}" >&2
- exit 1
-fi
-grep -q '"version": 4' "${GAME_ROOT}/port.json"
-grep -q '"name": "straywild.zip"' "${GAME_ROOT}/port.json"
-grep -q '"rtr": true' "${GAME_ROOT}/port.json"
-if unzip -Z1 "${ARCHIVE}" | grep -qx 'port.json'; then
- echo "port.json must be inside straywild/, not at the archive root." >&2
- exit 1
-fi
-if grep -Eq '^[[:space:]]*#[[:space:]]*PORTMASTER:' \
- "${STAGE_ROOT}/straywild.sh"; then
- echo "The distributable launcher contains an installed PortMaster signature." >&2
- exit 1
-fi
-grep -Fq 'PROFILE_PATH="$CONFDIR/performance_profile"' \
- "${STAGE_ROOT}/straywild.sh"
-grep -Fq 'straywild_PERFORMANCE_PROFILE=light' \
- "${STAGE_ROOT}/straywild.sh"
-grep -Fq 'CONTROLLER_MAPPING_FILE="$CONFDIR/cache/controller_mapping.txt"' \
- "${STAGE_ROOT}/straywild.sh"
-grep -Fq 'SAVEDIR="$PORTS_ROOT/saves/straywild"' \
- "${STAGE_ROOT}/straywild.sh"
-grep -Fq 'LEGACY_SAVEDIR="$PORTS_ROOT/saves/netfishing"' \
- "${STAGE_ROOT}/straywild.sh"
-grep -Fq 'cp -a "$LEGACY_CONFDIR/." "$CONFDIR/"' \
- "${STAGE_ROOT}/straywild.sh"
-grep -Fq '"straywild_DATA_DIR=$SAVEDIR"' \
- "${STAGE_ROOT}/straywild.sh"
-grep -Fq '"straywild_CREATE_DATA_DIR=1"' \
- "${STAGE_ROOT}/straywild.sh"
-grep -Fq 'printf '\''%s\n'\'' "$straywild_controllerconfig" > "$CONTROLLER_MAPPING_FILE"' \
- "${STAGE_ROOT}/straywild.sh"
-grep -Fq '"$GAME_LAUNCHER"' "${STAGE_ROOT}/straywild.sh"
-grep -Fq '"$CONTROLLER_MAPPING_FILE"' "${STAGE_ROOT}/straywild.sh"
-if grep -Fq 'SDL_GAMECONTROLLERCONFIG="$straywild_controllerconfig"' \
- "${STAGE_ROOT}/straywild.sh"; then
- echo "Raw controller mappings must not be passed through westonwrap.sh." >&2
- exit 1
-fi
-grep -Fq 'SDL_GAMECONTROLLERCONFIG="$(<"$CONTROLLER_MAPPING_FILE")"' \
- "${GAME_ROOT}/launch-straywild.sh"
-grep -Fq 'export SDL_GAMECONTROLLERCONFIG' \
- "${GAME_ROOT}/launch-straywild.sh"
-grep -Fq 'exec "$@"' "${GAME_ROOT}/launch-straywild.sh"
-grep -Fq 'GPTOKEYB_CONFIG="$GAMEDIR/straywild.gptk"' \
- "${STAGE_ROOT}/straywild.sh"
-grep -Fq '$GPTOKEYB "straywild.aarch64" -c "$GPTOKEYB_CONFIG" &' \
- "${STAGE_ROOT}/straywild.sh"
-grep -Fq 'pm_platform_helper "$GAME_EXECUTABLE"' \
- "${STAGE_ROOT}/straywild.sh"
-grep -Fq 'pm_finish' "${STAGE_ROOT}/straywild.sh"
-if [[ "$(grep -Ec '^[[:space:]]*\$GPTOKEYB[[:space:]]' \
- "${STAGE_ROOT}/straywild.sh")" -ne 1 ]]; then
- echo "straywild must start exactly one GPTOKEYB exit handler." >&2
- exit 1
-fi
-readonly GPTOKEYB_NOOP_KEYS=(
- back guide start
- a a_hk b b_hk x x_hk y y_hk
- l1 l1_hk l2 l2_hk l3 r1 r1_hk r2 r2_hk r3
- up down left right
- left_analog_up left_analog_down left_analog_left left_analog_right
- right_analog_up right_analog_down right_analog_left right_analog_right
-)
-for noop_key in "${GPTOKEYB_NOOP_KEYS[@]}"; do
- if ! grep -Eq \
- "^[[:space:]]*${noop_key}[[:space:]]*=[[:space:]]*\\\\\"[[:space:]]*$" \
- "${GAME_ROOT}/straywild.gptk"; then
- echo "GPTOKEYB no-op mapping is missing: ${noop_key}" >&2
- exit 1
- fi
-done
-if [[ "$(grep -Ec '^[[:space:]]*[a-z0-9_]+[[:space:]]*=' \
- "${GAME_ROOT}/straywild.gptk")" -ne "${#GPTOKEYB_NOOP_KEYS[@]}" ]]; then
- echo "GPTOKEYB no-op mapping contains an unexpected assignment." >&2
- exit 1
-fi
-if unzip -Z1 "${ARCHIVE}" | grep -E \
- '(^|/)(conf|\.git|\.godot|logs?|saves?|identities?)(/|$)|\.(gd|tscn|blend)$' \
- >/dev/null; then
- echo "The PortMaster archive contains prohibited release content." >&2
- exit 1
-fi
-
-sha256sum "${ARCHIVE}"
-echo "PortMaster package created at ${ARCHIVE}"
diff --git a/scripts/import_animalese_voice.sh b/scripts/import_animalese_voice.sh
deleted file mode 100755
index b9076ca..0000000
--- a/scripts/import_animalese_voice.sh
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-readonly TARGET_RMS_DBFS="-18.0"
-readonly PEAK_CEILING_DBFS="-3.0"
-readonly OUTPUT_SAMPLE_RATE="48000"
-
-usage() {
- printf 'usage: %s SOURCE_DIRECTORY SAMPLE_SET_DIRECTORY\n' "$0" >&2
- printf 'example: %s /path/to/voice_kim kim\n' "$0" >&2
-}
-
-if (( $# != 2 )); then
- usage
- exit 2
-fi
-
-if ! command -v sox >/dev/null 2>&1; then
- printf 'error: sox is required to import animalese samples\n' >&2
- exit 1
-fi
-
-readonly SCRIPT_DIRECTORY="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
-readonly PROJECT_ROOT="$(cd -- "$SCRIPT_DIRECTORY/.." && pwd)"
-readonly SOURCE_DIRECTORY="$(realpath -e -- "$1")"
-readonly SAMPLE_SET_DIRECTORY="$2"
-
-if [[ ! -d "$SOURCE_DIRECTORY" ]]; then
- printf 'error: source is not a directory: %s\n' "$SOURCE_DIRECTORY" >&2
- exit 1
-fi
-if [[ ! "$SAMPLE_SET_DIRECTORY" =~ ^[a-z0-9_-]+$ ]]; then
- printf 'error: sample-set directory must use lowercase letters, numbers, underscores, or hyphens\n' >&2
- exit 1
-fi
-
-readonly DESTINATION_DIRECTORY="$PROJECT_ROOT/sound/dialogue/animalese/$SAMPLE_SET_DIRECTORY"
-readonly WORK_DIRECTORY="$(mktemp -d -t straywild-animalese-import.XXXXXX)"
-
-cleanup() {
- if [[ "$WORK_DIRECTORY" == /tmp/straywild-animalese-import.* ]]; then
- find "$WORK_DIRECTORY" -depth -delete
- fi
-}
-trap cleanup EXIT
-
-mkdir -p -- "$WORK_DIRECTORY/converted" "$WORK_DIRECTORY/normalized"
-mapfile -d '' SOURCE_FILES < <(
- find "$SOURCE_DIRECTORY" -maxdepth 1 -type f -name '*.wav' -print0 \
- | sort -z
-)
-if (( ${#SOURCE_FILES[@]} == 0 )); then
- printf 'error: no .wav samples found in %s\n' "$SOURCE_DIRECTORY" >&2
- exit 1
-fi
-
-for source_file in "${SOURCE_FILES[@]}"; do
- filename="$(basename -- "$source_file")"
- if [[ ! "$filename" =~ ^[a-z0-9_]+\.wav$ ]]; then
- printf 'error: unsupported sample filename: %s\n' "$filename" >&2
- exit 1
- fi
- converted_file="$WORK_DIRECTORY/converted/$filename"
- normalized_file="$WORK_DIRECTORY/normalized/$filename"
-
- # Measure the exact channel/rate conversion that will be shipped, then use
- # the smaller of the RMS correction and peak-safe gain. Very short voice
- # clips are not reliable inputs for program-loudness algorithms such as
- # EBU R128, so this deterministic RMS/peak policy is intentional.
- # Reserve headroom before high-quality resampling. Several supplied 8-bit
- # clips sit near full scale and their interpolated waveform can otherwise
- # clip before the normalization gain is calculated.
- sox -D "$source_file" \
- -r "$OUTPUT_SAMPLE_RATE" -c 1 -b 32 -e floating-point \
- "$converted_file" gain -6 rate -v "$OUTPUT_SAMPLE_RATE"
- statistics="$(sox "$converted_file" -n stat 2>&1)"
- peak_amplitude="$(awk '/Maximum amplitude/ {print $3}' <<< "$statistics")"
- rms_amplitude="$(awk '/RMS.*amplitude/ {print $3; exit}' <<< "$statistics")"
- if ! awk -v peak="$peak_amplitude" -v rms="$rms_amplitude" \
- 'BEGIN {exit !(peak > 0.0 && rms > 0.0)}'; then
- printf 'error: sample is silent or unreadable: %s\n' "$source_file" >&2
- exit 1
- fi
-
- gain_db="$(
- awk \
- -v peak="$peak_amplitude" \
- -v rms="$rms_amplitude" \
- -v target_rms="$TARGET_RMS_DBFS" \
- -v peak_ceiling="$PEAK_CEILING_DBFS" \
- 'BEGIN {
- peak_db = 20.0 * log(peak) / log(10.0)
- rms_db = 20.0 * log(rms) / log(10.0)
- rms_gain = target_rms - rms_db
- peak_gain = peak_ceiling - peak_db
- gain = rms_gain < peak_gain ? rms_gain : peak_gain
- printf "%.6f", gain
- }'
- )"
- sox -D "$converted_file" \
- -r "$OUTPUT_SAMPLE_RATE" -c 1 -b 16 -e signed-integer \
- "$normalized_file" gain "$gain_db"
- printf '%-16s %8s dB\n' "$filename" "$gain_db"
-done
-
-mkdir -p -- "$DESTINATION_DIRECTORY"
-for normalized_file in "$WORK_DIRECTORY"/normalized/*.wav; do
- cp -- "$normalized_file" "$DESTINATION_DIRECTORY/$(basename -- "$normalized_file")"
-done
-
-printf 'imported %d normalized samples to %s\n' \
- "${#SOURCE_FILES[@]}" "$DESTINATION_DIRECTORY"
diff --git a/scripts/normalize_texture_imports.gd b/scripts/normalize_texture_imports.gd
deleted file mode 100644
index e7b2406..0000000
--- a/scripts/normalize_texture_imports.gd
+++ /dev/null
@@ -1,229 +0,0 @@
-extends SceneTree
-
-const DEFAULT_PROFILE: Dictionary = {
- "params": {
- "compress/mode": 2,
- "compress/high_quality": false,
- "compress/lossy_quality": 0.7,
- "compress/uastc_level": 0,
- "compress/rdo_quality_loss": 0.0,
- "compress/hdr_compression": 1,
- "compress/normal_map": 0,
- "compress/channel_pack": 0,
- "mipmaps/generate": false,
- "mipmaps/limit": -1,
- "roughness/mode": 0,
- "roughness/src_normal": "",
- "process/channel_remap/red": 0,
- "process/channel_remap/green": 1,
- "process/channel_remap/blue": 2,
- "process/channel_remap/alpha": 3,
- "process/fix_alpha_border": true,
- "process/premult_alpha": false,
- "process/normal_map_invert_y": false,
- "process/hdr_as_srgb": false,
- "process/hdr_clamp_exposure": false,
- "process/size_limit": 0,
- "detect_3d/compress_to": 0,
- },
-}
-const FACIAL_FEATURE_IMPORT_PATH: String = (
- "art/exported/characters/faces/"
-)
-const FACIAL_FEATURE_SIZE_LIMIT: int = 512
-const PROJECTED_TERRAIN_TEXTURE_IMPORTS: Array[String] = [
- "res://world/generation/chunks/assets/chunk_0000_grass_lite.png.import",
- "res://world/generation/chunks/assets/chunk_0001_sand.png.import",
- "res://world/generation/chunks/assets/chunk_0031_dirt.png.import",
-]
-
-var apply_changes: bool = false
-var verbose: bool = false
-var roots: PackedStringArray = PackedStringArray()
-var should_exit: bool = false
-
-var errors: int = 0
-var skipped_non_texture: int = 0
-var updated: int = 0
-var unchanged: int = 0
-
-func _initialize() -> void:
- _parse_args()
- if should_exit:
- return
- if roots.is_empty():
- printerr("At least one --root path is required.")
- quit(2)
- return
-
- for root: String in roots:
- var normalized_root := root
- if not normalized_root.ends_with("/"):
- normalized_root += "/"
- if not normalized_root.begins_with("res://") and not normalized_root.is_absolute_path():
- if normalized_root.begins_with("./"):
- normalized_root = "res://%s" % normalized_root.trim_prefix("./")
- else:
- normalized_root = "res://%s" % normalized_root
- _print("Scanning %s" % normalized_root)
- _scan_directory(normalized_root)
-
- print_summary()
- quit(1 if errors > 0 else 0)
-
-
-func _parse_args() -> void:
- var args := OS.get_cmdline_user_args()
- var i := 0
- while i < args.size():
- var arg := args[i]
- if arg == "--help" or arg == "-h":
- _print_usage()
- should_exit = true
- quit(0)
- return
- elif arg == "--apply":
- apply_changes = true
- i += 1
- elif arg == "--verbose" or arg == "-v":
- verbose = true
- i += 1
- elif arg == "--root":
- if i + 1 >= args.size():
- printerr("--root requires a path value")
- should_exit = true
- quit(1)
- return
- i += 1
- roots.append(args[i])
- i += 1
- elif arg.begins_with("--root="):
- roots.append(arg.trim_prefix("--root="))
- i += 1
- else:
- printerr("Unknown argument: ", arg)
- should_exit = true
- quit(2)
- return
-
-
-func _scan_directory(path: String) -> void:
- var dir := DirAccess.open(path)
- if dir == null:
- printerr("Failed to open root: ", path)
- errors += 1
- return
-
- dir.list_dir_begin()
- while true:
- var name := dir.get_next()
- if name.is_empty():
- break
- if name == "." or name == "..":
- continue
- if name == ".godot":
- continue
-
- var candidate := path.path_join(name)
- if dir.current_is_dir():
- _scan_directory(candidate)
- continue
-
- if name.get_extension().to_lower() != "import":
- continue
-
- _handle_import_file(candidate)
-
-
-func _handle_import_file(import_path: String) -> void:
- var cfg := ConfigFile.new()
- var err := cfg.load(import_path)
- if err != OK:
- printerr("Could not load import settings: ", import_path, " (", err, ")")
- errors += 1
- return
-
- if not cfg.has_section("remap") or not cfg.has_section_key("remap", "importer"):
- skipped_non_texture += 1
- return
-
- if str(cfg.get_value("remap", "importer")) != "texture":
- skipped_non_texture += 1
- return
-
- var changed := false
- for section_name: String in DEFAULT_PROFILE.keys():
- var section_data: Dictionary = DEFAULT_PROFILE[section_name]
- for section_key: String in section_data.keys():
- var desired: Variant = section_data[section_key]
- if section_key == "process/size_limit":
- desired = _size_limit_for(import_path)
- elif section_key == "mipmaps/generate":
- desired = _generates_mipmaps(import_path)
- var existing: Variant = null
- if cfg.has_section(section_name) and cfg.has_section_key(section_name, section_key):
- existing = cfg.get_value(section_name, section_key)
- if existing != desired:
- cfg.set_value(section_name, section_key, desired)
- changed = true
-
- if not changed:
- unchanged += 1
- if verbose:
- _print("Unchanged: %s" % import_path)
- return
-
- if apply_changes:
- err = cfg.save(import_path)
- if err != OK:
- printerr("Could not save import settings: ", import_path, " (", err, ")")
- errors += 1
- return
- if verbose:
- _print("Updated: %s" % import_path)
- else:
- _print("Would update: %s" % import_path)
-
- updated += 1
-
-
-func _size_limit_for(import_path: String) -> int:
- var normalized_path := import_path.replace("\\", "/")
- if FACIAL_FEATURE_IMPORT_PATH in normalized_path:
- return FACIAL_FEATURE_SIZE_LIMIT
- return int(DEFAULT_PROFILE["params"]["process/size_limit"])
-
-
-func _generates_mipmaps(import_path: String) -> bool:
- var normalized_path := import_path.replace("\\", "/")
- return normalized_path in PROJECTED_TERRAIN_TEXTURE_IMPORTS
-
-
-func print_summary() -> void:
- if apply_changes:
- _print(
- "Texture import normalization complete: "
- + "updated=%d unchanged=%d skipped_non_texture=%d errors=%d"
- % [updated, unchanged, skipped_non_texture, errors]
- )
- else:
- _print(
- "Dry-run complete: "
- + "would-update=%d unchanged=%d skipped_non_texture=%d errors=%d"
- % [updated, unchanged, skipped_non_texture, errors]
- )
- _print("Pass --apply to write normalized settings into .import files.")
-
-
-func _print(msg: String) -> void:
- print(msg)
-
-
-func _print_usage() -> void:
- print("Usage:")
- print(" godot --headless --path . --script scripts/normalize_texture_imports.gd [--apply] [--verbose] [--root ] [--root=] [--help]")
- print()
- print("Defaults:")
- print("- Dry-run only unless --apply is provided")
- print("- Requires at least one explicit --root path")
- print("- Skips .godot")
diff --git a/scripts/normalize_texture_imports.gd.uid b/scripts/normalize_texture_imports.gd.uid
deleted file mode 100644
index d2006e5..0000000
--- a/scripts/normalize_texture_imports.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b24mlapvqxj5a
diff --git a/scripts/normalize_texture_profiles.sh b/scripts/normalize_texture_profiles.sh
deleted file mode 100755
index 0551efd..0000000
--- a/scripts/normalize_texture_profiles.sh
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
-readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
-readonly GODOT_BIN="${GODOT_BIN:-godot}"
-
-if [[ $# -ne 1 ]]; then
- echo "usage: scripts/normalize_texture_profiles.sh res://path/to/textures" >&2
- exit 2
-fi
-
-readonly ROOT_PATH="$1"
-
-readonly WORK_ROOT="$(mktemp -d -t straywild-godot-data.XXXXXX)"
-trap 'rm -rf -- "${WORK_ROOT}"' EXIT
-
-cd "${PROJECT_ROOT}"
-
-export XDG_DATA_HOME="${WORK_ROOT}/data"
-export XDG_CONFIG_HOME="${WORK_ROOT}/config"
-export XDG_CACHE_HOME="${WORK_ROOT}/cache"
-
-"${GODOT_BIN}" --headless --path . \
- --script scripts/normalize_texture_imports.gd -- --apply --root "${ROOT_PATH}"
-
-# Synchronize Godot-generated remap and destination metadata with the updated
-# import parameters before the caller reviews or commits the sidecars.
-"${GODOT_BIN}" --headless --path . --import
diff --git a/scripts/portmaster/gameinfo.xml b/scripts/portmaster/gameinfo.xml
deleted file mode 100755
index 1b0a1c7..0000000
--- a/scripts/portmaster/gameinfo.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
- ./straywild.sh
- straywild
- A relaxed multiplayer fishing game about exploring a small island, catching and collecting fish, customizing your character, and spending time by the water.
- 20260804T000000
- straywild project
- Independent
- adventure, simulation
- ./straywild/screenshot.png
-
-
diff --git a/scripts/portmaster/launch-straywild.sh b/scripts/portmaster/launch-straywild.sh
deleted file mode 100755
index 50f2ec9..0000000
--- a/scripts/portmaster/launch-straywild.sh
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/bin/bash
-
-if [[ $# -lt 2 ]]; then
- echo "Usage: $0 [arguments...]" >&2
- exit 2
-fi
-
-CONTROLLER_MAPPING_FILE="$1"
-shift
-
-if [[ ! -r "$CONTROLLER_MAPPING_FILE" ]]; then
- echo "Controller mapping file is not readable: $CONTROLLER_MAPPING_FILE" >&2
- exit 1
-fi
-
-SDL_GAMECONTROLLERCONFIG="$(<"$CONTROLLER_MAPPING_FILE")"
-if [[ "$SDL_GAMECONTROLLERCONFIG" != *,*,* ]]; then
- echo "Controller mapping file is malformed: $CONTROLLER_MAPPING_FILE" >&2
- exit 1
-fi
-
-export SDL_GAMECONTROLLERCONFIG
-exec "$@"
diff --git a/scripts/portmaster/port.json b/scripts/portmaster/port.json
deleted file mode 100644
index 4c36648..0000000
--- a/scripts/portmaster/port.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "version": 4,
- "name": "straywild.zip",
- "items": [
- "straywild.sh",
- "straywild/"
- ],
- "items_opt": null,
- "attr": {
- "title": "straywild",
- "porter": [
- "Voyager"
- ],
- "desc": "A relaxed multiplayer fishing game about exploring a small island, catching and collecting fish, customizing your character, and spending time by the water.",
- "desc_md": null,
- "inst": "Ready to run on supported ARM64 PortMaster devices. This alpha package requires two analog sticks.",
- "inst_md": null,
- "genres": [
- "adventure",
- "simulation"
- ],
- "image": {},
- "rtr": true,
- "exp": false,
- "runtime": [
- "weston_pkg_0.2.squashfs"
- ],
- "store": [],
- "availability": "full",
- "reqs": [
- "analog_2"
- ],
- "arch": [
- "aarch64"
- ],
- "min_glibc": "2.28"
- }
-}
diff --git a/scripts/portmaster/screenshot.png b/scripts/portmaster/screenshot.png
deleted file mode 100755
index 4fe8f2e..0000000
Binary files a/scripts/portmaster/screenshot.png and /dev/null differ
diff --git a/scripts/portmaster/screenshot.png.import b/scripts/portmaster/screenshot.png.import
deleted file mode 100644
index 3aa5f2d..0000000
--- a/scripts/portmaster/screenshot.png.import
+++ /dev/null
@@ -1,40 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://ww0w8iubiwsf"
-path="res://.godot/imported/screenshot.png-6eed5589b80ced9ab24aed11cadf34f5.ctex"
-metadata={
-"vram_texture": false
-}
-
-[deps]
-
-source_file="res://scripts/portmaster/screenshot.png"
-dest_files=["res://.godot/imported/screenshot.png-6eed5589b80ced9ab24aed11cadf34f5.ctex"]
-
-[params]
-
-compress/mode=0
-compress/high_quality=false
-compress/lossy_quality=0.7
-compress/uastc_level=0
-compress/rdo_quality_loss=0.0
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/channel_remap/red=0
-process/channel_remap/green=1
-process/channel_remap/blue=2
-process/channel_remap/alpha=3
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=1
diff --git a/scripts/portmaster/straywild.gptk b/scripts/portmaster/straywild.gptk
deleted file mode 100644
index 2e8602e..0000000
--- a/scripts/portmaster/straywild.gptk
+++ /dev/null
@@ -1,41 +0,0 @@
-# straywild reads the controller directly. GPTOKEYB is retained only for its
-# device-specific force-quit chord, so every keyboard and mouse binding is
-# disabled to prevent duplicate input.
-
-back = \"
-guide = \"
-start = \"
-
-a = \"
-a_hk = \"
-b = \"
-b_hk = \"
-x = \"
-x_hk = \"
-y = \"
-y_hk = \"
-
-l1 = \"
-l1_hk = \"
-l2 = \"
-l2_hk = \"
-l3 = \"
-r1 = \"
-r1_hk = \"
-r2 = \"
-r2_hk = \"
-r3 = \"
-
-up = \"
-down = \"
-left = \"
-right = \"
-
-left_analog_up = \"
-left_analog_down = \"
-left_analog_left = \"
-left_analog_right = \"
-right_analog_up = \"
-right_analog_down = \"
-right_analog_left = \"
-right_analog_right = \"
diff --git a/scripts/portmaster/straywild.sh b/scripts/portmaster/straywild.sh
deleted file mode 100755
index d4e56a7..0000000
--- a/scripts/portmaster/straywild.sh
+++ /dev/null
@@ -1,178 +0,0 @@
-#!/bin/bash
-
-XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share}
-
-if [ -d "/opt/system/Tools/PortMaster/" ]; then
- controlfolder="/opt/system/Tools/PortMaster"
-elif [ -d "/opt/tools/PortMaster/" ]; then
- controlfolder="/opt/tools/PortMaster"
-elif [ -d "$XDG_DATA_HOME/PortMaster/" ]; then
- controlfolder="$XDG_DATA_HOME/PortMaster"
-else
- controlfolder="/roms/ports/PortMaster"
-fi
-
-source "$controlfolder/control.txt"
-[ -f "${controlfolder}/mod_${CFW_NAME}.txt" ] && source "${controlfolder}/mod_${CFW_NAME}.txt"
-get_controls
-
-GAMEDIR="/${directory}/ports/straywild"
-
-CONFDIR="$GAMEDIR/conf"
-PORTS_ROOT="${GAMEDIR%/straywild}"
-SAVEDIR="$PORTS_ROOT/saves/straywild"
-LEGACY_GAMEDIR="$PORTS_ROOT/netfishing"
-LEGACY_CONFDIR="$LEGACY_GAMEDIR/conf"
-LEGACY_SAVEDIR="$PORTS_ROOT/saves/netfishing"
-DATA_BOOTSTRAP_DIR="$CONFDIR/data/godot/app_userdata/straywild"
-LEGACY_DATA_BOOTSTRAP_DIR="$CONFDIR/data/godot/app_userdata/NETFISHING"
-GAME_EXECUTABLE="$GAMEDIR/straywild.aarch64"
-GAME_LAUNCHER="$GAMEDIR/launch-straywild.sh"
-GPTOKEYB_CONFIG="$GAMEDIR/straywild.gptk"
-CONTROLLER_MAPPING_FILE="$CONFDIR/cache/controller_mapping.txt"
-WESTON_DIR="/tmp/straywild-weston"
-WESTON_RUNTIME="weston_pkg_0.2"
-HARBOURMASTER="$controlfolder/harbourmaster"
-
-mkdir -p "$CONFDIR/data" "$CONFDIR/config" "$CONFDIR/cache" "$WESTON_DIR"
-chmod +x "$GAME_EXECUTABLE"
-
-# A renamed PortMaster entry installs beside the former NETfishing port. Copy
-# its device-local configuration once and continue using its established save
-# root when present. New installations use straywild paths exclusively.
-if [ -d "$LEGACY_CONFDIR" ] && [ ! -f "$CONFDIR/.netfishing-imported" ]; then
- cp -a "$LEGACY_CONFDIR/." "$CONFDIR/"
- touch "$CONFDIR/.netfishing-imported"
-fi
-if [ ! -f "$SAVEDIR/straywild_data.json" ] \
- && [ ! -f "$SAVEDIR/netfishing_data.json" ] \
- && { [ -f "$LEGACY_SAVEDIR/straywild_data.json" ] \
- || [ -f "$LEGACY_SAVEDIR/netfishing_data.json" ]; }; then
- SAVEDIR="$LEGACY_SAVEDIR"
-fi
-
-# straywild uses a predictable save directory beside the port instead of
-# presenting a folder picker on a small screen. Preserve an established
-# data-root choice when upgrading an existing installation.
-straywild_DATA_ENVIRONMENT=()
-if [ -f "$SAVEDIR/straywild_data.json" ] \
- || [ -f "$SAVEDIR/netfishing_data.json" ] || {
- [ ! -f "$DATA_BOOTSTRAP_DIR/data_root_bootstrap.json" ] &&
- [ ! -f "$DATA_BOOTSTRAP_DIR/data_root_bootstrap.json.backup" ] &&
- [ ! -f "$LEGACY_DATA_BOOTSTRAP_DIR/data_root_bootstrap.json" ] &&
- [ ! -f "$LEGACY_DATA_BOOTSTRAP_DIR/data_root_bootstrap.json.backup" ]
-}; then
- mkdir -p "$SAVEDIR"
- straywild_DATA_ENVIRONMENT+=(
- "straywild_DATA_DIR=$SAVEDIR"
- "straywild_CREATE_DATA_DIR=1"
- )
-fi
-
-> "$GAMEDIR/log.txt" && exec > >(tee "$GAMEDIR/log.txt") 2>&1
-
-if [ ! -f "$controlfolder/libs/${WESTON_RUNTIME}.squashfs" ]; then
- if [ ! -x "$HARBOURMASTER" ]; then
- pm_message "straywild requires the latest PortMaster and WestonPack runtime."
- sleep 5
- exit 1
- fi
- $ESUDO "$HARBOURMASTER" --quiet --no-check runtime_check "${WESTON_RUNTIME}.squashfs"
-fi
-
-if [ ! -f "$controlfolder/libs/${WESTON_RUNTIME}.squashfs" ]; then
- pm_message "WestonPack could not be installed. Check the network connection and PortMaster runtime manager."
- sleep 5
- exit 1
-fi
-
-if [[ "$PM_CAN_MOUNT" != "N" ]]; then
- $ESUDO umount "$WESTON_DIR" 2>/dev/null
-fi
-$ESUDO mount "$controlfolder/libs/${WESTON_RUNTIME}.squashfs" "$WESTON_DIR"
-
-cd "$GAMEDIR" || exit 1
-
-# Godot 4.7 identifies the muOS H700 virtual controller with a CRC-bearing
-# GUID and numbers its buttons without the volume keys exposed by SDL2. The
-# PortMaster SDL2 database entry therefore cannot drive Godot's gamepad layer.
-GODOT_MUOS_MAPPING="19004ca6010000000100000000010000,muOS-Keys,a:b0,b:b1,x:b3,y:b2,leftshoulder:b4,rightshoulder:b5,lefttrigger:b10,righttrigger:b11,guide:b8,start:b7,back:b6,dpup:h0.1,dpleft:h0.8,dpright:h0.2,dpdown:h0.4,leftx:a0,lefty:a1,leftstick:b9,rightx:a2,righty:a3,rightstick:b12,platform:Linux,"
-MUOS_SDL2_GUID="19000000010000000100000000010000"
-if [[ "$sdl_controllerconfig" == "${MUOS_SDL2_GUID},"* ]]; then
- straywild_controllerconfig="$GODOT_MUOS_MAPPING"
-elif [ -n "$sdl_controllerconfig" ]; then
- straywild_controllerconfig="$sdl_controllerconfig"
-else
- straywild_controllerconfig="$GODOT_MUOS_MAPPING"
-fi
-printf '%s\n' "$straywild_controllerconfig" > "$CONTROLLER_MAPPING_FILE"
-
-straywild_GODOT_OPTIONS=()
-straywild_GAME_ENVIRONMENT=()
-PERFORMANCE_PROFILE="${straywild_PERFORMANCE_PROFILE:-}"
-PROFILE_PATH="$CONFDIR/performance_profile"
-if [[ -z "$PERFORMANCE_PROFILE" && -r "$PROFILE_PATH" ]]; then
- IFS= read -r PERFORMANCE_PROFILE < "$PROFILE_PATH" || true
- PERFORMANCE_PROFILE="${PERFORMANCE_PROFILE%$'\r'}"
-fi
-case "$PERFORMANCE_PROFILE" in
- normal)
- straywild_GAME_ENVIRONMENT+=(
- "straywild_PERFORMANCE_PROFILE=normal"
- )
- ;;
- light|"")
- straywild_GAME_ENVIRONMENT+=(
- "straywild_PERFORMANCE_PROFILE=light"
- "straywild_LOW_END=1"
- )
- straywild_GODOT_OPTIONS+=(
- --single-window
- --disable-vsync
- --max-fps 30
- --audio-output-latency 40
- )
- ;;
- *)
- echo "Unknown performance profile '$PERFORMANCE_PROFILE'; using light." >&2
- straywild_GAME_ENVIRONMENT+=(
- "straywild_PERFORMANCE_PROFILE=light"
- "straywild_LOW_END=1"
- )
- straywild_GODOT_OPTIONS+=(
- --single-window
- --disable-vsync
- --max-fps 30
- --audio-output-latency 40
- )
- ;;
-esac
-
-# straywild reads gameplay input natively. The explicit no-op map prevents
-# GPTOKEYB from injecting duplicate keyboard or mouse input while retaining
-# its device-specific force-quit chord, including on ROCKNIX.
-$GPTOKEYB "straywild.aarch64" -c "$GPTOKEYB_CONFIG" &
-pm_platform_helper "$GAME_EXECUTABLE"
-
-$ESUDO env CRUSTY_RESOLUTION="${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}" \
- "$WESTON_DIR/westonwrap.sh" headless noop kiosk crusty_x11egl \
- XDG_DATA_HOME="$CONFDIR/data" \
- XDG_CONFIG_HOME="$CONFDIR/config" \
- XDG_CACHE_HOME="$CONFDIR/cache" \
- GODOT_SILENCE_ROOT_WARNING=1 \
- "${straywild_DATA_ENVIRONMENT[@]}" \
- "${straywild_GAME_ENVIRONMENT[@]}" \
- "$GAME_LAUNCHER" \
- "$CONTROLLER_MAPPING_FILE" \
- "$GAME_EXECUTABLE" \
- "${straywild_GODOT_OPTIONS[@]}" \
- --resolution "${DISPLAY_WIDTH}x${DISPLAY_HEIGHT}" \
- --fullscreen \
- --rendering-driver opengl3_es \
- --audio-driver ALSA
-
-$ESUDO "$WESTON_DIR/westonwrap.sh" cleanup
-if [[ "$PM_CAN_MOUNT" != "N" ]]; then
- $ESUDO umount "$WESTON_DIR" 2>/dev/null
-fi
-pm_finish
diff --git a/scripts/prepare_portmaster_assets.sh b/scripts/prepare_portmaster_assets.sh
deleted file mode 100755
index 0429f69..0000000
--- a/scripts/prepare_portmaster_assets.sh
+++ /dev/null
@@ -1,96 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-if [[ $# -ne 1 ]]; then
- echo "usage: scripts/prepare_portmaster_assets.sh " >&2
- exit 2
-fi
-
-readonly PROJECT_ROOT="$1"
-readonly GENERAL_TEXTURE_LIMIT=128
-readonly GAMEPLAY_TEXTURE_LIMIT=64
-readonly AUDIO_SAMPLE_RATE=22050
-
-if [[ ! -f "${PROJECT_ROOT}/project.godot" ]]; then
- echo "PortMaster asset project is missing project.godot: ${PROJECT_ROOT}" >&2
- exit 1
-fi
-
-limit_texture_imports() {
- local asset_root="$1"
- local size_limit="$2"
- local import_file
- local updated=0
-
- if [[ ! -d "${asset_root}" ]]; then
- return
- fi
- while IFS= read -r -d '' import_file; do
- if ! grep -q '^importer="texture"$' "${import_file}"; then
- continue
- fi
- if ! grep -q '^process/size_limit=' "${import_file}"; then
- continue
- fi
- sed -i \
- -e 's|^compress/mode=.*$|compress/mode=2|' \
- -e "s|^process/size_limit=.*$|process/size_limit=${size_limit}|" \
- "${import_file}"
- updated=$((updated + 1))
- done < <(find "${asset_root}" -type f -name '*.import' -print0)
- printf 'PortMaster texture limit %d px: %d imports under %s\n' \
- "${size_limit}" "${updated}" "${asset_root#${PROJECT_ROOT}/}"
-}
-
-
-configure_audio_imports() {
- local asset_root="$1"
- local import_file
- local updated=0
-
- if [[ ! -d "${asset_root}" ]]; then
- return
- fi
- while IFS= read -r -d '' import_file; do
- if ! grep -q '^importer="wav"$' "${import_file}"; then
- continue
- fi
- sed -i \
- -e 's|^force/mono=.*$|force/mono=true|' \
- -e 's|^force/max_rate=.*$|force/max_rate=true|' \
- -e "s|^force/max_rate_hz=.*$|force/max_rate_hz=${AUDIO_SAMPLE_RATE}|" \
- -e 's|^compress/mode=.*$|compress/mode=2|' \
- "${import_file}"
- updated=$((updated + 1))
- done < <(find "${asset_root}" -type f -name '*.wav.import' -print0)
- printf 'PortMaster audio profile %d Hz mono: %d imports under %s\n' \
- "${AUDIO_SAMPLE_RATE}" "${updated}" \
- "${asset_root#${PROJECT_ROOT}/}"
-}
-
-# The entire PortMaster project uses a deliberately small, VRAM-compressed
-# texture ceiling. Fonts are unaffected, preserving readable interface text
-# while raster artwork is reduced for one-gigabyte shared-memory devices.
-limit_texture_imports "${PROJECT_ROOT}" "${GENERAL_TEXTURE_LIMIT}"
-
-# These catalogs contain most of the authored high-resolution textures and
-# account for the largest runtime residency. At a 640x480 output, 64 pixels
-# remains sufficient for the deliberately low-fidelity light profile.
-for gameplay_root in \
- "art/exported/characters" \
- "art/exported/environment" \
- "art/exported/items" \
- "fish/species" \
- "gathering" \
- "items/icons" \
- "world"; do
- limit_texture_imports \
- "${PROJECT_ROOT}/${gameplay_root}" \
- "${GAMEPLAY_TEXTURE_LIMIT}"
-done
-
-# Short speech, call, and gameplay effects remain available, but use a small
-# mono QOA representation to reduce both package and resident sample memory.
-configure_audio_imports "${PROJECT_ROOT}/audio/sfx"
-configure_audio_imports "${PROJECT_ROOT}/sound/dialogue"
diff --git a/scripts/run_validations.sh b/scripts/run_validations.sh
deleted file mode 100755
index ff108a8..0000000
--- a/scripts/run_validations.sh
+++ /dev/null
@@ -1,441 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
-readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
-readonly GODOT_BIN="${GODOT_BIN:-godot}"
-readonly TEST_TIMEOUT_SECONDS="${TEST_TIMEOUT_SECONDS:-120}"
-readonly RUN_ROOT="$(mktemp -d -t straywild-validations.XXXXXX)"
-
-export straywild_ISOLATED_VALIDATION_STORAGE=1
-
-readonly -a QUICK_TESTS=(
- "scripts/validate_animalese_samples.gd"
- "tests/android_readiness_validation.gd"
- "tests/camera_drag_validation.gd"
- "tests/character_rig_validation.gd"
- "tests/network_chat_protocol_validation.gd"
- "tests/controller_focus_presentation_validation.gd"
- "tests/controller_menu_accessibility_validation.gd"
- "tests/controller_mapping_validation.gd"
- "tests/controller_world_interaction_validation.gd"
- "tests/controller_ui_navigation_validation.gd"
- "tests/dedicated_server_config_validation.gd"
- "tests/digging_prototype_validation.gd"
- "tests/equipment_catalog_validation.gd"
- "tests/exported_decal_hotfix_validation.gd"
- "tests/file_dialog_controller_navigation_validation.gd"
- "tests/fish_catalog_content_validation.gd"
- "tests/fish_quality_validation.gd"
- "tests/fishing_shop_controller_validation.gd"
- "tests/fishing_audio_validation.gd"
- "tests/fishing_surface_validation.gd"
- "tests/friend_relationship_validation.gd"
- "tests/fur_pattern_validation.gd"
- "tests/generated_world_runtime_validation.gd"
- "tests/gathering_marker_surface_validation.gd"
- "tests/inventory_storage_validation.gd"
- "tests/keyboard_mouse_mapping_validation.gd"
- "tests/logbook_validation.gd"
- "tests/network_player_animation_protocol_validation.gd"
- "tests/net_attachment_validation.gd"
- "tests/on_screen_keyboard_validation.gd"
- "tests/player_gathering_animation_validation.gd"
- "tests/player_experience_validation.gd"
- "tests/shoreline_ambience_validation.gd"
- "tests/surface_drawing_validation.gd"
- "tests/surface_drawing_texture_renderer_validation.gd"
- "tests/tackle_order_validation.gd"
- "tests/terrain_biome_validation.gd"
- "tests/terrain_blender_material_validation.gd"
- "tests/terrain_chunk_generator_validation.gd"
- "tests/texture_sampling_validation.gd"
- "tests/tree_gathering_prototype_validation.gd"
- "tests/unified_inventory_validation.gd"
- "tests/world_time_validation.gd"
- "tests/world_layout_validation.gd"
- "tests/world_spawn_protocol_validation.gd"
- "tests/world_weather_validation.gd"
-)
-
-readonly -a RUNTIME_TESTS=(
- "tests/inventory_notepad_art_validation.gd"
- "tests/light_performance_profile_validation.gd"
- "tests/logbook_runtime_validation.gd"
- "tests/player_experience_ui_validation.gd"
- "tests/title_credits_validation.gd"
- "tests/ui_scaling_runtime_validation.gd"
-)
-
-readonly -a HOST_TESTS=(
- "tests/art_tools_validation.gd"
- "tests/dedicated_host_session_validation.gd"
- "tests/economy_regression_validation.gd"
- "tests/fish_hotbar_showcase_validation.gd"
- "tests/fishing_authority_validation.gd"
- "tests/gathering_showcase_validation.gd"
- "tests/inventory_storage_persistence_validation.gd"
- "tests/job_system_validation.gd"
- "tests/progression_archive_validation.gd"
- "tests/surface_drawing_runtime_validation.gd"
-)
-
-readonly -a NETWORK_TESTS=(
- "tests/chat_privacy_multiplayer_validation.gd"
- "tests/economy_regression_validation.gd"
- "tests/fish_showcase_multiplayer_validation.gd"
- "tests/friend_multiplayer_validation.gd"
- "tests/fishing_multiplayer_validation.gd"
- "tests/home_spawn_multiplayer_validation.gd"
- "tests/job_multiplayer_validation.gd"
- "tests/movement_multiplayer_validation.gd"
- "tests/operator_multiplayer_validation.gd"
- "tests/profile_multiplayer_validation.gd"
- "tests/surface_drawing_multiplayer_validation.gd"
- "tests/world_time_multiplayer_validation.gd"
- "tests/world_layout_multiplayer_validation.gd"
- "tests/world_spawn_multiplayer_validation.gd"
-)
-
-readonly SESSION_SWITCH_NETWORK_TEST="tests/session_switch_multiplayer_validation.gd"
-readonly LATE_JOIN_NETWORK_TEST="tests/late_join_multiplayer_validation.gd"
-
-cleanup() {
- rm -rf -- "${RUN_ROOT}"
-}
-trap cleanup EXIT INT TERM
-
-usage() {
- printf \
- 'Usage: %s {quick|full|host|network|session-switch|late-join|all|--list}\n' \
- "$0"
-}
-
-prepare_root() {
- local name="$1"
- local root="${RUN_ROOT}/${name}"
- mkdir -p -- "${root}/data" "${root}/config" "${root}/cache"
- printf '%s\n' "${root}"
-}
-
-run_test() {
- local script="$1"
- local name="${script#tests/}"
- name="${name%.gd}"
- local root
- root="$(prepare_root "${name}")"
- printf '\n==> %s\n' "${script}"
- XDG_DATA_HOME="${root}/data" \
- XDG_CONFIG_HOME="${root}/config" \
- XDG_CACHE_HOME="${root}/cache" \
- timeout "${TEST_TIMEOUT_SECONDS}s" "${GODOT_BIN}" \
- --headless --path "${PROJECT_ROOT}" --script "${script}"
-}
-
-
-wait_for_network_host() {
- local script="$1"
- local host_pid="$2"
- local test_port="7777"
- local declared_port
- declared_port="$(
- sed -n -E \
- 's/^const TEST_PORT(: int)? = ([0-9]+)$/\2/p' \
- "${PROJECT_ROOT}/${script}"
- )"
- if [[ -n "${declared_port}" ]]; then
- test_port="${declared_port}"
- fi
- if ! command -v ss >/dev/null 2>&1; then
- sleep 2
- return 0
- fi
- # A cold generated-world startup can take longer than ten seconds before
- # NetworkSession is ready to bind. Keep the client launch tied to the actual
- # listener instead of treating that normal startup work as a host failure.
- for _attempt in {1..600}; do
- if ! kill -0 "${host_pid}" 2>/dev/null; then
- return 1
- fi
- if ss -H -lun "sport = :${test_port}" | grep -q .; then
- return 0
- fi
- sleep 0.1
- done
- return 1
-}
-
-
-run_network_test() {
- local script="$1"
- local name="${script#tests/}"
- name="${name%.gd}"
- local host_root client_root host_pid host_status client_status
- host_root="$(prepare_root "${name}-host")"
- client_root="$(prepare_root "${name}-client")"
- printf '\n==> %s (host + client)\n' "${script}"
- XDG_DATA_HOME="${host_root}/data" \
- XDG_CONFIG_HOME="${host_root}/config" \
- XDG_CACHE_HOME="${host_root}/cache" \
- timeout "${TEST_TIMEOUT_SECONDS}s" "${GODOT_BIN}" \
- --headless --path "${PROJECT_ROOT}" --script "${script}" -- host \
- >"${host_root}/output.log" 2>&1 &
- host_pid=$!
- if ! wait_for_network_host "${script}" "${host_pid}"; then
- set +e
- wait "${host_pid}"
- host_status=$?
- set -e
- printf '%s\n' '-- host output --'
- sed -n '1,240p' "${host_root}/output.log"
- printf 'error: host did not become ready (exit %d)\n' \
- "${host_status}" >&2
- return 1
- fi
- set +e
- XDG_DATA_HOME="${client_root}/data" \
- XDG_CONFIG_HOME="${client_root}/config" \
- XDG_CACHE_HOME="${client_root}/cache" \
- timeout "${TEST_TIMEOUT_SECONDS}s" "${GODOT_BIN}" \
- --headless --path "${PROJECT_ROOT}" --script "${script}" -- client \
- >"${client_root}/output.log" 2>&1
- client_status=$?
- wait "${host_pid}"
- host_status=$?
- set -e
- printf '%s\n' '-- host output --'
- sed -n '1,240p' "${host_root}/output.log"
- printf '%s\n' '-- client output --'
- sed -n '1,240p' "${client_root}/output.log"
- if ((host_status != 0 || client_status != 0)); then
- printf 'error: host exited %d; client exited %d\n' \
- "${host_status}" "${client_status}" >&2
- return 1
- fi
-}
-
-run_session_switch_network_test() {
- local script="${SESSION_SWITCH_NETWORK_TEST}"
- local name="${script#tests/}"
- name="${name%.gd}"
- local first_root second_root client_root
- local first_pid second_pid first_status second_status client_status
- local both_hosts_ready=0
- first_root="$(prepare_root "${name}-first-host")"
- second_root="$(prepare_root "${name}-second-host")"
- client_root="$(prepare_root "${name}-client")"
- printf '\n==> %s (two hosts + client)\n' "${script}"
- XDG_DATA_HOME="${first_root}/data" \
- XDG_CONFIG_HOME="${first_root}/config" \
- XDG_CACHE_HOME="${first_root}/cache" \
- timeout "${TEST_TIMEOUT_SECONDS}s" "${GODOT_BIN}" \
- --headless --path "${PROJECT_ROOT}" --script "${script}" -- first_host \
- >"${first_root}/output.log" 2>&1 &
- first_pid=$!
- XDG_DATA_HOME="${second_root}/data" \
- XDG_CONFIG_HOME="${second_root}/config" \
- XDG_CACHE_HOME="${second_root}/cache" \
- timeout "${TEST_TIMEOUT_SECONDS}s" "${GODOT_BIN}" \
- --headless --path "${PROJECT_ROOT}" --script "${script}" -- second_host \
- >"${second_root}/output.log" 2>&1 &
- second_pid=$!
- for _attempt in {1..600}; do
- if ! kill -0 "${first_pid}" 2>/dev/null \
- || ! kill -0 "${second_pid}" 2>/dev/null; then
- break
- fi
- if ! command -v ss >/dev/null 2>&1; then
- sleep 2
- both_hosts_ready=1
- break
- fi
- if ss -H -lun "sport = :18142" | grep -q . \
- && ss -H -lun "sport = :18143" | grep -q .; then
- both_hosts_ready=1
- break
- fi
- sleep 0.1
- done
- if ((both_hosts_ready == 0)); then
- kill "${first_pid}" "${second_pid}" 2>/dev/null || true
- wait "${first_pid}" 2>/dev/null || true
- wait "${second_pid}" 2>/dev/null || true
- printf '%s\n' '-- first host output --'
- sed -n '1,240p' "${first_root}/output.log"
- printf '%s\n' '-- second host output --'
- sed -n '1,240p' "${second_root}/output.log"
- printf '%s\n' 'error: session-switch hosts did not become ready' >&2
- return 1
- fi
- set +e
- XDG_DATA_HOME="${client_root}/data" \
- XDG_CONFIG_HOME="${client_root}/config" \
- XDG_CACHE_HOME="${client_root}/cache" \
- timeout "${TEST_TIMEOUT_SECONDS}s" "${GODOT_BIN}" \
- --headless --path "${PROJECT_ROOT}" --script "${script}" -- client \
- >"${client_root}/output.log" 2>&1
- client_status=$?
- wait "${first_pid}"
- first_status=$?
- wait "${second_pid}"
- second_status=$?
- set -e
- printf '%s\n' '-- first host output --'
- sed -n '1,240p' "${first_root}/output.log"
- printf '%s\n' '-- second host output --'
- sed -n '1,240p' "${second_root}/output.log"
- printf '%s\n' '-- client output --'
- sed -n '1,240p' "${client_root}/output.log"
- if ((first_status != 0 || second_status != 0 || client_status != 0)); then
- printf 'error: hosts exited %d/%d; client exited %d\n' \
- "${first_status}" "${second_status}" "${client_status}" >&2
- return 1
- fi
-}
-
-
-run_late_join_network_test() {
- local script="${LATE_JOIN_NETWORK_TEST}"
- local name="${script#tests/}"
- name="${name%.gd}"
- local host_root first_root late_root
- local host_pid first_pid late_pid host_status first_status late_status
- host_root="$(prepare_root "${name}-host")"
- first_root="$(prepare_root "${name}-first")"
- late_root="$(prepare_root "${name}-late")"
- printf '\n==> %s (host + first client + late client)\n' "${script}"
- XDG_DATA_HOME="${host_root}/data" \
- XDG_CONFIG_HOME="${host_root}/config" \
- XDG_CACHE_HOME="${host_root}/cache" \
- timeout "${TEST_TIMEOUT_SECONDS}s" "${GODOT_BIN}" \
- --headless --path "${PROJECT_ROOT}" --script "${script}" -- host \
- >"${host_root}/output.log" 2>&1 &
- host_pid=$!
- if ! wait_for_network_host "${script}" "${host_pid}"; then
- set +e
- wait "${host_pid}"
- host_status=$?
- set -e
- printf '%s\n' '-- host output --'
- sed -n '1,240p' "${host_root}/output.log"
- printf 'error: host did not become ready (exit %d)\n' \
- "${host_status}" >&2
- return 1
- fi
- XDG_DATA_HOME="${first_root}/data" \
- XDG_CONFIG_HOME="${first_root}/config" \
- XDG_CACHE_HOME="${first_root}/cache" \
- timeout "${TEST_TIMEOUT_SECONDS}s" "${GODOT_BIN}" \
- --headless --path "${PROJECT_ROOT}" --script "${script}" -- first \
- >"${first_root}/output.log" 2>&1 &
- first_pid=$!
- sleep 4
- XDG_DATA_HOME="${late_root}/data" \
- XDG_CONFIG_HOME="${late_root}/config" \
- XDG_CACHE_HOME="${late_root}/cache" \
- timeout "${TEST_TIMEOUT_SECONDS}s" "${GODOT_BIN}" \
- --headless --path "${PROJECT_ROOT}" --script "${script}" -- late \
- >"${late_root}/output.log" 2>&1 &
- late_pid=$!
- set +e
- wait "${late_pid}"
- late_status=$?
- wait "${first_pid}"
- first_status=$?
- wait "${host_pid}"
- host_status=$?
- set -e
- printf '%s\n' '-- host output --'
- sed -n '1,240p' "${host_root}/output.log"
- printf '%s\n' '-- first client output --'
- sed -n '1,240p' "${first_root}/output.log"
- printf '%s\n' '-- late client output --'
- sed -n '1,240p' "${late_root}/output.log"
- if ((host_status != 0 || first_status != 0 || late_status != 0)); then
- printf 'error: host exited %d; clients exited %d/%d\n' \
- "${host_status}" "${first_status}" "${late_status}" >&2
- return 1
- fi
-}
-
-run_quick() {
- local test_script
- for test_script in "${QUICK_TESTS[@]}"; do
- run_test "${test_script}"
- done
-}
-
-run_full() {
- local test_script
- run_quick
- for test_script in "${RUNTIME_TESTS[@]}"; do
- run_test "${test_script}"
- done
-}
-
-run_host() {
- local test_script
- for test_script in "${HOST_TESTS[@]}"; do
- run_test "${test_script}"
- done
-}
-
-run_network() {
- local test_script
- for test_script in "${NETWORK_TESTS[@]}"; do
- run_network_test "${test_script}"
- done
- run_late_join_network_test
- run_session_switch_network_test
-}
-
-list_tests() {
- printf '%s\n' 'Quick tests:'
- printf ' %s\n' "${QUICK_TESTS[@]}"
- printf '%s\n' 'Additional full-suite tests:'
- printf ' %s\n' "${RUNTIME_TESTS[@]}"
- printf '%s\n' 'Single-process tests requiring a local UDP bind:'
- printf ' %s\n' "${HOST_TESTS[@]}"
- printf '%s\n' 'Paired network tests:'
- printf ' %s\n' "${NETWORK_TESTS[@]}"
- printf '%s\n' 'Two-host session-switch network test:'
- printf ' %s\n' "${SESSION_SWITCH_NETWORK_TEST}"
- printf '%s\n' 'Three-peer late-join network test:'
- printf ' %s\n' "${LATE_JOIN_NETWORK_TEST}"
-}
-
-case "${1:-}" in
- quick)
- run_quick
- ;;
- full)
- run_full
- ;;
- host)
- run_host
- ;;
- network)
- run_network
- ;;
- session-switch)
- run_session_switch_network_test
- ;;
- late-join)
- run_late_join_network_test
- ;;
- all)
- run_full
- run_host
- run_network
- ;;
- --list)
- list_tests
- ;;
- *)
- usage >&2
- exit 2
- ;;
-esac
-
-printf '\nAll requested validations passed.\n'
diff --git a/scripts/setup_android_build.sh b/scripts/setup_android_build.sh
deleted file mode 100755
index bc96083..0000000
--- a/scripts/setup_android_build.sh
+++ /dev/null
@@ -1,394 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-ANDROID_HOME="${ANDROID_HOME:-${HOME}/Android/Sdk}"
-JAVA_HOME="${JAVA_HOME:-}"
-INSTALL=0
-AUTO=0
-CMDLINE_TOOLS_ZIP="https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip"
-SDK_VERSION="33"
-BUILDTOOLS_VERSION="34.0.0"
-
-usage() {
- cat <&2
- usage
- exit 2 ;;
- esac
-done
-
-PROJECT_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
-GODOT="${GODOT_BIN:-godot}"
-PROJECT_VERSION="$(
- sed -n 's/^config\/version="\([^"]*\)"/\1/p' "${PROJECT_ROOT}/project.godot"
-)"
-MISSING=0
-
-print_status() { printf '%s %s\n' "[$1]" "$2"; }
-
-check_dir() {
- local path="$1" label="$2"
- if [[ -d "$path" ]]; then
- print_status OK "${label}: ${path}"
- else
- print_status ERR "Missing ${label}: ${path}"
- MISSING=$((MISSING+1))
- fi
-}
-
-check_file() {
- local path="$1" label="$2"
- if [[ -x "$path" ]]; then
- print_status OK "${label}: ${path}"
- else
- print_status ERR "Missing ${label}: ${path}"
- MISSING=$((MISSING+1))
- fi
-}
-
-resolve_java_home() {
- local candidate=""
- if [[ -n "$JAVA_HOME" && -x "$JAVA_HOME/bin/java" ]]; then
- return 0
- fi
-
- if command -v java >/dev/null 2>&1; then
- candidate="$(readlink -f "$(command -v java)")"
- candidate="$(dirname "$(dirname "$candidate")")"
- if [[ -x "$candidate/bin/java" ]]; then
- JAVA_HOME="$candidate"
- return 0
- fi
- fi
-
- # Common Ubuntu/Debian/JDK locations
- for p in \
- /usr/lib/jvm/default-java \
- /usr/lib/jvm/java-17-openjdk-amd64 \
- /usr/lib/jvm/java-17-openjdk \
- /usr/lib/jvm/jdk-17* \
- /usr/lib/jvm/temurin-17*;
- do
- if [[ -e $p && -x "$p/bin/java" ]]; then
- JAVA_HOME="$p"
- return 0
- fi
- done
- return 1
-}
-
-check_java() {
- if resolve_java_home && [[ -x "$JAVA_HOME/bin/java" ]]; then
- print_status OK "Java binary: $JAVA_HOME/bin/java"
- check_file "$JAVA_HOME/bin/javac" "javac binary"
- check_file "$JAVA_HOME/bin/keytool" "keytool binary"
- "$JAVA_HOME/bin/java" -version 2>&1 | sed -n '1,2p'
- local java_ver
- java_ver=$("$JAVA_HOME/bin/java" -version 2>&1 | sed -n '1p')
- if [[ "$java_ver" =~ \"([0-9]+)\\. ]]; then
- local java_major="${BASH_REMATCH[1]}"
- if [[ "$java_major" != "17" ]]; then
- print_status WARN "Detected Java $java_major. Godot Android export is commonly validated with JDK 17."
- fi
- fi
- else
- print_status ERR "Java not found. Set JAVA_HOME to a JDK 17+ home or install one."
- MISSING=$((MISSING+1))
- JAVA_HOME=""
- fi
-}
-
-find_sdkmanager() {
- if [[ -x "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" ]]; then
- echo "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager"
- return
- fi
- if [[ -x "$ANDROID_HOME/cmdline-tools/bin/sdkmanager" ]]; then
- echo "$ANDROID_HOME/cmdline-tools/bin/sdkmanager"; return
- fi
- if [[ -x "$ANDROID_HOME/tools/bin/sdkmanager" ]]; then
- echo "$ANDROID_HOME/tools/bin/sdkmanager"; return
- fi
- if command -v sdkmanager >/dev/null 2>&1; then
- command -v sdkmanager
- return
- fi
- echo ""
-}
-
-sdkmanager_path="$(find_sdkmanager)"
-TMP_CMDLINE_TOOLS_DIR=""
-
-cleanup_tmpdir() {
- [[ -n "${TMP_CMDLINE_TOOLS_DIR:-}" && -d "$TMP_CMDLINE_TOOLS_DIR" ]] && rm -rf "$TMP_CMDLINE_TOOLS_DIR"
- TMP_CMDLINE_TOOLS_DIR=""
-}
-
-check_sdk_paths() {
- print_status INFO "ANDROID_HOME: ${ANDROID_HOME}"
- check_dir "$ANDROID_HOME" "Android SDK root"
- check_dir "$ANDROID_HOME/platform-tools" "platform-tools"
- check_file "$ANDROID_HOME/platform-tools/adb" "adb binary"
-
- if [[ -d "$ANDROID_HOME/build-tools/$BUILDTOOLS_VERSION" ]]; then
- print_status OK "build-tools ${BUILDTOOLS_VERSION}: present"
- check_file \
- "$ANDROID_HOME/build-tools/$BUILDTOOLS_VERSION/apksigner" \
- "apksigner"
- else
- print_status ERR "Missing build-tools version: ${BUILDTOOLS_VERSION}"
- MISSING=$((MISSING+1))
- fi
-
- if [[ -d "$ANDROID_HOME/platforms" ]]; then
- if ls "$ANDROID_HOME/platforms/android-${SDK_VERSION}" >/dev/null 2>&1; then
- print_status OK "platform ${SDK_VERSION}: present"
- else
- print_status ERR "Missing Android platform: android-${SDK_VERSION}"
- MISSING=$((MISSING+1))
- fi
- else
- print_status ERR "Missing platforms directory: $ANDROID_HOME/platforms"
- MISSING=$((MISSING+1))
- fi
-
- if [[ -x "$sdkmanager_path" ]]; then
- print_status OK "sdkmanager: ${sdkmanager_path}"
- else
- print_status ERR "sdkmanager not found in SDK or PATH"
- MISSING=$((MISSING+1))
- fi
-}
-
-install_jdk_hint() {
- local pm=0
- local -a elevate=()
- echo "Attempting automatic JDK install..."
- if command -v apt-get >/dev/null 2>&1; then
- pm="apt-get"
- elif command -v dnf >/dev/null 2>&1; then
- pm="dnf"
- elif command -v pacman >/dev/null 2>&1; then
- pm="pacman"
- else
- echo "No supported package manager found (apt-get/dnf/pacman)."
- echo "Install JDK 17 manually, then rerun with --java-home set."
- return 1
- fi
-
- if [[ $EUID -ne 0 ]]; then
- if ! command -v sudo >/dev/null 2>&1; then
- echo "sudo not found; please run with root privileges for package install."
- return 1
- fi
- elevate=(sudo)
- fi
-
- if [[ "$pm" == "apt-get" ]]; then
- echo "apt-get install -y openjdk-17-jdk"
- if [[ $AUTO -eq 1 ]]; then
- "${elevate[@]}" apt-get update
- "${elevate[@]}" apt-get install -y openjdk-17-jdk
- else
- echo "Run as manual step first: sudo apt-get update && sudo apt-get install -y openjdk-17-jdk"
- return 1
- fi
- elif [[ "$pm" == "dnf" ]]; then
- echo "dnf install -y java-17-openjdk-devel"
- if [[ $AUTO -eq 1 ]]; then
- "${elevate[@]}" dnf install -y java-17-openjdk-devel
- else
- return 1
- fi
- else
- echo "pacman -S --noconfirm jdk17-openjdk"
- if [[ $AUTO -eq 1 ]]; then
- "${elevate[@]}" pacman -S --noconfirm jdk17-openjdk
- else
- return 1
- fi
- fi
-}
-
-install_cmdline_tools() {
- if [[ -x "$sdkmanager_path" ]]; then
- return 0
- fi
-
- echo "Downloading Android cmdline-tools to bootstrap sdkmanager..."
- TMP_CMDLINE_TOOLS_DIR=""
- TMP_CMDLINE_TOOLS_DIR="$(mktemp -d)"
- if [[ -z "$TMP_CMDLINE_TOOLS_DIR" ]]; then
- echo "Failed to create temporary directory."
- return 1
- fi
- trap cleanup_tmpdir RETURN
-
- if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then
- echo "curl/wget required to download cmdline-tools. Install one and rerun."
- return 1
- fi
-
- local zipfile="${TMP_CMDLINE_TOOLS_DIR}/cmdline-tools.zip"
- if command -v curl >/dev/null 2>&1; then
- if ! curl -fL "$CMDLINE_TOOLS_ZIP" -o "$zipfile"; then
- echo "Failed to download cmdline-tools from $CMDLINE_TOOLS_ZIP"
- return 1
- fi
- else
- if ! wget -q "$CMDLINE_TOOLS_ZIP" -O "$zipfile"; then
- echo "Failed to download cmdline-tools from $CMDLINE_TOOLS_ZIP"
- return 1
- fi
- fi
-
- if [[ ! -s "$zipfile" ]]; then
- echo "Downloaded cmdline-tools archive is empty."
- return 1
- fi
- unzip -q "$zipfile" -d "$TMP_CMDLINE_TOOLS_DIR"
- if [[ ! -d "$TMP_CMDLINE_TOOLS_DIR/cmdline-tools" ]]; then
- echo "Downloaded archive did not contain expected cmdline-tools payload."
- return 1
- fi
- mkdir -p "$ANDROID_HOME" || {
- echo "Cannot create Android SDK root: $ANDROID_HOME"
- return 1
- }
- mkdir -p "$ANDROID_HOME/cmdline-tools"
- rm -rf "$ANDROID_HOME/cmdline-tools/latest"
- mkdir -p "$ANDROID_HOME/cmdline-tools/latest"
- mv "$TMP_CMDLINE_TOOLS_DIR/cmdline-tools"/* "$ANDROID_HOME/cmdline-tools/latest/"
- echo "Installed SDK cmdline-tools to $ANDROID_HOME/cmdline-tools/latest"
- sdkmanager_path="$(find_sdkmanager)"
- return 0
-}
-
-install_sdk_components() {
- if [[ -z "$sdkmanager_path" ]]; then
- echo "Cannot install SDK packages without sdkmanager."
- return 1
- fi
- "$sdkmanager_path" --sdk_root="${ANDROID_HOME}" --list | sed -n '1,40p'
- echo "Installing required SDK components: platform-tools, platform-android-${SDK_VERSION}, build-tools-${BUILDTOOLS_VERSION}"
- if [[ $AUTO -eq 1 ]]; then
- yes | "$sdkmanager_path" --sdk_root="${ANDROID_HOME}" --install "platform-tools" "platforms;android-${SDK_VERSION}" "build-tools;${BUILDTOOLS_VERSION}"
- else
- echo "Run this manually after accepting licenses:"
- echo " yes | \"${sdkmanager_path}\" --sdk_root=\"${ANDROID_HOME}\" --install \"platform-tools\" \"platforms;android-${SDK_VERSION}\" \"build-tools;${BUILDTOOLS_VERSION}\""
- return 1
- fi
-}
-
-check_editor_settings() {
- local editor_settings="$HOME/.config/godot/editor_settings-4.7.tres"
- print_status INFO "Checking editor settings: $editor_settings"
- if [[ -f "$editor_settings" ]]; then
- if rg -n 'export/android/android_sdk_path|export/android/java_sdk_path' "$editor_settings" >/dev/null 2>&1; then
- rg -n 'export/android/android_sdk_path|export/android/java_sdk_path' "$editor_settings"
- else
- print_status WARN "Editor export settings entries are missing for Android in this file"
- echo " Set in editor: Export > Presets > Android:"
- echo " android_sdk_path = ${ANDROID_HOME}"
- echo " java_sdk_path = ${JAVA_HOME}"
- fi
- else
- print_status WARN "No editor settings file found at expected path: $editor_settings"
- fi
-}
-
-run_export_hint() {
- echo
- echo "Example export commands:"
- echo " ${GODOT} --headless --path \"$PROJECT_ROOT\" --export-release \"Android\" builds/${PROJECT_VERSION}/android/straywild.apk"
- echo " ${GODOT} --headless --path \"$PROJECT_ROOT\" --export-debug \"Android\" builds/android/straywild-debug.apk"
-}
-
-print_status INFO "Starting Android dependency preflight..."
-check_java
-check_sdk_paths
-check_editor_settings
-
-if [[ $MISSING -eq 0 ]]; then
- print_status OK "Dependency check passed."
- run_export_hint
- exit 0
-fi
-
-if [[ $INSTALL -eq 1 ]]; then
- echo
- print_status INFO "Attempting dependency install path..."
- if [[ \
- -z "$JAVA_HOME" \
- || ! -x "$JAVA_HOME/bin/java" \
- || ! -x "$JAVA_HOME/bin/javac" \
- || ! -x "$JAVA_HOME/bin/keytool" \
- ]]; then
- install_jdk_hint || true
- fi
-
- resolve_java_home || true
- check_java
-
- install_cmdline_tools || true
- sdkmanager_path="$(find_sdkmanager)"
- install_sdk_components || true
-
- MISSING=0
- check_java
- check_sdk_paths
- if [[ $MISSING -eq 0 ]]; then
- echo
- print_status OK "Dependencies fixed."
- run_export_hint
- exit 0
- fi
-fi
-
-print_status ERR "Dependency check failed with ${MISSING} issue(s)."
-cat < void:
- var root_to_create: String = OS.get_environment("straywild_TEST_CREATE_ROOT")
- if not root_to_create.is_empty():
- _create_validation_root(root_to_create)
- return
- var failures: PackedStringArray = []
- _expect(
- "external temporary directory",
- "/tmp/straywild-test-data",
- PlayerDataRoot.PATH_ALLOWED,
- failures,
- )
- _expect(
- "external trailing slash",
- "/tmp/straywild-test-data/",
- PlayerDataRoot.PATH_ALLOWED,
- failures,
- )
- _expect(
- "similar project-name sibling",
- "/tmp/straywild-data",
- PlayerDataRoot.PATH_ALLOWED,
- failures,
- )
- _expect(
- "documents directory",
- "/home/player/Documents/straywild",
- PlayerDataRoot.PATH_ALLOWED,
- failures,
- )
- _expect("source project", PROJECT_PATH, PlayerDataRoot.PATH_SOURCE_PROJECT, failures)
- _expect(
- "source project child",
- PROJECT_PATH.path_join("player-data"),
- PlayerDataRoot.PATH_SOURCE_PROJECT,
- failures,
- )
- _expect(
- "installation directory",
- INSTALL_PATH,
- PlayerDataRoot.PATH_INSTALLATION,
- failures,
- )
- _expect(
- "installation child",
- INSTALL_PATH.path_join("portable-data"),
- PlayerDataRoot.PATH_INSTALLATION,
- failures,
- )
- _expect_with_references(
- "empty project reference",
- "/tmp/straywild-test-data",
- "",
- INSTALL_PATH,
- PlayerDataRoot.PATH_ALLOWED,
- failures,
- )
- _expect_with_references(
- "root project reference",
- "/tmp/straywild-test-data",
- "/",
- INSTALL_PATH,
- PlayerDataRoot.PATH_ALLOWED,
- failures,
- )
- _expect_with_references(
- "Windows component and case normalization",
- "C:\\Games\\straywild Data",
- "C:\\Games\\straywild",
- "C:\\Program Files\\straywild",
- PlayerDataRoot.PATH_ALLOWED,
- failures,
- true,
- )
- if failures.is_empty():
- print("PlayerDataRoot path validation: PASS")
- quit(0)
- return
- for failure: String in failures:
- push_error(failure)
- quit(1)
-
-
-func _create_validation_root(path: String) -> void:
- var data_root := PlayerDataRoot.new()
- get_root().add_child(data_root)
- if data_root.select_new_root(path):
- print("PlayerDataRoot validation root created: ", data_root.root_path)
- quit(0)
- return
- push_error(data_root.error_message)
- quit(1)
-
-
-func _expect(
- label: String,
- candidate: String,
- expected: StringName,
- failures: PackedStringArray,
-) -> void:
- _expect_with_references(
- label,
- candidate,
- PROJECT_PATH,
- INSTALL_PATH,
- expected,
- failures,
- )
-
-
-func _expect_with_references(
- label: String,
- candidate: String,
- project: String,
- installation: String,
- expected: StringName,
- failures: PackedStringArray,
- case_insensitive: bool = false,
-) -> void:
- var actual: StringName = PlayerDataRoot.classify_candidate_path(
- candidate, project, installation, case_insensitive
- )
- if actual != expected:
- failures.append("%s: expected %s, got %s" % [label, expected, actual])
diff --git a/scripts/test_player_data_root_paths.gd.uid b/scripts/test_player_data_root_paths.gd.uid
deleted file mode 100644
index 90f8e0b..0000000
--- a/scripts/test_player_data_root_paths.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://e8glhlm205au
diff --git a/scripts/validate_animalese_samples.gd b/scripts/validate_animalese_samples.gd
deleted file mode 100644
index a4e7502..0000000
--- a/scripts/validate_animalese_samples.gd
+++ /dev/null
@@ -1,73 +0,0 @@
-extends SceneTree
-
-const AnimaleseVoiceType = preload("res://ui/animalese_voice.gd")
-const VoiceProfilesType = preload(
- "res://player/animalese_voice_profiles.gd"
-)
-const EXPECTED_CHARACTERS := "abcdefghijklmnopqrstuvwxyz0123456789"
-const ROBOT_CHARACTERS := "abcdefghijklmnopqrstuvwxyz"
-
-var _failures: Array[String] = []
-
-
-func _initialize() -> void:
- _expect(
- AnimaleseVoiceType.SUPPORTED_CHARACTERS == EXPECTED_CHARACTERS,
- "animalese supports every letter and digit",
- )
- _expect(
- VoiceProfilesType.DEFAULT_SAMPLE_SET_ID == "kat",
- "kat is the default animalese voice set",
- )
- _expect(
- VoiceProfilesType.is_valid_sample_set("robot"),
- "the original tones remain available as robot",
- )
- _expect(
- VoiceProfilesType.is_valid_sample_set("kim"),
- "kim is available as an animalese voice set",
- )
- _validate_set("kat", EXPECTED_CHARACTERS, false)
- _validate_set("robot", ROBOT_CHARACTERS, true)
- _validate_set("kim", EXPECTED_CHARACTERS, false)
- if _failures.is_empty():
- print("Animalese sample validation: PASS")
- quit(0)
- return
- for failure: String in _failures:
- push_error(failure)
- quit(1)
-
-
-func _validate_set(
- sample_set_id: String,
- characters: String,
- expects_fallback: bool,
-) -> void:
- var sample_directory := VoiceProfilesType.sample_directory_for(
- sample_set_id
- )
- _expect(
- not sample_directory.is_empty(),
- "%s has a sample directory" % sample_set_id,
- )
- for character_index: int in range(characters.length()):
- var character := characters.substr(character_index, 1)
- var sample_path := "%s/%s.wav" % [sample_directory, character]
- _expect(
- ResourceLoader.exists(sample_path, "AudioStreamWAV"),
- "%s sample loads for %s" % [sample_set_id, character],
- )
- if expects_fallback:
- _expect(
- ResourceLoader.exists(
- "%s/fallback.wav" % sample_directory,
- "AudioStreamWAV",
- ),
- "%s supplies a fallback for digits and symbols" % sample_set_id,
- )
-
-
-func _expect(condition: bool, message: String) -> void:
- if not condition:
- _failures.append(message)
diff --git a/scripts/validate_animalese_samples.gd.uid b/scripts/validate_animalese_samples.gd.uid
deleted file mode 100644
index 9399a64..0000000
--- a/scripts/validate_animalese_samples.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cwqm75icgcr4u
diff --git a/server/dedicated_server_config.gd b/server/dedicated_server_config.gd
deleted file mode 100644
index 3370457..0000000
--- a/server/dedicated_server_config.gd
+++ /dev/null
@@ -1,293 +0,0 @@
-class_name DedicatedServerConfig
-extends RefCounted
-
-const DEFAULT_NAME: String = "straywild Dedicated Server"
-const DEFAULT_BIND_ADDRESS: String = "*"
-const DEFAULT_PORT: int = 7777
-const DEFAULT_MAX_PLAYERS: int = 8
-const DEFAULT_WORLD_SEED: int = 13001
-const MAX_WORLD_SEED: int = 2147483646
-const MAX_OPERATORS: int = 64
-
-var server_name: String = DEFAULT_NAME
-var bind_address: String = DEFAULT_BIND_ADDRESS
-var port: int = DEFAULT_PORT
-var max_players: int = DEFAULT_MAX_PLAYERS
-var world_seed: int = DEFAULT_WORLD_SEED
-var public_listing: bool = false
-var discovery_url: String = ""
-var data_directory: String = ""
-var chat_logging: bool = false
-var chat_log_path: String = ""
-var operator_fingerprints: PackedStringArray = PackedStringArray()
-var error_message: String = ""
-
-
-static func from_runtime() -> DedicatedServerConfig:
- var result := DedicatedServerConfig.new()
- result.data_directory = ProjectSettings.globalize_path(
- "user://dedicated-server-data"
- )
- result.discovery_url = str(ProjectSettings.get_setting(
- "network/discovery/base_url", ""
- )).strip_edges()
- var config_path: String = _environment_string(
- "straywild_SERVER_CONFIG",
- "NETFISHING_SERVER_CONFIG",
- "",
- ).strip_edges()
- for argument: String in OS.get_cmdline_user_args():
- if argument.begins_with("--config="):
- config_path = argument.trim_prefix("--config=").strip_edges()
- if not config_path.is_empty() and not result._load_file(config_path):
- return result
- result._apply_environment()
- result._apply_arguments(OS.get_cmdline_user_args())
- result._validate()
- return result
-
-
-func is_valid() -> bool:
- return error_message.is_empty()
-
-
-func _load_file(path: String) -> bool:
- var file := ConfigFile.new()
- var error: Error = file.load(path)
- if error != OK:
- error_message = "Could not load server configuration: %s" % path
- return false
- server_name = str(file.get_value("server", "name", server_name))
- bind_address = str(file.get_value(
- "server", "bind_address", bind_address
- ))
- port = int(file.get_value("server", "port", port))
- max_players = int(file.get_value(
- "server", "max_players", max_players
- ))
- world_seed = int(file.get_value("world", "seed", world_seed))
- public_listing = bool(file.get_value(
- "server", "public", public_listing
- ))
- data_directory = str(file.get_value(
- "server", "data_directory", data_directory
- ))
- discovery_url = str(file.get_value(
- "discovery", "url", discovery_url
- ))
- chat_logging = bool(file.get_value(
- "privacy", "chat_logging", chat_logging
- ))
- chat_log_path = str(file.get_value(
- "privacy", "chat_log_path", chat_log_path
- ))
- operator_fingerprints = _parse_fingerprint_list(file.get_value(
- "moderation", "operators", operator_fingerprints
- ))
- return true
-
-
-func _apply_environment() -> void:
- server_name = _environment_string(
- "straywild_SERVER_NAME", "NETFISHING_SERVER_NAME", server_name
- )
- bind_address = _environment_string(
- "straywild_SERVER_BIND", "NETFISHING_SERVER_BIND", bind_address
- )
- port = _environment_int(
- "straywild_SERVER_PORT", "NETFISHING_SERVER_PORT", port
- )
- max_players = _environment_int(
- "straywild_SERVER_MAX_PLAYERS",
- "NETFISHING_SERVER_MAX_PLAYERS",
- max_players,
- )
- world_seed = _environment_int(
- "straywild_WORLD_SEED", "NETFISHING_WORLD_SEED", world_seed
- )
- public_listing = _environment_bool(
- "straywild_SERVER_PUBLIC", "NETFISHING_SERVER_PUBLIC", public_listing
- )
- discovery_url = _environment_string(
- "straywild_DISCOVERY_URL", "NETFISHING_DISCOVERY_URL", discovery_url
- )
- data_directory = _environment_string(
- "straywild_DATA_DIR", "NETFISHING_DATA_DIR", data_directory
- )
- chat_logging = _environment_bool(
- "straywild_CHAT_LOGGING", "NETFISHING_CHAT_LOGGING", chat_logging
- )
- chat_log_path = _environment_string(
- "straywild_CHAT_LOG_PATH", "NETFISHING_CHAT_LOG_PATH", chat_log_path
- )
- var operators_variable: String = _available_environment_variable(
- "straywild_SERVER_OPERATORS",
- "NETFISHING_SERVER_OPERATORS",
- )
- if not operators_variable.is_empty():
- operator_fingerprints = _parse_fingerprint_list(
- OS.get_environment(operators_variable)
- )
-
-
-func _apply_arguments(arguments: PackedStringArray) -> void:
- for argument: String in arguments:
- if argument.begins_with("--name="):
- server_name = argument.trim_prefix("--name=")
- elif argument.begins_with("--bind="):
- bind_address = argument.trim_prefix("--bind=")
- elif argument.begins_with("--port="):
- port = _parse_int(argument.trim_prefix("--port="), port)
- elif argument.begins_with("--max-players="):
- max_players = _parse_int(
- argument.trim_prefix("--max-players="), max_players
- )
- elif argument.begins_with("--world-seed="):
- world_seed = _parse_int(
- argument.trim_prefix("--world-seed="), world_seed
- )
- elif argument.begins_with("--data-dir="):
- data_directory = argument.trim_prefix("--data-dir=")
- elif argument.begins_with("--discovery-url="):
- discovery_url = argument.trim_prefix("--discovery-url=")
- elif argument.begins_with("--operators="):
- operator_fingerprints = _parse_fingerprint_list(
- argument.trim_prefix("--operators=")
- )
- elif argument.begins_with("--chat-log-path="):
- chat_log_path = argument.trim_prefix("--chat-log-path=")
- elif argument == "--public":
- public_listing = true
- elif argument == "--private":
- public_listing = false
- elif argument == "--chat-logging":
- chat_logging = true
- elif argument == "--no-chat-logging":
- chat_logging = false
-
-
-func _validate() -> void:
- server_name = server_name.strip_edges().left(48)
- bind_address = bind_address.strip_edges()
- discovery_url = discovery_url.strip_edges().trim_suffix("/")
- data_directory = data_directory.strip_edges()
- chat_log_path = chat_log_path.strip_edges()
- if chat_logging and chat_log_path.is_empty() and not data_directory.is_empty():
- chat_log_path = data_directory.path_join("logs/chat.jsonl")
- operator_fingerprints = _normalized_fingerprints(operator_fingerprints)
- if server_name.is_empty():
- error_message = "Server name cannot be empty."
- elif not _safe_text(server_name):
- error_message = "Server name contains unsupported characters."
- elif bind_address.is_empty():
- error_message = "Bind address cannot be empty."
- elif port < 1 or port > 65535:
- error_message = "Server port must be between 1 and 65535."
- elif max_players < 1 or max_players > 128:
- error_message = "Maximum players must be between 1 and 128."
- elif world_seed <= 0 or world_seed > MAX_WORLD_SEED:
- error_message = "World seed must be between 1 and %d." % MAX_WORLD_SEED
- elif not data_directory.is_empty() and not data_directory.is_absolute_path():
- error_message = "Server data directory must be an absolute path."
- elif chat_logging and (
- chat_log_path.is_empty() or not chat_log_path.is_absolute_path()
- ):
- error_message = (
- "Chat logging requires an absolute chat log path."
- )
- elif public_listing and not (
- discovery_url.begins_with("https://")
- or discovery_url.begins_with("http://")
- ):
- error_message = "A public server requires a discovery URL."
- elif operator_fingerprints.size() > MAX_OPERATORS:
- error_message = "A server may configure at most %d operators." % (
- MAX_OPERATORS
- )
- else:
- for fingerprint: String in operator_fingerprints:
- if not NetworkIdentityCrypto.valid_fingerprint(fingerprint):
- error_message = (
- "Server operator fingerprints must be 64 lowercase hex characters."
- )
- break
-
-
-static func _safe_text(value: String) -> bool:
- for index: int in value.length():
- var codepoint: int = value.unicode_at(index)
- if codepoint < 32 or codepoint == 127:
- return false
- return true
-
-
-static func _available_environment_variable(
- name: String,
- legacy_name: String,
-) -> String:
- if OS.has_environment(name):
- return name
- return legacy_name if OS.has_environment(legacy_name) else ""
-
-
-static func _environment_string(
- name: String,
- legacy_name: String,
- fallback: String,
-) -> String:
- var variable: String = _available_environment_variable(name, legacy_name)
- return OS.get_environment(variable) if not variable.is_empty() else fallback
-
-
-static func _environment_int(
- name: String,
- legacy_name: String,
- fallback: int,
-) -> int:
- var variable: String = _available_environment_variable(name, legacy_name)
- return (
- _parse_int(OS.get_environment(variable), fallback)
- if not variable.is_empty() else fallback
- )
-
-
-static func _environment_bool(
- name: String,
- legacy_name: String,
- fallback: bool,
-) -> bool:
- var variable: String = _available_environment_variable(name, legacy_name)
- if variable.is_empty():
- return fallback
- var value: String = OS.get_environment(variable).strip_edges().to_lower()
- return value in ["1", "true", "yes", "on"]
-
-
-static func _parse_int(value: String, fallback: int) -> int:
- return int(value) if value.strip_edges().is_valid_int() else fallback
-
-
-static func _parse_fingerprint_list(value: Variant) -> PackedStringArray:
- var values: PackedStringArray = PackedStringArray()
- if typeof(value) == TYPE_STRING:
- var text: String = str(value)
- for separator: String in [";", "\n", "\r", "\t", " "]:
- text = text.replace(separator, ",")
- values = text.split(",", false)
- elif typeof(value) in [TYPE_ARRAY, TYPE_PACKED_STRING_ARRAY]:
- for entry: Variant in value:
- if typeof(entry) in [TYPE_STRING, TYPE_STRING_NAME]:
- values.append(str(entry))
- return _normalized_fingerprints(values)
-
-
-static func _normalized_fingerprints(
- values: PackedStringArray,
-) -> PackedStringArray:
- var result: PackedStringArray = PackedStringArray()
- for value: String in values:
- var fingerprint: String = value.strip_edges().to_lower()
- if not fingerprint.is_empty() and fingerprint not in result:
- result.append(fingerprint)
- result.sort()
- return result
diff --git a/server/dedicated_server_config.gd.uid b/server/dedicated_server_config.gd.uid
deleted file mode 100644
index 720cccc..0000000
--- a/server/dedicated_server_config.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://nmg477qkcbhe
diff --git a/settings/controller_mapping_manager.gd b/settings/controller_mapping_manager.gd
deleted file mode 100644
index 278022e..0000000
--- a/settings/controller_mapping_manager.gd
+++ /dev/null
@@ -1,1037 +0,0 @@
-class_name ControllerMappingManager
-extends Node
-
-signal active_profile_changed
-signal active_controller_changed(controller_name: String)
-signal controller_input_observed(event: InputEvent)
-
-const FORMAT_VERSION: int = 1
-const PROFILE_PATH: String = "user://controller_mappings.json"
-const PROFILE_TEMP_PATH: String = "user://controller_mappings.json.tmp"
-const PROFILE_BACKUP_PATH: String = "user://controller_mappings.json.backup"
-const MAX_PROFILE_BYTES: int = 1024 * 1024
-const CAPTURE_AXIS_THRESHOLD: float = 0.55
-const CAPTURE_AXIS_RELEASE_THRESHOLD: float = 0.30
-const ACTIVE_DEVICE_AXIS_THRESHOLD: float = 0.35
-const MUOS_MAPPING_REVISION: String = "muos-v4"
-const MUOS_CONTROLLER_NAMES: Array[String] = [
- "muOS-Keys",
- "Deeplay-keys",
-]
-const MUOS_MAPPING_BINDINGS: String = (
- "a:b0,b:b1,x:b3,y:b2,"
- + "leftshoulder:b4,rightshoulder:b5,"
- + "lefttrigger:b10,righttrigger:b11,"
- + "guide:b8,start:b7,back:b6,"
- + "dpup:h0.1,dpleft:h0.8,dpright:h0.2,dpdown:h0.4,"
- + "leftx:a0,lefty:a1,leftstick:b9,"
- + "rightx:a2,righty:a3,rightstick:b12,platform:Linux,"
-)
-const ROLE_A: StringName = &"a"
-const ROLE_B: StringName = &"b"
-const ROLE_X: StringName = &"x"
-const ROLE_Y: StringName = &"y"
-const ROLE_LB: StringName = &"lb"
-const ROLE_RB: StringName = &"rb"
-# Keep the serialized keys for profile compatibility, but name the roles after
-# their game actions. A handheld may expose either action as a button, a
-# dedicated trigger axis, or one half of a shared axis.
-const ROLE_POINTER_MODIFIER: StringName = &"lt"
-const ROLE_CAMERA_ZOOM: StringName = &"rt"
-const ROLE_LT: StringName = ROLE_POINTER_MODIFIER
-const ROLE_RT: StringName = ROLE_CAMERA_ZOOM
-const ROLE_SELECT: StringName = &"select"
-const ROLE_START: StringName = &"start"
-const ROLE_LEFT_STICK_CLICK: StringName = &"left_stick_click"
-const ROLE_RIGHT_STICK_CLICK: StringName = &"right_stick_click"
-const ROLE_DPAD_UP: StringName = &"dpad_up"
-const ROLE_DPAD_DOWN: StringName = &"dpad_down"
-const ROLE_DPAD_LEFT: StringName = &"dpad_left"
-const ROLE_DPAD_RIGHT: StringName = &"dpad_right"
-const ROLE_LEFT_STICK_X: StringName = &"left_stick_x"
-const ROLE_LEFT_STICK_Y: StringName = &"left_stick_y"
-const ROLE_RIGHT_STICK_X: StringName = &"right_stick_x"
-const ROLE_RIGHT_STICK_Y: StringName = &"right_stick_y"
-
-const ROLE_ORDER: Array[StringName] = [
- ROLE_A,
- ROLE_B,
- ROLE_X,
- ROLE_Y,
- ROLE_LB,
- ROLE_RB,
- ROLE_POINTER_MODIFIER,
- ROLE_CAMERA_ZOOM,
- ROLE_SELECT,
- ROLE_START,
- ROLE_LEFT_STICK_CLICK,
- ROLE_RIGHT_STICK_CLICK,
- ROLE_DPAD_UP,
- ROLE_DPAD_DOWN,
- ROLE_DPAD_LEFT,
- ROLE_DPAD_RIGHT,
- ROLE_LEFT_STICK_X,
- ROLE_LEFT_STICK_Y,
- ROLE_RIGHT_STICK_X,
- ROLE_RIGHT_STICK_Y,
-]
-
-const ROLE_LABELS: Dictionary = {
- ROLE_A: "jump / menu accept",
- ROLE_B: "menu back",
- ROLE_X: "player menu",
- ROLE_Y: "interact / character call",
- ROLE_LB: "focus chat or world",
- ROLE_RB: "primary action",
- ROLE_POINTER_MODIFIER: "virtual mouse modifier",
- ROLE_CAMERA_ZOOM: "zoom modifier (+RT)",
- ROLE_SELECT: "chat",
- ROLE_START: "pause",
- ROLE_LEFT_STICK_CLICK: "sprint",
- ROLE_RIGHT_STICK_CLICK: "sneak",
- ROLE_DPAD_UP: "emote wheel",
- ROLE_DPAD_DOWN: "quick menu",
- ROLE_DPAD_LEFT: "previous hotbar slot",
- ROLE_DPAD_RIGHT: "next hotbar slot",
- ROLE_LEFT_STICK_X: "move left / right",
- ROLE_LEFT_STICK_Y: "move up / down",
- ROLE_RIGHT_STICK_X: "camera left / right",
- ROLE_RIGHT_STICK_Y: "camera up / down (+RT to zoom)",
-}
-
-const ROLE_PROMPTS: Dictionary = {
- ROLE_A: "press the a button",
- ROLE_B: "press the b button",
- ROLE_X: "press the x button",
- ROLE_Y: "press the y button",
- ROLE_LB: "press the left bumper",
- ROLE_RB: "press the right bumper",
- ROLE_POINTER_MODIFIER: "press or squeeze LT",
- ROLE_CAMERA_ZOOM: "press or squeeze RT",
- ROLE_SELECT: "press select / back",
- ROLE_START: "press start",
- ROLE_LEFT_STICK_CLICK: "click the left stick",
- ROLE_RIGHT_STICK_CLICK: "click the right stick",
- ROLE_DPAD_UP: "press d-pad up",
- ROLE_DPAD_DOWN: "press d-pad down",
- ROLE_DPAD_LEFT: "press d-pad left",
- ROLE_DPAD_RIGHT: "press d-pad right",
- ROLE_LEFT_STICK_X: "move the left stick left",
- ROLE_LEFT_STICK_Y: "move the left stick up",
- ROLE_RIGHT_STICK_X: "move the right stick left",
- ROLE_RIGHT_STICK_Y: "move the right stick up",
-}
-
-const STICK_AXIS_ROLES: Array[StringName] = [
- ROLE_LEFT_STICK_X,
- ROLE_LEFT_STICK_Y,
- ROLE_RIGHT_STICK_X,
- ROLE_RIGHT_STICK_Y,
-]
-const TRIGGER_ROLES: Array[StringName] = [
- ROLE_POINTER_MODIFIER,
- ROLE_CAMERA_ZOOM,
-]
-
-const BUTTON_ACTION_ROLES: Dictionary = {
- &"jump": ROLE_A,
- &"ui_accept": ROLE_A,
- &"ui_cancel": ROLE_B,
- &"open_backpack": ROLE_X,
- &"interact": ROLE_Y,
- &"character_call": ROLE_Y,
- &"focus_gameplay": ROLE_LB,
- &"fish_primary": ROLE_RB,
- &"open_chat": ROLE_SELECT,
- &"open_system_menu": ROLE_START,
- &"sprint": ROLE_LEFT_STICK_CLICK,
- &"sneak": ROLE_RIGHT_STICK_CLICK,
- &"open_emotes": ROLE_DPAD_UP,
- &"open_quick_actions": ROLE_DPAD_DOWN,
- &"hotbar_previous": ROLE_DPAD_LEFT,
- &"hotbar_next": ROLE_DPAD_RIGHT,
-}
-
-const AXIS_ACTION_ROLES: Dictionary = {
- &"move_left": [ROLE_LEFT_STICK_X, -1.0],
- &"move_right": [ROLE_LEFT_STICK_X, 1.0],
- &"move_forward": [ROLE_LEFT_STICK_Y, -1.0],
- &"move_backward": [ROLE_LEFT_STICK_Y, 1.0],
- &"ui_left": [ROLE_LEFT_STICK_X, -1.0],
- &"ui_right": [ROLE_LEFT_STICK_X, 1.0],
- &"ui_up": [ROLE_LEFT_STICK_Y, -1.0],
- &"ui_down": [ROLE_LEFT_STICK_Y, 1.0],
-}
-
-const UI_DPAD_ACTION_ROLES: Dictionary = {
- &"ui_up": ROLE_DPAD_UP,
- &"ui_down": ROLE_DPAD_DOWN,
- &"ui_left": ROLE_DPAD_LEFT,
- &"ui_right": ROLE_DPAD_RIGHT,
-}
-
-var _profiles: Dictionary = {}
-var _active_device_id: int = 0
-var _active_profile_key: String = "default"
-var _active_controller_name: String = "controller"
-var _default_joy_events: Dictionary = {}
-var _axis_rest_by_device: Dictionary = {}
-var _installed_compatibility_guids: Dictionary = {}
-
-
-func _ready() -> void:
- if not Input.joy_connection_changed.is_connected(
- _on_joy_connection_changed
- ):
- Input.joy_connection_changed.connect(_on_joy_connection_changed)
- _install_known_controller_mappings()
- for device_id: int in Input.get_connected_joypads():
- _sample_axis_rest_values(device_id)
- _capture_project_defaults()
- load_profiles()
- _refresh_active_controller()
-
-
-func _input(event: InputEvent) -> void:
- var device_id: int = -1
- var button_event := event as InputEventJoypadButton
- if button_event != null:
- device_id = button_event.device
- var motion_event := event as InputEventJoypadMotion
- if motion_event != null:
- device_id = motion_event.device
- var claims_active_device: bool = (
- button_event != null and button_event.pressed
- )
- if motion_event != null:
- claims_active_device = _axis_motion_claims_device(motion_event)
- if (
- claims_active_device
- and device_id >= 0
- and device_id != _active_device_id
- ):
- _set_active_controller(device_id)
- if button_event != null or motion_event != null:
- controller_input_observed.emit(event)
-
-
-func _process(_delta: float) -> void:
- var connected: Array[int] = Input.get_connected_joypads()
- if connected.is_empty():
- connected.append(0)
- if not connected.has(_active_device_id):
- _set_active_controller(connected[0])
- elif (
- _active_controller_name == "controller"
- and Input.get_connected_joypads().has(_active_device_id)
- ):
- _set_active_controller(_active_device_id)
- _sample_axis_rest_values(_active_device_id)
-
-
-func load_profiles() -> bool:
- _recover_interrupted_write()
- if not FileAccess.file_exists(PROFILE_PATH):
- _profiles = {}
- return true
- var file := FileAccess.open(PROFILE_PATH, FileAccess.READ)
- if file == null:
- return false
- if file.get_length() > MAX_PROFILE_BYTES:
- file.close()
- push_warning("Controller mappings are too large; using defaults.")
- _profiles = {}
- return false
- var json := JSON.new()
- var error: Error = json.parse(file.get_as_text())
- file.close()
- if error != OK or typeof(json.data) != TYPE_DICTIONARY:
- push_warning("Controller mappings are malformed; using defaults.")
- _profiles = {}
- return false
- var data: Dictionary = json.data
- if int(data.get("format_version", -1)) != FORMAT_VERSION:
- push_warning("Controller mapping version is unsupported; using defaults.")
- _profiles = {}
- return false
- var raw_profiles: Variant = data.get("profiles", {})
- if typeof(raw_profiles) != TYPE_DICTIONARY:
- _profiles = {}
- return false
- _profiles = _validated_profiles(raw_profiles as Dictionary)
- return true
-
-
-func _validated_profiles(raw_profiles: Dictionary) -> Dictionary:
- var validated: Dictionary = {}
- for key_value: Variant in raw_profiles:
- var raw_profile: Variant = raw_profiles[key_value]
- if typeof(raw_profile) != TYPE_DICTIONARY:
- continue
- var profile := raw_profile as Dictionary
- var raw_bindings: Variant = profile.get("bindings", {})
- if typeof(raw_bindings) != TYPE_DICTIONARY:
- continue
- var bindings := raw_bindings as Dictionary
- var complete: bool = true
- for role: StringName in ROLE_ORDER:
- var raw_binding: Variant = bindings.get(str(role), {})
- if (
- typeof(raw_binding) != TYPE_DICTIONARY
- or not validate_binding(role, raw_binding as Dictionary)
- ):
- complete = false
- break
- if not complete or bindings_have_conflicts(bindings):
- continue
- validated[str(key_value)] = {
- "controller_name": str(
- profile.get("controller_name", "controller")
- ),
- "bindings": bindings.duplicate(true),
- }
- return validated
-
-
-func has_custom_mapping() -> bool:
- return _profiles.has(_active_profile_key)
-
-
-func get_active_device_id() -> int:
- return _active_device_id
-
-
-func get_active_controller_name() -> String:
- return _active_controller_name
-
-
-func get_active_controller_guid() -> String:
- if not Input.get_connected_joypads().has(_active_device_id):
- return ""
- return Input.get_joy_guid(_active_device_id).strip_edges()
-
-
-func get_active_profile_key() -> String:
- return _active_profile_key
-
-
-func get_active_controller_platform() -> String:
- return OS.get_name()
-
-
-func recalibrate_active_device() -> void:
- _recalibrate_device(_active_device_id)
-
-
-func get_role_label(role: StringName) -> String:
- return str(ROLE_LABELS.get(role, str(role)))
-
-
-func get_role_prompt(role: StringName) -> String:
- return str(ROLE_PROMPTS.get(role, "press or move the requested input"))
-
-
-func role_expects_axis(role: StringName) -> bool:
- return role in STICK_AXIS_ROLES
-
-
-func role_accepts_axis(role: StringName) -> bool:
- return role in STICK_AXIS_ROLES or role in TRIGGER_ROLES
-
-
-func are_capture_inputs_neutral(device_id: int = -1) -> bool:
- var target_device: int = (
- _active_device_id if device_id < 0 else device_id
- )
- for button_index: int in JOY_BUTTON_MAX:
- if Input.is_joy_button_pressed(target_device, button_index):
- return false
- for axis_index: int in JOY_AXIS_MAX:
- var rest: float = _axis_rest_value(target_device, axis_index)
- var current: float = Input.get_joy_axis(target_device, axis_index)
- if absf(current - rest) > CAPTURE_AXIS_RELEASE_THRESHOLD:
- return false
- return true
-
-
-func get_pressed_capture_button(device_id: int = -1) -> int:
- var target_device: int = (
- _active_device_id if device_id < 0 else device_id
- )
- for button_index: int in JOY_BUTTON_MAX:
- if Input.is_joy_button_pressed(target_device, button_index):
- return button_index
- return -1
-
-
-func get_active_bindings() -> Dictionary:
- var raw_profile: Variant = _profiles.get(_active_profile_key, {})
- var profile: Dictionary = (
- raw_profile as Dictionary
- if typeof(raw_profile) == TYPE_DICTIONARY else {}
- )
- var bindings: Variant = profile.get("bindings", {})
- if (
- typeof(bindings) == TYPE_DICTIONARY
- and not (bindings as Dictionary).is_empty()
- ):
- return (bindings as Dictionary).duplicate(true)
- return default_bindings()
-
-
-func get_binding(role: StringName) -> Dictionary:
- var bindings: Dictionary = get_active_bindings()
- var binding: Variant = bindings.get(str(role), {})
- if typeof(binding) == TYPE_DICTIONARY:
- return (binding as Dictionary).duplicate(true)
- return {}
-
-
-func set_binding(role: StringName, binding: Dictionary) -> bool:
- if role not in ROLE_ORDER or not validate_binding(role, binding):
- return false
- var bindings: Dictionary = get_active_bindings()
- bindings[str(role)] = binding.duplicate(true)
- return replace_active_bindings(bindings)
-
-
-func replace_active_bindings(bindings: Dictionary) -> bool:
- for role: StringName in ROLE_ORDER:
- var binding: Variant = bindings.get(str(role), {})
- if typeof(binding) != TYPE_DICTIONARY:
- return false
- if not validate_binding(role, binding as Dictionary):
- return false
- if bindings_have_conflicts(bindings):
- return false
- var previous: Dictionary = _profiles.duplicate(true)
- _profiles[_active_profile_key] = {
- "controller_name": _active_controller_name,
- "bindings": bindings.duplicate(true),
- }
- if not _save_profiles():
- _profiles = previous
- return false
- _apply_active_profile()
- active_profile_changed.emit()
- return true
-
-
-func reset_active_profile() -> bool:
- if not _profiles.has(_active_profile_key):
- _restore_project_defaults()
- active_profile_changed.emit()
- return true
- var previous: Dictionary = _profiles.duplicate(true)
- _profiles.erase(_active_profile_key)
- if not _save_profiles():
- _profiles = previous
- return false
- _restore_project_defaults()
- active_profile_changed.emit()
- return true
-
-
-func binding_from_event(role: StringName, event: InputEvent) -> Dictionary:
- var button := event as InputEventJoypadButton
- if role in TRIGGER_ROLES and button != null and button.pressed:
- return {
- "kind": "button",
- "button": int(button.button_index),
- }
- if role_accepts_axis(role):
- var motion := event as InputEventJoypadMotion
- if motion == null:
- return {}
- _sample_axis_rest_values(motion.device)
- var rest: float = _axis_rest_value(motion.device, motion.axis)
- var travel: float = motion.axis_value - rest
- if absf(travel) < CAPTURE_AXIS_THRESHOLD:
- return {}
- return {
- "kind": "axis",
- "axis": int(motion.axis),
- "direction": signf(travel),
- "rest": rest,
- }
- if button == null or not button.pressed:
- return {}
- return {
- "kind": "button",
- "button": int(button.button_index),
- }
-
-
-func validate_binding(role: StringName, binding: Dictionary) -> bool:
- var binding_kind: String = str(binding.get("kind", ""))
- if role_expects_axis(role) and binding_kind != "axis":
- return false
- if role not in STICK_AXIS_ROLES and role not in TRIGGER_ROLES:
- if binding_kind != "button":
- return false
- if role in TRIGGER_ROLES and binding_kind not in ["axis", "button"]:
- return false
- if binding_kind == "button":
- var button_index: int = int(binding.get("button", -1))
- return button_index >= 0 and button_index < JOY_BUTTON_MAX
- var axis_index: int = int(binding.get("axis", -1))
- var direction: float = float(binding.get("direction", 0.0))
- var rest: float = float(binding.get("rest", 0.0))
- return (
- axis_index >= 0
- and axis_index < JOY_AXIS_MAX
- and not is_zero_approx(direction)
- and rest >= -1.0
- and rest <= 1.0
- )
-
-
-static func bindings_have_conflicts(bindings: Dictionary) -> bool:
- for role_index: int in ROLE_ORDER.size():
- var first_role: StringName = ROLE_ORDER[role_index]
- var first_value: Variant = bindings.get(str(first_role), {})
- if typeof(first_value) != TYPE_DICTIONARY:
- continue
- for other_index: int in range(role_index + 1, ROLE_ORDER.size()):
- var second_role: StringName = ROLE_ORDER[other_index]
- var second_value: Variant = bindings.get(str(second_role), {})
- if typeof(second_value) != TYPE_DICTIONARY:
- continue
- if bindings_conflict(
- first_role,
- first_value as Dictionary,
- second_role,
- second_value as Dictionary,
- ):
- return true
- return false
-
-
-static func bindings_conflict(
- first_role: StringName,
- first: Dictionary,
- second_role: StringName,
- second: Dictionary,
-) -> bool:
- var binding_kind: String = str(first.get("kind", ""))
- if binding_kind != str(second.get("kind", "")):
- return false
- if binding_kind == "button":
- return int(first.get("button", -1)) == int(
- second.get("button", -1)
- )
- if binding_kind != "axis":
- return false
- if int(first.get("axis", -1)) != int(second.get("axis", -1)):
- return false
- var opposite_trigger_halves: bool = (
- first_role in TRIGGER_ROLES
- and second_role in TRIGGER_ROLES
- and signf(float(first.get("direction", 0.0)))
- != signf(float(second.get("direction", 0.0)))
- )
- return not opposite_trigger_halves
-
-
-func binding_label(binding: Dictionary) -> String:
- if str(binding.get("kind", "")) == "button":
- return _button_label(int(binding.get("button", -1)))
- if str(binding.get("kind", "")) == "axis":
- return "%s %s" % [
- _axis_label(int(binding.get("axis", -1))),
- "+" if float(binding.get("direction", 0.0)) > 0.0 else "−",
- ]
- return "unmapped"
-
-
-static func _button_label(button_index: int) -> String:
- match button_index:
- JOY_BUTTON_A:
- return "A button"
- JOY_BUTTON_B:
- return "B button"
- JOY_BUTTON_X:
- return "X button"
- JOY_BUTTON_Y:
- return "Y button"
- JOY_BUTTON_BACK:
- return "back / select"
- JOY_BUTTON_GUIDE:
- return "guide button"
- JOY_BUTTON_START:
- return "start button"
- JOY_BUTTON_LEFT_STICK:
- return "left stick click"
- JOY_BUTTON_RIGHT_STICK:
- return "right stick click"
- JOY_BUTTON_LEFT_SHOULDER:
- return "left shoulder"
- JOY_BUTTON_RIGHT_SHOULDER:
- return "right shoulder"
- JOY_BUTTON_DPAD_UP:
- return "d-pad up"
- JOY_BUTTON_DPAD_DOWN:
- return "d-pad down"
- JOY_BUTTON_DPAD_LEFT:
- return "d-pad left"
- JOY_BUTTON_DPAD_RIGHT:
- return "d-pad right"
- _:
- return "button %d" % button_index
-
-
-static func _axis_label(axis_index: int) -> String:
- match axis_index:
- JOY_AXIS_LEFT_X:
- return "left stick X"
- JOY_AXIS_LEFT_Y:
- return "left stick Y"
- JOY_AXIS_RIGHT_X:
- return "right stick X"
- JOY_AXIS_RIGHT_Y:
- return "right stick Y"
- JOY_AXIS_TRIGGER_LEFT:
- return "left trigger axis"
- JOY_AXIS_TRIGGER_RIGHT:
- return "right trigger axis"
- _:
- return "axis %d" % axis_index
-
-
-func get_role_strength(role: StringName) -> float:
- var binding: Dictionary = get_binding(role)
- if str(binding.get("kind", "")) == "button":
- return (
- 1.0
- if Input.is_joy_button_pressed(
- _active_device_id,
- int(binding.get("button", -1)),
- )
- else 0.0
- )
- return _axis_binding_strength(binding)
-
-
-func get_role_axis(role: StringName) -> float:
- var binding: Dictionary = get_binding(role)
- if str(binding.get("kind", "")) != "axis":
- return 0.0
- var raw: float = Input.get_joy_axis(
- _active_device_id,
- int(binding.get("axis", -1)),
- )
- var direction: float = signf(float(binding.get("direction", -1.0)))
- var rest: float = float(binding.get("rest", 0.0))
- var delta: float = raw - rest
- if is_zero_approx(delta):
- return 0.0
- var toward_captured_direction: bool = signf(delta) == direction
- var endpoint: float = direction if toward_captured_direction else -direction
- var available: float = absf(endpoint - rest)
- if available <= 0.001:
- return 0.0
- var magnitude: float = clampf(absf(delta) / available, 0.0, 1.0)
- return -magnitude if toward_captured_direction else magnitude
-
-
-func event_matches_role(event: InputEvent, role: StringName) -> bool:
- if not _event_belongs_to_active_device(event):
- return false
- var binding: Dictionary = get_binding(role)
- var button := event as InputEventJoypadButton
- if button != null and str(binding.get("kind", "")) == "button":
- return button.button_index == int(binding.get("button", -1))
- var motion := event as InputEventJoypadMotion
- if motion == null or str(binding.get("kind", "")) != "axis":
- return false
- if motion.axis != int(binding.get("axis", -1)):
- return false
- return _axis_binding_strength(binding, motion.axis_value) >= 0.5
-
-
-func event_uses_role(event: InputEvent, role: StringName) -> bool:
- if not _event_belongs_to_active_device(event):
- return false
- var binding: Dictionary = get_binding(role)
- var button := event as InputEventJoypadButton
- if button != null and str(binding.get("kind", "")) == "button":
- return button.button_index == int(binding.get("button", -1))
- var motion := event as InputEventJoypadMotion
- return (
- motion != null
- and str(binding.get("kind", "")) == "axis"
- and motion.axis == int(binding.get("axis", -1))
- )
-
-
-static func default_bindings() -> Dictionary:
- return {
- str(ROLE_A): _button_binding(JOY_BUTTON_A),
- str(ROLE_B): _button_binding(JOY_BUTTON_B),
- str(ROLE_X): _button_binding(JOY_BUTTON_X),
- str(ROLE_Y): _button_binding(JOY_BUTTON_Y),
- str(ROLE_LB): _button_binding(JOY_BUTTON_LEFT_SHOULDER),
- str(ROLE_RB): _button_binding(JOY_BUTTON_RIGHT_SHOULDER),
- str(ROLE_POINTER_MODIFIER): _axis_binding(
- JOY_AXIS_TRIGGER_RIGHT, 1.0, 0.0
- ),
- str(ROLE_CAMERA_ZOOM): _axis_binding(
- JOY_AXIS_TRIGGER_LEFT, 1.0, 0.0
- ),
- str(ROLE_SELECT): _button_binding(JOY_BUTTON_BACK),
- str(ROLE_START): _button_binding(JOY_BUTTON_START),
- str(ROLE_LEFT_STICK_CLICK): _button_binding(JOY_BUTTON_LEFT_STICK),
- str(ROLE_RIGHT_STICK_CLICK): _button_binding(JOY_BUTTON_RIGHT_STICK),
- str(ROLE_DPAD_UP): _button_binding(JOY_BUTTON_DPAD_UP),
- str(ROLE_DPAD_DOWN): _button_binding(JOY_BUTTON_DPAD_DOWN),
- str(ROLE_DPAD_LEFT): _button_binding(JOY_BUTTON_DPAD_LEFT),
- str(ROLE_DPAD_RIGHT): _button_binding(JOY_BUTTON_DPAD_RIGHT),
- str(ROLE_LEFT_STICK_X): _axis_binding(JOY_AXIS_LEFT_X, -1.0, 0.0),
- str(ROLE_LEFT_STICK_Y): _axis_binding(JOY_AXIS_LEFT_Y, -1.0, 0.0),
- str(ROLE_RIGHT_STICK_X): _axis_binding(JOY_AXIS_RIGHT_X, -1.0, 0.0),
- str(ROLE_RIGHT_STICK_Y): _axis_binding(JOY_AXIS_RIGHT_Y, -1.0, 0.0),
- }
-
-
-static func _button_binding(button: JoyButton) -> Dictionary:
- return {"kind": "button", "button": int(button)}
-
-
-static func _axis_binding(
- axis: JoyAxis,
- direction: float,
- rest: float,
-) -> Dictionary:
- return {
- "kind": "axis",
- "axis": int(axis),
- "direction": direction,
- "rest": rest,
- }
-
-
-func _axis_binding_strength(
- binding: Dictionary,
- raw_override: float = INF,
-) -> float:
- var axis: int = int(binding.get("axis", -1))
- if axis < 0:
- return 0.0
- var raw: float = (
- Input.get_joy_axis(_active_device_id, axis)
- if is_inf(raw_override) else raw_override
- )
- var rest: float = float(binding.get("rest", 0.0))
- var direction: float = signf(float(binding.get("direction", 0.0)))
- var endpoint: float = direction
- var available: float = absf(endpoint - rest)
- if available <= 0.001:
- return 0.0
- return clampf((raw - rest) * direction / available, 0.0, 1.0)
-
-
-func _refresh_active_controller() -> void:
- var connected: Array[int] = Input.get_connected_joypads()
- if connected.has(_active_device_id):
- _set_active_controller(_active_device_id)
- return
- _set_active_controller(connected[0] if not connected.is_empty() else 0)
-
-
-func _on_joy_connection_changed(device_id: int, connected: bool) -> void:
- _axis_rest_by_device.erase(str(device_id))
- if connected:
- _install_known_controller_mapping(device_id)
- call_deferred("_recalibrate_device", device_id)
- _refresh_active_controller()
-
-
-func _install_known_controller_mappings() -> void:
- for device_id: int in Input.get_connected_joypads():
- _install_known_controller_mapping(device_id)
-
-
-func _install_known_controller_mapping(device_id: int) -> bool:
- var controller_name: String = Input.get_joy_name(device_id).strip_edges()
- if not should_install_muos_compatibility_mapping(
- controller_name,
- Input.is_joy_known(device_id),
- ):
- return false
- var guid: String = Input.get_joy_guid(device_id).strip_edges()
- if guid.is_empty():
- return false
- if _installed_compatibility_guids.has(guid):
- return true
- _installed_compatibility_guids[guid] = true
- Input.add_joy_mapping(
- build_muos_controller_mapping(guid, controller_name),
- true,
- )
- return true
-
-
-static func build_muos_controller_mapping(
- guid: String,
- controller_name: String,
-) -> String:
- return "%s,%s,%s" % [
- guid.strip_edges(),
- controller_name.strip_edges(),
- MUOS_MAPPING_BINDINGS,
- ]
-
-
-static func is_muos_controller_name(controller_name: String) -> bool:
- return controller_name.strip_edges() in MUOS_CONTROLLER_NAMES
-
-
-static func should_install_muos_compatibility_mapping(
- controller_name: String,
- is_known: bool,
-) -> bool:
- # Never update an already-recognized virtual controller at runtime. Godot
- # can stop delivering its standardized input events after that replacement.
- return is_muos_controller_name(controller_name) and not is_known
-
-
-func _set_active_controller(device_id: int) -> void:
- var previous_device_id: int = _active_device_id
- var previous_profile_key: String = _active_profile_key
- var previous_controller_name: String = _active_controller_name
- _active_device_id = maxi(device_id, 0)
- var controller_is_listed: bool = Input.get_connected_joypads().has(
- _active_device_id
- )
- var guid: String = (
- Input.get_joy_guid(_active_device_id).strip_edges()
- if controller_is_listed else ""
- )
- var controller_name: String = (
- Input.get_joy_name(_active_device_id).strip_edges()
- if controller_is_listed else "controller"
- )
- if controller_name.is_empty():
- controller_name = "controller"
- _active_controller_name = controller_name
- if guid.is_empty():
- _active_profile_key = "name:" + controller_name
- elif is_muos_controller_name(controller_name):
- _active_profile_key = guid + ":" + MUOS_MAPPING_REVISION
- else:
- _active_profile_key = guid
- if (
- previous_profile_key == "name:controller"
- and _active_profile_key != previous_profile_key
- and _profiles.has(previous_profile_key)
- and not _profiles.has(_active_profile_key)
- ):
- var previous_profile: Variant = _profiles[previous_profile_key]
- if typeof(previous_profile) == TYPE_DICTIONARY:
- _profiles[_active_profile_key] = (
- previous_profile as Dictionary
- ).duplicate(true)
- _profiles.erase(previous_profile_key)
- _save_profiles()
- _sample_axis_rest_values(_active_device_id)
- _apply_active_profile()
- if (
- previous_device_id != _active_device_id
- or previous_profile_key != _active_profile_key
- or previous_controller_name != _active_controller_name
- ):
- active_controller_changed.emit(_active_controller_name)
-
-
-func _axis_motion_claims_device(event: InputEventJoypadMotion) -> bool:
- var device_key: String = str(event.device)
- if not _axis_rest_by_device.has(device_key):
- _sample_axis_rest_values(event.device)
- return false
- var rest: float = _axis_rest_value(event.device, event.axis)
- return absf(event.axis_value - rest) >= ACTIVE_DEVICE_AXIS_THRESHOLD
-
-
-func _event_belongs_to_active_device(event: InputEvent) -> bool:
- var button := event as InputEventJoypadButton
- if button != null:
- return button.device == _active_device_id
- var motion := event as InputEventJoypadMotion
- return motion != null and motion.device == _active_device_id
-
-
-func _sample_axis_rest_values(device_id: int) -> void:
- var key: String = str(device_id)
- if _axis_rest_by_device.has(key):
- return
- var values: Dictionary = {}
- for axis: int in JOY_AXIS_MAX:
- values[str(axis)] = Input.get_joy_axis(device_id, axis)
- _axis_rest_by_device[key] = values
-
-
-func _recalibrate_device(device_id: int) -> void:
- _axis_rest_by_device.erase(str(device_id))
- var connected: Array[int] = Input.get_connected_joypads()
- if not connected.has(device_id) and not (
- connected.is_empty() and device_id == 0
- ):
- return
- _sample_axis_rest_values(device_id)
-
-
-func _axis_rest_value(device_id: int, axis: int) -> float:
- _sample_axis_rest_values(device_id)
- var values: Dictionary = _axis_rest_by_device.get(str(device_id), {})
- return float(values.get(str(axis), 0.0))
-
-
-func _capture_project_defaults() -> void:
- for action: StringName in _all_managed_actions():
- var joy_events: Array[InputEvent] = []
- for event: InputEvent in InputMap.action_get_events(action):
- if event is InputEventJoypadButton or event is InputEventJoypadMotion:
- joy_events.append(event.duplicate())
- _default_joy_events[action] = joy_events
-
-
-func _all_managed_actions() -> Array[StringName]:
- var result: Array[StringName] = []
- for source: Dictionary in [
- BUTTON_ACTION_ROLES,
- AXIS_ACTION_ROLES,
- UI_DPAD_ACTION_ROLES,
- ]:
- for action_value: Variant in source.keys():
- var action: StringName = StringName(action_value)
- if action not in result:
- result.append(action)
- return result
-
-
-func _remove_managed_joy_events() -> void:
- for action: StringName in _all_managed_actions():
- if not InputMap.has_action(action):
- continue
- for event: InputEvent in InputMap.action_get_events(action):
- if event is InputEventJoypadButton or event is InputEventJoypadMotion:
- InputMap.action_erase_event(action, event)
-
-
-func _restore_project_defaults() -> void:
- _remove_managed_joy_events()
- for action_value: Variant in _default_joy_events:
- var action: StringName = StringName(action_value)
- for event: InputEvent in _default_joy_events[action]:
- var active_event: InputEvent = event.duplicate()
- active_event.device = _active_device_id
- InputMap.action_add_event(action, active_event)
-
-
-func _apply_active_profile() -> void:
- if not has_custom_mapping():
- _restore_project_defaults()
- return
- _remove_managed_joy_events()
- var bindings: Dictionary = get_active_bindings()
- for action_value: Variant in BUTTON_ACTION_ROLES:
- var action: StringName = StringName(action_value)
- _add_button_action_binding(
- action,
- bindings.get(str(BUTTON_ACTION_ROLES[action]), {}),
- )
- for action_value: Variant in AXIS_ACTION_ROLES:
- var action: StringName = StringName(action_value)
- var role_data: Array = AXIS_ACTION_ROLES[action]
- _add_axis_action_binding(
- action,
- bindings.get(str(role_data[0]), {}),
- float(role_data[1]),
- )
- for action_value: Variant in UI_DPAD_ACTION_ROLES:
- var action: StringName = StringName(action_value)
- _add_button_action_binding(
- action,
- bindings.get(str(UI_DPAD_ACTION_ROLES[action]), {}),
- )
-
-
-func _add_button_action_binding(action: StringName, value: Variant) -> void:
- if not InputMap.has_action(action) or typeof(value) != TYPE_DICTIONARY:
- return
- var binding: Dictionary = value as Dictionary
- if str(binding.get("kind", "")) != "button":
- return
- var event := InputEventJoypadButton.new()
- event.device = _active_device_id
- event.button_index = int(binding.get("button", -1)) as JoyButton
- InputMap.action_add_event(action, event)
-
-
-func _add_axis_action_binding(
- action: StringName,
- value: Variant,
- logical_direction: float,
-) -> void:
- if not InputMap.has_action(action) or typeof(value) != TYPE_DICTIONARY:
- return
- var binding: Dictionary = value as Dictionary
- if str(binding.get("kind", "")) != "axis":
- return
- var captured_negative_direction: float = signf(
- float(binding.get("direction", -1.0))
- )
- var event := InputEventJoypadMotion.new()
- event.device = _active_device_id
- event.axis = int(binding.get("axis", -1)) as JoyAxis
- event.axis_value = logical_direction * -captured_negative_direction
- InputMap.action_add_event(action, event)
-
-
-func _save_profiles() -> bool:
- var file := FileAccess.open(PROFILE_TEMP_PATH, FileAccess.WRITE)
- if file == null:
- return false
- file.store_string(JSON.stringify({
- "format_version": FORMAT_VERSION,
- "profiles": _profiles,
- }, "\t"))
- file.flush()
- var write_error: Error = file.get_error()
- file.close()
- if write_error != OK:
- _remove_if_present(PROFILE_TEMP_PATH)
- return false
- _remove_if_present(PROFILE_BACKUP_PATH)
- var had_primary: bool = FileAccess.file_exists(PROFILE_PATH)
- if had_primary and not _rename_file(PROFILE_PATH, PROFILE_BACKUP_PATH):
- _remove_if_present(PROFILE_TEMP_PATH)
- return false
- if not _rename_file(PROFILE_TEMP_PATH, PROFILE_PATH):
- if had_primary:
- _rename_file(PROFILE_BACKUP_PATH, PROFILE_PATH)
- return false
- _remove_if_present(PROFILE_BACKUP_PATH)
- return true
-
-
-func _recover_interrupted_write() -> void:
- if FileAccess.file_exists(PROFILE_PATH):
- _remove_if_present(PROFILE_TEMP_PATH)
- _remove_if_present(PROFILE_BACKUP_PATH)
- return
- if FileAccess.file_exists(PROFILE_BACKUP_PATH):
- _rename_file(PROFILE_BACKUP_PATH, PROFILE_PATH)
- _remove_if_present(PROFILE_TEMP_PATH)
-
-
-func _rename_file(from_path: String, to_path: String) -> bool:
- return DirAccess.rename_absolute(
- ProjectSettings.globalize_path(from_path),
- ProjectSettings.globalize_path(to_path),
- ) == OK
-
-
-func _remove_if_present(path: String) -> void:
- if FileAccess.file_exists(path):
- DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
diff --git a/settings/controller_mapping_manager.gd.uid b/settings/controller_mapping_manager.gd.uid
deleted file mode 100644
index 3fcbc13..0000000
--- a/settings/controller_mapping_manager.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c80x8jkdnx0xy
diff --git a/settings/keyboard_mouse_mapping_manager.gd b/settings/keyboard_mouse_mapping_manager.gd
deleted file mode 100644
index 54dacc8..0000000
--- a/settings/keyboard_mouse_mapping_manager.gd
+++ /dev/null
@@ -1,472 +0,0 @@
-class_name KeyboardMouseMappingManager
-extends Node
-
-signal mapping_changed
-
-const FORMAT_VERSION: int = 2
-const LEGACY_FORMAT_VERSION: int = 1
-const MAPPING_PATH: String = "user://keyboard_mouse_bindings.json"
-const MAPPING_TEMP_PATH: String = "user://keyboard_mouse_bindings.json.tmp"
-const MAPPING_BACKUP_PATH: String = (
- "user://keyboard_mouse_bindings.json.backup"
-)
-const MAX_MAPPING_BYTES: int = 256 * 1024
-
-const ROLE_MOVE_FORWARD: StringName = &"move_forward"
-const ROLE_MOVE_BACKWARD: StringName = &"move_backward"
-const ROLE_MOVE_LEFT: StringName = &"move_left"
-const ROLE_MOVE_RIGHT: StringName = &"move_right"
-const ROLE_SPRINT: StringName = &"sprint"
-const ROLE_JUMP: StringName = &"jump"
-const ROLE_SNEAK: StringName = &"sneak"
-const ROLE_SLOW_WALK: StringName = &"slow_walk"
-const ROLE_INTERACT: StringName = &"interact"
-const ROLE_PRIMARY_ACTION: StringName = &"fish_primary"
-const ROLE_ALTERNATE_REEL: StringName = &"reel_alternate"
-const ROLE_CAMERA_DRAG: StringName = &"camera_drag"
-const ROLE_CAMERA_ZOOM_IN: StringName = &"camera_zoom_in"
-const ROLE_CAMERA_ZOOM_OUT: StringName = &"camera_zoom_out"
-const ROLE_PLAYER_MENU: StringName = &"open_backpack"
-const ROLE_TACKLE_BOX: StringName = &"open_tacklebox"
-const ROLE_PROP_BOOK: StringName = &"open_props"
-const ROLE_EMOTE_WHEEL: StringName = &"open_emotes"
-const ROLE_CHARACTER_CALL: StringName = &"character_call"
-const ROLE_CHAT: StringName = &"open_chat"
-const ROLE_FREE_CAMERA: StringName = &"toggle_free_camera"
-const ROLE_HIDE_HUD: StringName = &"toggle_hud"
-const ROLE_MENU_ACCEPT: StringName = &"ui_accept"
-const ROLE_MENU_BACK: StringName = &"ui_cancel"
-
-const ROLE_ORDER: Array[StringName] = [
- ROLE_MOVE_FORWARD,
- ROLE_MOVE_BACKWARD,
- ROLE_MOVE_LEFT,
- ROLE_MOVE_RIGHT,
- ROLE_SPRINT,
- ROLE_JUMP,
- ROLE_SNEAK,
- ROLE_SLOW_WALK,
- ROLE_INTERACT,
- ROLE_PRIMARY_ACTION,
- ROLE_ALTERNATE_REEL,
- ROLE_CAMERA_DRAG,
- ROLE_CAMERA_ZOOM_IN,
- ROLE_CAMERA_ZOOM_OUT,
- ROLE_PLAYER_MENU,
- ROLE_TACKLE_BOX,
- ROLE_PROP_BOOK,
- ROLE_EMOTE_WHEEL,
- ROLE_CHARACTER_CALL,
- ROLE_CHAT,
- ROLE_FREE_CAMERA,
- ROLE_HIDE_HUD,
- &"hotbar_1",
- &"hotbar_2",
- &"hotbar_3",
- &"hotbar_4",
- &"hotbar_5",
- &"hotbar_6",
- &"hotbar_7",
- &"hotbar_8",
- &"hotbar_9",
- ROLE_MENU_ACCEPT,
- ROLE_MENU_BACK,
-]
-
-const ROLE_LABELS: Dictionary = {
- ROLE_MOVE_FORWARD: "move forward",
- ROLE_MOVE_BACKWARD: "move backward",
- ROLE_MOVE_LEFT: "move left",
- ROLE_MOVE_RIGHT: "move right",
- ROLE_SPRINT: "sprint",
- ROLE_JUMP: "jump",
- ROLE_SNEAK: "sneak",
- ROLE_SLOW_WALK: "slow walk",
- ROLE_INTERACT: "interact",
- ROLE_PRIMARY_ACTION: "primary action",
- ROLE_ALTERNATE_REEL: "alternate reel",
- ROLE_CAMERA_DRAG: "rotate camera",
- ROLE_CAMERA_ZOOM_IN: "hotbar select (+SHIFT to zoom)",
- ROLE_CAMERA_ZOOM_OUT: "hotbar select (+SHIFT to zoom)",
- ROLE_PLAYER_MENU: "player menu",
- ROLE_TACKLE_BOX: "tackle box",
- ROLE_PROP_BOOK: "prop book",
- ROLE_EMOTE_WHEEL: "emote wheel",
- ROLE_CHARACTER_CALL: "character call",
- ROLE_CHAT: "chat",
- ROLE_FREE_CAMERA: "free camera",
- ROLE_HIDE_HUD: "hide hud",
- &"hotbar_1": "hotbar 1",
- &"hotbar_2": "hotbar 2",
- &"hotbar_3": "hotbar 3",
- &"hotbar_4": "hotbar 4",
- &"hotbar_5": "hotbar 5",
- &"hotbar_6": "hotbar 6",
- &"hotbar_7": "hotbar 7",
- &"hotbar_8": "hotbar 8",
- &"hotbar_9": "hotbar 9",
- ROLE_MENU_ACCEPT: "menu accept",
- ROLE_MENU_BACK: "menu back / pause",
-}
-
-var _default_events: Dictionary = {}
-var _default_bindings: Dictionary = {}
-var _bindings: Dictionary = {}
-
-
-func _ready() -> void:
- _capture_project_defaults()
- load_mapping()
- _apply_active_mapping()
-
-
-func load_mapping() -> bool:
- _recover_interrupted_write()
- if not FileAccess.file_exists(MAPPING_PATH):
- _bindings = {}
- return true
- var file := FileAccess.open(MAPPING_PATH, FileAccess.READ)
- if file == null:
- _bindings = {}
- return false
- if file.get_length() > MAX_MAPPING_BYTES:
- file.close()
- push_warning("Keyboard bindings are too large; using defaults.")
- _bindings = {}
- return false
- var json := JSON.new()
- var error: Error = json.parse(file.get_as_text())
- file.close()
- if error != OK or typeof(json.data) != TYPE_DICTIONARY:
- push_warning("Keyboard bindings are malformed; using defaults.")
- _bindings = {}
- return false
- var data := json.data as Dictionary
- var format_version: int = int(data.get("format_version", -1))
- if format_version not in [LEGACY_FORMAT_VERSION, FORMAT_VERSION]:
- push_warning("Keyboard binding version is unsupported; using defaults.")
- _bindings = {}
- return false
- var raw_bindings: Variant = data.get("bindings", {})
- if (
- typeof(raw_bindings) != TYPE_DICTIONARY
- ):
- push_warning("Keyboard bindings are incomplete; using defaults.")
- _bindings = {}
- return false
- var loaded_bindings := (raw_bindings as Dictionary).duplicate(true)
- if format_version == LEGACY_FORMAT_VERSION:
- loaded_bindings[str(ROLE_ALTERNATE_REEL)] = (
- _default_bindings[str(ROLE_ALTERNATE_REEL)].duplicate(true)
- )
- if not _validate_complete_bindings(loaded_bindings):
- push_warning("Keyboard bindings are incomplete; using defaults.")
- _bindings = {}
- return false
- _bindings = loaded_bindings
- return true
-
-
-func has_custom_mapping() -> bool:
- return not _bindings.is_empty()
-
-
-func get_active_bindings() -> Dictionary:
- if has_custom_mapping():
- return _bindings.duplicate(true)
- return _default_bindings.duplicate(true)
-
-
-func get_binding(role: StringName) -> Dictionary:
- var binding: Variant = get_active_bindings().get(str(role), {})
- if typeof(binding) != TYPE_DICTIONARY:
- return {}
- return (binding as Dictionary).duplicate(true)
-
-
-func get_binding_label(role: StringName) -> String:
- return binding_label(get_binding(role))
-
-
-func get_role_label(role: StringName) -> String:
- return str(ROLE_LABELS.get(role, str(role).replace("_", " ")))
-
-
-func binding_from_event(event: InputEvent) -> Dictionary:
- var key_event := event as InputEventKey
- if key_event != null:
- if not key_event.pressed or key_event.echo:
- return {}
- if key_event.keycode in [KEY_SHIFT, KEY_CTRL, KEY_ALT, KEY_META]:
- return {}
- if key_event.keycode == KEY_NONE and (
- key_event.physical_keycode == KEY_NONE
- ):
- return {}
- return {
- "kind": "key",
- "keycode": int(key_event.keycode),
- "physical_keycode": int(key_event.physical_keycode),
- "alt": key_event.alt_pressed,
- "shift": key_event.shift_pressed,
- "ctrl": key_event.ctrl_pressed,
- "meta": key_event.meta_pressed,
- "location": int(key_event.location),
- }
- var mouse_event := event as InputEventMouseButton
- if mouse_event == null or not mouse_event.pressed:
- return {}
- return {
- "kind": "mouse_button",
- "button": int(mouse_event.button_index),
- }
-
-
-func validate_binding(binding: Dictionary) -> bool:
- var kind: String = str(binding.get("kind", ""))
- if kind == "mouse_button":
- var button: int = int(binding.get("button", 0))
- return (
- button >= int(MOUSE_BUTTON_LEFT)
- and button <= int(MOUSE_BUTTON_XBUTTON2)
- )
- if kind != "key":
- return false
- var keycode: int = int(binding.get("keycode", int(KEY_NONE)))
- var physical_keycode: int = int(
- binding.get("physical_keycode", int(KEY_NONE))
- )
- if keycode == int(KEY_NONE) and physical_keycode == int(KEY_NONE):
- return false
- for modifier: String in ["alt", "shift", "ctrl", "meta"]:
- if typeof(binding.get(modifier, false)) != TYPE_BOOL:
- return false
- return true
-
-
-func set_binding(role: StringName, binding: Dictionary) -> bool:
- if role not in ROLE_ORDER or not validate_binding(binding):
- return false
- var updated: Dictionary = get_active_bindings()
- updated[str(role)] = binding.duplicate(true)
- return replace_active_bindings(updated)
-
-
-func replace_active_bindings(bindings: Dictionary) -> bool:
- if not _validate_complete_bindings(bindings):
- return false
- var previous: Dictionary = _bindings.duplicate(true)
- _bindings = bindings.duplicate(true)
- if not _save_mapping():
- _bindings = previous
- return false
- _apply_active_mapping()
- mapping_changed.emit()
- return true
-
-
-func reset_mapping() -> bool:
- var previous: Dictionary = _bindings.duplicate(true)
- _bindings = {}
- if not _remove_if_present(MAPPING_PATH):
- _bindings = previous
- return false
- _remove_if_present(MAPPING_TEMP_PATH)
- _remove_if_present(MAPPING_BACKUP_PATH)
- _restore_project_defaults()
- mapping_changed.emit()
- return true
-
-
-func binding_label(binding: Dictionary) -> String:
- if str(binding.get("kind", "")) == "mouse_button":
- return _mouse_button_label(int(binding.get("button", 0)))
- if str(binding.get("kind", "")) != "key":
- return "unmapped"
- var event := _event_from_binding(binding) as InputEventKey
- if event == null:
- return "unmapped"
- var label: String = event.as_text_physical_keycode()
- if label.is_empty():
- label = event.as_text_keycode()
- return label.to_lower() if not label.is_empty() else "unknown key"
-
-
-static func bindings_conflict(first: Dictionary, second: Dictionary) -> bool:
- if str(first.get("kind", "")) != str(second.get("kind", "")):
- return false
- if str(first.get("kind", "")) == "mouse_button":
- return int(first.get("button", 0)) == int(second.get("button", 0))
- if str(first.get("kind", "")) != "key":
- return false
- return (
- int(first.get("keycode", 0)) == int(second.get("keycode", 0))
- and int(first.get("physical_keycode", 0))
- == int(second.get("physical_keycode", 0))
- and bool(first.get("alt", false)) == bool(second.get("alt", false))
- and bool(first.get("shift", false)) == bool(second.get("shift", false))
- and bool(first.get("ctrl", false)) == bool(second.get("ctrl", false))
- and bool(first.get("meta", false)) == bool(second.get("meta", false))
- )
-
-
-func _capture_project_defaults() -> void:
- _default_events.clear()
- _default_bindings.clear()
- for role: StringName in ROLE_ORDER:
- var action: StringName = role
- var events: Array[InputEvent] = []
- if InputMap.has_action(action):
- for event: InputEvent in InputMap.action_get_events(action):
- if not _is_keyboard_mouse_event(event):
- continue
- events.append(event.duplicate())
- if not _default_bindings.has(str(role)):
- var binding: Dictionary = binding_from_event(
- _pressed_copy(event)
- )
- if not binding.is_empty():
- _default_bindings[str(role)] = binding
- _default_events[action] = events
-
-
-func _validate_complete_bindings(bindings: Dictionary) -> bool:
- for role: StringName in ROLE_ORDER:
- var raw_binding: Variant = bindings.get(str(role), {})
- if (
- typeof(raw_binding) != TYPE_DICTIONARY
- or not validate_binding(raw_binding as Dictionary)
- ):
- return false
- return true
-
-
-func _apply_active_mapping() -> void:
- if not has_custom_mapping():
- _restore_project_defaults()
- return
- _remove_managed_keyboard_mouse_events()
- for role: StringName in ROLE_ORDER:
- var binding: Variant = _bindings.get(str(role), {})
- if typeof(binding) != TYPE_DICTIONARY:
- continue
- var event: InputEvent = _event_from_binding(binding as Dictionary)
- if event != null:
- InputMap.action_add_event(role, event)
-
-
-func _restore_project_defaults() -> void:
- _remove_managed_keyboard_mouse_events()
- for action_value: Variant in _default_events:
- var action: StringName = StringName(action_value)
- for event: InputEvent in _default_events[action]:
- InputMap.action_add_event(action, event.duplicate())
-
-
-func _remove_managed_keyboard_mouse_events() -> void:
- for action: StringName in ROLE_ORDER:
- if not InputMap.has_action(action):
- continue
- for event: InputEvent in InputMap.action_get_events(action):
- if _is_keyboard_mouse_event(event):
- InputMap.action_erase_event(action, event)
-
-
-func _event_from_binding(binding: Dictionary) -> InputEvent:
- if not validate_binding(binding):
- return null
- if str(binding.get("kind", "")) == "mouse_button":
- var mouse_event := InputEventMouseButton.new()
- mouse_event.button_index = int(binding.get("button", 0)) as MouseButton
- return mouse_event
- var key_event := InputEventKey.new()
- key_event.keycode = int(binding.get("keycode", 0)) as Key
- key_event.physical_keycode = int(
- binding.get("physical_keycode", 0)
- ) as Key
- key_event.alt_pressed = bool(binding.get("alt", false))
- key_event.shift_pressed = bool(binding.get("shift", false))
- key_event.ctrl_pressed = bool(binding.get("ctrl", false))
- key_event.meta_pressed = bool(binding.get("meta", false))
- key_event.location = int(binding.get("location", 0)) as KeyLocation
- return key_event
-
-
-static func _pressed_copy(event: InputEvent) -> InputEvent:
- var result: InputEvent = event.duplicate()
- if result is InputEventKey:
- (result as InputEventKey).pressed = true
- elif result is InputEventMouseButton:
- (result as InputEventMouseButton).pressed = true
- return result
-
-
-static func _is_keyboard_mouse_event(event: InputEvent) -> bool:
- return event is InputEventKey or event is InputEventMouseButton
-
-
-static func _mouse_button_label(button: int) -> String:
- var labels: Dictionary = {
- int(MOUSE_BUTTON_LEFT): "mouse left",
- int(MOUSE_BUTTON_RIGHT): "mouse right",
- int(MOUSE_BUTTON_MIDDLE): "mouse middle",
- int(MOUSE_BUTTON_WHEEL_UP): "wheel up",
- int(MOUSE_BUTTON_WHEEL_DOWN): "wheel down",
- int(MOUSE_BUTTON_WHEEL_LEFT): "wheel left",
- int(MOUSE_BUTTON_WHEEL_RIGHT): "wheel right",
- int(MOUSE_BUTTON_XBUTTON1): "mouse 4",
- int(MOUSE_BUTTON_XBUTTON2): "mouse 5",
- }
- return str(labels.get(button, "mouse %d" % button))
-
-
-func _save_mapping() -> bool:
- var file := FileAccess.open(MAPPING_TEMP_PATH, FileAccess.WRITE)
- if file == null:
- return false
- file.store_string(JSON.stringify({
- "format_version": FORMAT_VERSION,
- "bindings": _bindings,
- }, "\t"))
- file.flush()
- var write_error: Error = file.get_error()
- file.close()
- if write_error != OK:
- _remove_if_present(MAPPING_TEMP_PATH)
- return false
- _remove_if_present(MAPPING_BACKUP_PATH)
- var had_primary: bool = FileAccess.file_exists(MAPPING_PATH)
- if had_primary and not _rename_file(MAPPING_PATH, MAPPING_BACKUP_PATH):
- _remove_if_present(MAPPING_TEMP_PATH)
- return false
- if not _rename_file(MAPPING_TEMP_PATH, MAPPING_PATH):
- if had_primary:
- _rename_file(MAPPING_BACKUP_PATH, MAPPING_PATH)
- return false
- _remove_if_present(MAPPING_BACKUP_PATH)
- return true
-
-
-func _recover_interrupted_write() -> void:
- if FileAccess.file_exists(MAPPING_PATH):
- _remove_if_present(MAPPING_TEMP_PATH)
- _remove_if_present(MAPPING_BACKUP_PATH)
- return
- if FileAccess.file_exists(MAPPING_BACKUP_PATH):
- _rename_file(MAPPING_BACKUP_PATH, MAPPING_PATH)
- _remove_if_present(MAPPING_TEMP_PATH)
-
-
-static func _rename_file(from_path: String, to_path: String) -> bool:
- return DirAccess.rename_absolute(
- ProjectSettings.globalize_path(from_path),
- ProjectSettings.globalize_path(to_path),
- ) == OK
-
-
-static func _remove_if_present(path: String) -> bool:
- if not FileAccess.file_exists(path):
- return true
- return DirAccess.remove_absolute(
- ProjectSettings.globalize_path(path)
- ) == OK
diff --git a/settings/keyboard_mouse_mapping_manager.gd.uid b/settings/keyboard_mouse_mapping_manager.gd.uid
deleted file mode 100644
index 7c0679c..0000000
--- a/settings/keyboard_mouse_mapping_manager.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b7ih6m4nte705
diff --git a/settings/player_settings.gd b/settings/player_settings.gd
index 59099fc..44c00a3 100644
--- a/settings/player_settings.gd
+++ b/settings/player_settings.gd
@@ -7,18 +7,12 @@ const MIN_MOUSE_SENSITIVITY: float = 0.001
const MAX_MOUSE_SENSITIVITY: float = 0.012
const MIN_CONTROLLER_SENSITIVITY: float = 0.5
const MAX_CONTROLLER_SENSITIVITY: float = 5.0
-const MIN_AUDIO_VOLUME: float = 0.0
-const MAX_AUDIO_VOLUME: float = 1.0
const MIN_WORLD_PIXEL_SIZE: int = 1
const MAX_WORLD_PIXEL_SIZE: int = 5
const DEFAULT_WORLD_PIXEL_SIZE: int = 3
const MIN_UI_PIXEL_SIZE: int = 1
const MAX_UI_PIXEL_SIZE: int = 5
const DEFAULT_UI_PIXEL_SIZE: int = 3
-const DEFAULT_CHAT_DOCK_RIGHT: bool = false
-const DEFAULT_CHAT_MOBILE_MODE: bool = false
-const DEFAULT_PAINT_DOCK_RIGHT: bool = true
-const MAX_CHAT_DRAFT_CHARACTERS: int = 256
const COMPACT_DISPLAY_HEIGHT: int = 560
const WORLD_DESKTOP_GRID_HEIGHTS: Array[int] = [0, 540, 360, 216, 96]
const WORLD_COMPACT_GRID_HEIGHTS: Array[int] = [0, 360, 240, 144, 72]
@@ -26,25 +20,10 @@ const UI_DESKTOP_RENDER_HEIGHTS: Array[int] = [0, 612, 504, 396, 288]
const UI_COMPACT_RENDER_HEIGHTS: Array[int] = [0, 408, 336, 264, 192]
@export var auto_click_enabled: bool = false
-# Legacy settings-file field. Tuffy is now always used at runtime.
-@export var use_readable_interface_font: bool = true
@export_range(0.10, 0.50, 0.01) var auto_click_interval: float = 0.20
@export_range(0.001, 0.012, 0.0005) var mouse_camera_sensitivity: float = 0.005
@export_range(0.5, 5.0, 0.1) var controller_camera_sensitivity: float = 2.5
@export var invert_camera_y: bool = false
-@export var swap_hotbar_camera_scroll: bool = false
-@export var on_screen_keyboard_enabled: bool = false
-@export var chat_draft: String = ""
-@export var chat_collapsed: bool = false
-@export var chat_dock_right: bool = DEFAULT_CHAT_DOCK_RIGHT
-@export var chat_mobile_mode: bool = DEFAULT_CHAT_MOBILE_MODE
-@export var paint_dock_right: bool = DEFAULT_PAINT_DOCK_RIGHT
-@export var presentation_layout_customized: bool = false
-@export var fullscreen_enabled: bool = false
-@export_range(0.0, 1.0, 0.01) var master_volume: float = 1.0
-@export_range(0.0, 1.0, 0.01) var music_volume: float = 1.0
-@export_range(0.0, 1.0, 0.01) var effects_volume: float = 1.0
-@export_range(0.0, 1.0, 0.01) var environment_volume: float = 1.0
@export_range(1, 5, 1) var world_pixel_size: int = DEFAULT_WORLD_PIXEL_SIZE
@export_range(1, 5, 1) var ui_pixel_size: int = DEFAULT_UI_PIXEL_SIZE
@@ -60,11 +39,6 @@ func is_valid() -> bool:
and is_finite(controller_camera_sensitivity)
and controller_camera_sensitivity >= MIN_CONTROLLER_SENSITIVITY
and controller_camera_sensitivity <= MAX_CONTROLLER_SENSITIVITY
- and _is_valid_audio_volume(master_volume)
- and _is_valid_audio_volume(music_volume)
- and _is_valid_audio_volume(effects_volume)
- and _is_valid_audio_volume(environment_volume)
- and chat_draft.length() <= MAX_CHAT_DRAFT_CHARACTERS
and world_pixel_size >= MIN_WORLD_PIXEL_SIZE
and world_pixel_size <= MAX_WORLD_PIXEL_SIZE
and ui_pixel_size >= MIN_UI_PIXEL_SIZE
@@ -75,51 +49,15 @@ func is_valid() -> bool:
func copy() -> PlayerSettings:
var result := PlayerSettings.new()
result.auto_click_enabled = auto_click_enabled
- result.use_readable_interface_font = true
result.auto_click_interval = auto_click_interval
result.mouse_camera_sensitivity = mouse_camera_sensitivity
result.controller_camera_sensitivity = controller_camera_sensitivity
result.invert_camera_y = invert_camera_y
- result.swap_hotbar_camera_scroll = swap_hotbar_camera_scroll
- result.on_screen_keyboard_enabled = on_screen_keyboard_enabled
- result.chat_draft = chat_draft
- result.chat_collapsed = chat_collapsed
- result.chat_dock_right = chat_dock_right
- result.chat_mobile_mode = chat_mobile_mode
- result.paint_dock_right = paint_dock_right
- result.presentation_layout_customized = presentation_layout_customized
- result.fullscreen_enabled = fullscreen_enabled
- result.master_volume = master_volume
- result.music_volume = music_volume
- result.effects_volume = effects_volume
- result.environment_volume = environment_volume
result.world_pixel_size = world_pixel_size
result.ui_pixel_size = ui_pixel_size
return result
-func normalize_implicit_presentation_layout() -> bool:
- if presentation_layout_customized:
- return false
- var changed: bool = (
- chat_dock_right != DEFAULT_CHAT_DOCK_RIGHT
- or chat_mobile_mode != DEFAULT_CHAT_MOBILE_MODE
- or paint_dock_right != DEFAULT_PAINT_DOCK_RIGHT
- )
- chat_dock_right = DEFAULT_CHAT_DOCK_RIGHT
- chat_mobile_mode = DEFAULT_CHAT_MOBILE_MODE
- paint_dock_right = DEFAULT_PAINT_DOCK_RIGHT
- return changed
-
-
-static func _is_valid_audio_volume(value: float) -> bool:
- return (
- is_finite(value)
- and value >= MIN_AUDIO_VOLUME
- and value <= MAX_AUDIO_VOLUME
- )
-
-
static func get_world_grid_height(
pixel_size: int,
displayed_height: int,
diff --git a/settings/player_settings_manager.gd b/settings/player_settings_manager.gd
index 362abc9..7e989a7 100644
--- a/settings/player_settings_manager.gd
+++ b/settings/player_settings_manager.gd
@@ -5,10 +5,6 @@ const SETTINGS_VERSION: int = 1
const SETTINGS_PATH: String = "user://player_settings.json"
const TEMP_PATH: String = "user://player_settings.json.tmp"
const BACKUP_PATH: String = "user://player_settings.json.backup"
-const SILENT_VOLUME_DB: float = -80.0
-const HEADLESS_WRITE_ENVIRONMENT: String = (
- "straywild_ISOLATED_VALIDATION_STORAGE"
-)
signal settings_changed(settings: PlayerSettings)
@@ -20,14 +16,12 @@ func load_settings() -> bool:
_recover_interrupted_write()
if not FileAccess.file_exists(SETTINGS_PATH):
current_settings = PlayerSettings.new()
- _apply_audio_settings(current_settings)
emit_signal("settings_changed", current_settings)
return true
var file := FileAccess.open(SETTINGS_PATH, FileAccess.READ)
if file == null:
push_warning("Unable to open player settings; using defaults.")
current_settings = PlayerSettings.new()
- _apply_audio_settings(current_settings)
emit_signal("settings_changed", current_settings)
return false
var json := JSON.new()
@@ -48,58 +42,13 @@ func load_settings() -> bool:
var presentation: Dictionary = {}
if typeof(data.get("presentation")) == TYPE_DICTIONARY:
presentation = data["presentation"]
- var audio: Dictionary = {}
- if typeof(data.get("audio")) == TYPE_DICTIONARY:
- audio = data["audio"]
if (
typeof(accessibility.get("auto_click_enabled")) != TYPE_BOOL
or typeof(camera.get("invert_vertical")) != TYPE_BOOL
- or (
- camera.has("swap_hotbar_camera_scroll")
- and typeof(camera["swap_hotbar_camera_scroll"]) != TYPE_BOOL
- )
- or (
- accessibility.has("on_screen_keyboard_enabled")
- and typeof(accessibility["on_screen_keyboard_enabled"]) != TYPE_BOOL
- )
- or (
- accessibility.has("use_readable_interface_font")
- and typeof(accessibility["use_readable_interface_font"]) != TYPE_BOOL
- )
- or (
- presentation.has("chat_draft")
- and typeof(presentation["chat_draft"]) != TYPE_STRING
- )
- or (
- presentation.has("chat_collapsed")
- and typeof(presentation["chat_collapsed"]) != TYPE_BOOL
- )
- or (
- presentation.has("chat_dock_right")
- and typeof(presentation["chat_dock_right"]) != TYPE_BOOL
- )
- or (
- presentation.has("chat_mobile_mode")
- and typeof(presentation["chat_mobile_mode"]) != TYPE_BOOL
- )
- or (
- presentation.has("paint_dock_right")
- and typeof(presentation["paint_dock_right"]) != TYPE_BOOL
- )
- or (
- presentation.has("layout_customized")
- and typeof(presentation["layout_customized"]) != TYPE_BOOL
- )
- or (
- presentation.has("fullscreen_enabled")
- and typeof(presentation["fullscreen_enabled"]) != TYPE_BOOL
- )
):
return _use_defaults_after_corruption("Player settings values are invalid.")
var loaded := PlayerSettings.new()
loaded.auto_click_enabled = accessibility["auto_click_enabled"]
- # Accept the legacy field, but normalize runtime and future saved state.
- loaded.use_readable_interface_font = true
loaded.auto_click_interval = _read_float(
accessibility.get("auto_click_interval"),
-1.0
@@ -113,12 +62,6 @@ func load_settings() -> bool:
-1.0
)
loaded.invert_camera_y = camera["invert_vertical"]
- loaded.swap_hotbar_camera_scroll = bool(
- camera.get("swap_hotbar_camera_scroll", false)
- )
- loaded.on_screen_keyboard_enabled = bool(
- accessibility.get("on_screen_keyboard_enabled", false)
- )
loaded.world_pixel_size = _read_clamped_integer(
presentation.get(
"world_pixel_size",
@@ -137,35 +80,10 @@ func load_settings() -> bool:
PlayerSettings.MAX_UI_PIXEL_SIZE,
PlayerSettings.DEFAULT_UI_PIXEL_SIZE
)
- loaded.chat_draft = str(presentation.get("chat_draft", "")).left(
- PlayerSettings.MAX_CHAT_DRAFT_CHARACTERS
- )
- loaded.chat_collapsed = bool(presentation.get("chat_collapsed", false))
- loaded.chat_dock_right = bool(presentation.get("chat_dock_right", false))
- loaded.chat_mobile_mode = bool(
- presentation.get("chat_mobile_mode", false)
- )
- loaded.paint_dock_right = bool(presentation.get("paint_dock_right", true))
- loaded.presentation_layout_customized = bool(
- presentation.get("layout_customized", false)
- )
- var normalized_implicit_layout: bool = (
- loaded.normalize_implicit_presentation_layout()
- )
- loaded.fullscreen_enabled = bool(
- presentation.get("fullscreen_enabled", false)
- )
- loaded.master_volume = _read_float(audio.get("master", 1.0), 1.0)
- loaded.music_volume = _read_float(audio.get("music", 1.0), 1.0)
- loaded.effects_volume = _read_float(audio.get("effects", 1.0), 1.0)
- loaded.environment_volume = _read_float(
- audio.get("environment", 1.0), 1.0
- )
if not loaded.is_valid():
return _use_defaults_after_corruption("Player settings values are invalid.")
current_settings = loaded
- _is_dirty = normalized_implicit_layout
- _apply_audio_settings(current_settings)
+ _is_dirty = false
emit_signal("settings_changed", current_settings)
return true
@@ -174,15 +92,11 @@ func apply_settings(settings: PlayerSettings) -> bool:
if settings == null or not settings.is_valid():
return false
var previous: PlayerSettings = current_settings
- var normalized_settings := settings.copy()
- normalized_settings.normalize_implicit_presentation_layout()
- current_settings = normalized_settings
+ current_settings = settings.copy()
_is_dirty = true
if not save_now():
current_settings = previous
- _apply_audio_settings(current_settings)
return false
- _apply_audio_settings(current_settings)
emit_signal("settings_changed", current_settings)
return true
@@ -202,32 +116,7 @@ func apply_pixelation(world_pixel_size: int, ui_pixel_size: int) -> bool:
return apply_settings(edited)
-func update_chat_preferences(draft: String, collapsed: bool) -> void:
- var clean_draft := draft.left(PlayerSettings.MAX_CHAT_DRAFT_CHARACTERS)
- if (
- current_settings.chat_draft == clean_draft
- and current_settings.chat_collapsed == collapsed
- ):
- return
- current_settings.chat_draft = clean_draft
- current_settings.chat_collapsed = collapsed
- _is_dirty = true
-
-
-func save_chat_preferences() -> bool:
- return save_if_dirty()
-
-
func save_now() -> bool:
- if (
- OS.has_feature("editor")
- and DisplayServer.get_name() == "headless"
- and not OS.has_environment(HEADLESS_WRITE_ENVIRONMENT)
- ):
- push_error(
- "Refusing to write player settings from an unisolated headless editor run."
- )
- return false
if current_settings == null or not current_settings.is_valid():
return false
var data: Dictionary = {
@@ -235,41 +124,17 @@ func save_now() -> bool:
"accessibility": {
"auto_click_enabled": current_settings.auto_click_enabled,
"auto_click_interval": current_settings.auto_click_interval,
- "use_readable_interface_font": true,
- "on_screen_keyboard_enabled": (
- current_settings.on_screen_keyboard_enabled
- ),
},
"camera": {
"mouse_sensitivity": current_settings.mouse_camera_sensitivity,
"controller_sensitivity": current_settings.controller_camera_sensitivity,
"invert_vertical": current_settings.invert_camera_y,
- "swap_hotbar_camera_scroll": (
- current_settings.swap_hotbar_camera_scroll
- ),
- },
- "audio": {
- "master": current_settings.master_volume,
- "music": current_settings.music_volume,
- "effects": current_settings.effects_volume,
- "environment": current_settings.environment_volume,
},
"presentation": {
"world_pixel_size": current_settings.world_pixel_size,
"ui_pixel_size": current_settings.ui_pixel_size,
- "chat_draft": current_settings.chat_draft,
- "chat_collapsed": current_settings.chat_collapsed,
- "chat_dock_right": current_settings.chat_dock_right,
- "chat_mobile_mode": current_settings.chat_mobile_mode,
- "paint_dock_right": current_settings.paint_dock_right,
- "layout_customized": (
- current_settings.presentation_layout_customized
- ),
- "fullscreen_enabled": current_settings.fullscreen_enabled,
},
}
- if current_settings.chat_draft.is_empty():
- (data["presentation"] as Dictionary).erase("chat_draft")
var file := FileAccess.open(TEMP_PATH, FileAccess.WRITE)
if file == null:
return false
@@ -298,22 +163,6 @@ func save_if_dirty() -> bool:
return not _is_dirty or save_now()
-func preview_audio_levels(
- master: float,
- music: float,
- effects: float,
- environment: float,
-) -> void:
- _set_bus_volume(&"Master", master)
- _set_bus_volume(&"Music", music)
- _set_bus_volume(&"SFX", effects)
- _set_bus_volume(&"Environment", environment)
-
-
-func restore_audio_levels() -> void:
- _apply_audio_settings(current_settings)
-
-
func _exit_tree() -> void:
save_if_dirty()
@@ -322,38 +171,10 @@ func _use_defaults_after_corruption(message: String) -> bool:
push_warning(message)
current_settings = PlayerSettings.new()
_is_dirty = false
- _apply_audio_settings(current_settings)
emit_signal("settings_changed", current_settings)
return false
-func _apply_audio_settings(settings: PlayerSettings) -> void:
- preview_audio_levels(
- settings.master_volume,
- settings.music_volume,
- settings.effects_volume,
- settings.environment_volume,
- )
-
-
-func _set_bus_volume(bus_name: StringName, linear_volume: float) -> void:
- var bus_index: int = AudioServer.get_bus_index(bus_name)
- if bus_index < 0:
- return
- var resolved_volume: float = clampf(
- linear_volume,
- PlayerSettings.MIN_AUDIO_VOLUME,
- PlayerSettings.MAX_AUDIO_VOLUME,
- )
- AudioServer.set_bus_mute(bus_index, resolved_volume <= 0.0)
- AudioServer.set_bus_volume_db(
- bus_index,
- SILENT_VOLUME_DB
- if resolved_volume <= 0.0
- else linear_to_db(resolved_volume),
- )
-
-
func _recover_interrupted_write() -> void:
if FileAccess.file_exists(SETTINGS_PATH):
_remove_if_present(TEMP_PATH)
diff --git a/sound/dialogue/animalese/alphanumeric/0.wav b/sound/dialogue/animalese/alphanumeric/0.wav
deleted file mode 100644
index 779e725..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/0.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/0.wav.import b/sound/dialogue/animalese/alphanumeric/0.wav.import
deleted file mode 100644
index 19ac04b..0000000
--- a/sound/dialogue/animalese/alphanumeric/0.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cbn5ed728rsm"
-path="res://.godot/imported/0.wav-9378d7763e1ef87ae982ab57f6e81f2d.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/0.wav"
-dest_files=["res://.godot/imported/0.wav-9378d7763e1ef87ae982ab57f6e81f2d.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/1.wav b/sound/dialogue/animalese/alphanumeric/1.wav
deleted file mode 100644
index db7dd62..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/1.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/1.wav.import b/sound/dialogue/animalese/alphanumeric/1.wav.import
deleted file mode 100644
index fa2905a..0000000
--- a/sound/dialogue/animalese/alphanumeric/1.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://8ofrxxya8xo8"
-path="res://.godot/imported/1.wav-759e5f8cac5cef5d0e23605844f5032c.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/1.wav"
-dest_files=["res://.godot/imported/1.wav-759e5f8cac5cef5d0e23605844f5032c.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/2.wav b/sound/dialogue/animalese/alphanumeric/2.wav
deleted file mode 100644
index 74dd315..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/2.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/2.wav.import b/sound/dialogue/animalese/alphanumeric/2.wav.import
deleted file mode 100644
index 800b899..0000000
--- a/sound/dialogue/animalese/alphanumeric/2.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cmxvxip3pwuun"
-path="res://.godot/imported/2.wav-4b3515a4b79f8ae9ffc829c8a5e3c285.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/2.wav"
-dest_files=["res://.godot/imported/2.wav-4b3515a4b79f8ae9ffc829c8a5e3c285.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/3.wav b/sound/dialogue/animalese/alphanumeric/3.wav
deleted file mode 100644
index 6f90835..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/3.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/3.wav.import b/sound/dialogue/animalese/alphanumeric/3.wav.import
deleted file mode 100644
index 5430aea..0000000
--- a/sound/dialogue/animalese/alphanumeric/3.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://c8017waek7ew8"
-path="res://.godot/imported/3.wav-ffb8f531dceaa153b492b74c4bbf7c7c.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/3.wav"
-dest_files=["res://.godot/imported/3.wav-ffb8f531dceaa153b492b74c4bbf7c7c.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/4.wav b/sound/dialogue/animalese/alphanumeric/4.wav
deleted file mode 100644
index 061b5da..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/4.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/4.wav.import b/sound/dialogue/animalese/alphanumeric/4.wav.import
deleted file mode 100644
index 65e2fa4..0000000
--- a/sound/dialogue/animalese/alphanumeric/4.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://bbtypop1cmrw1"
-path="res://.godot/imported/4.wav-effad197373ffb9b48afe78e0fe6c95a.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/4.wav"
-dest_files=["res://.godot/imported/4.wav-effad197373ffb9b48afe78e0fe6c95a.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/5.wav b/sound/dialogue/animalese/alphanumeric/5.wav
deleted file mode 100644
index a6c5000..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/5.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/5.wav.import b/sound/dialogue/animalese/alphanumeric/5.wav.import
deleted file mode 100644
index cddbc1f..0000000
--- a/sound/dialogue/animalese/alphanumeric/5.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://ddwhh6igah1ok"
-path="res://.godot/imported/5.wav-2cd0baa0afa291660575e1fc431a6b91.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/5.wav"
-dest_files=["res://.godot/imported/5.wav-2cd0baa0afa291660575e1fc431a6b91.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/6.wav b/sound/dialogue/animalese/alphanumeric/6.wav
deleted file mode 100644
index 41154f8..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/6.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/6.wav.import b/sound/dialogue/animalese/alphanumeric/6.wav.import
deleted file mode 100644
index 4cb51b2..0000000
--- a/sound/dialogue/animalese/alphanumeric/6.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cqsddej565w48"
-path="res://.godot/imported/6.wav-8c88e3785af1748d3cc8334b825655e1.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/6.wav"
-dest_files=["res://.godot/imported/6.wav-8c88e3785af1748d3cc8334b825655e1.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/7.wav b/sound/dialogue/animalese/alphanumeric/7.wav
deleted file mode 100644
index e732f42..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/7.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/7.wav.import b/sound/dialogue/animalese/alphanumeric/7.wav.import
deleted file mode 100644
index 7ed72ee..0000000
--- a/sound/dialogue/animalese/alphanumeric/7.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://de5lwv3vtq4jo"
-path="res://.godot/imported/7.wav-aa6620dad62f7a8fa68ac3dfe0e8bce0.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/7.wav"
-dest_files=["res://.godot/imported/7.wav-aa6620dad62f7a8fa68ac3dfe0e8bce0.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/8.wav b/sound/dialogue/animalese/alphanumeric/8.wav
deleted file mode 100644
index 2779375..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/8.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/8.wav.import b/sound/dialogue/animalese/alphanumeric/8.wav.import
deleted file mode 100644
index 4733d6c..0000000
--- a/sound/dialogue/animalese/alphanumeric/8.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://lsklvocqcohu"
-path="res://.godot/imported/8.wav-ee305634b68bbc3c852777570ad90d81.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/8.wav"
-dest_files=["res://.godot/imported/8.wav-ee305634b68bbc3c852777570ad90d81.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/9.wav b/sound/dialogue/animalese/alphanumeric/9.wav
deleted file mode 100644
index 644d326..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/9.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/9.wav.import b/sound/dialogue/animalese/alphanumeric/9.wav.import
deleted file mode 100644
index 06be5cb..0000000
--- a/sound/dialogue/animalese/alphanumeric/9.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cuwg5n2w8trdf"
-path="res://.godot/imported/9.wav-2dbc1a5726fda3c5d3e5b8a5c940e14e.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/9.wav"
-dest_files=["res://.godot/imported/9.wav-2dbc1a5726fda3c5d3e5b8a5c940e14e.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/a.wav b/sound/dialogue/animalese/alphanumeric/a.wav
deleted file mode 100644
index cd50a8b..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/a.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/a.wav.import b/sound/dialogue/animalese/alphanumeric/a.wav.import
deleted file mode 100644
index f058d25..0000000
--- a/sound/dialogue/animalese/alphanumeric/a.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://dnnf2sfntra41"
-path="res://.godot/imported/a.wav-07b66f5c4c337a0412ac22c096725756.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/a.wav"
-dest_files=["res://.godot/imported/a.wav-07b66f5c4c337a0412ac22c096725756.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/b.wav b/sound/dialogue/animalese/alphanumeric/b.wav
deleted file mode 100644
index 56c49e2..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/b.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/b.wav.import b/sound/dialogue/animalese/alphanumeric/b.wav.import
deleted file mode 100644
index 82c50e4..0000000
--- a/sound/dialogue/animalese/alphanumeric/b.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cxueylrkf0358"
-path="res://.godot/imported/b.wav-4eb4b2c71e0d5f50efbe3fc8793801e4.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/b.wav"
-dest_files=["res://.godot/imported/b.wav-4eb4b2c71e0d5f50efbe3fc8793801e4.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/c.wav b/sound/dialogue/animalese/alphanumeric/c.wav
deleted file mode 100644
index 6531a4a..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/c.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/c.wav.import b/sound/dialogue/animalese/alphanumeric/c.wav.import
deleted file mode 100644
index 5fc9844..0000000
--- a/sound/dialogue/animalese/alphanumeric/c.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cbwe5uie8fa6k"
-path="res://.godot/imported/c.wav-86a3dcc44eaabde775a8efc14c630f27.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/c.wav"
-dest_files=["res://.godot/imported/c.wav-86a3dcc44eaabde775a8efc14c630f27.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/d.wav b/sound/dialogue/animalese/alphanumeric/d.wav
deleted file mode 100644
index 3838dd7..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/d.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/d.wav.import b/sound/dialogue/animalese/alphanumeric/d.wav.import
deleted file mode 100644
index c58cbf9..0000000
--- a/sound/dialogue/animalese/alphanumeric/d.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://yyr410g00p5p"
-path="res://.godot/imported/d.wav-5577435bb675e68970dd9de887fff671.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/d.wav"
-dest_files=["res://.godot/imported/d.wav-5577435bb675e68970dd9de887fff671.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/e.wav b/sound/dialogue/animalese/alphanumeric/e.wav
deleted file mode 100644
index 8ba7d24..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/e.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/e.wav.import b/sound/dialogue/animalese/alphanumeric/e.wav.import
deleted file mode 100644
index 8d384a2..0000000
--- a/sound/dialogue/animalese/alphanumeric/e.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cp6stdrpayltv"
-path="res://.godot/imported/e.wav-6373ce27ee05eb89c9d510ca465a24d1.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/e.wav"
-dest_files=["res://.godot/imported/e.wav-6373ce27ee05eb89c9d510ca465a24d1.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/f.wav b/sound/dialogue/animalese/alphanumeric/f.wav
deleted file mode 100644
index 0ec0644..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/f.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/f.wav.import b/sound/dialogue/animalese/alphanumeric/f.wav.import
deleted file mode 100644
index fdc83d0..0000000
--- a/sound/dialogue/animalese/alphanumeric/f.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://bc1lj4kba0fr1"
-path="res://.godot/imported/f.wav-637c7bb130969a55972a1c232962cb40.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/f.wav"
-dest_files=["res://.godot/imported/f.wav-637c7bb130969a55972a1c232962cb40.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/g.wav b/sound/dialogue/animalese/alphanumeric/g.wav
deleted file mode 100644
index c942d53..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/g.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/g.wav.import b/sound/dialogue/animalese/alphanumeric/g.wav.import
deleted file mode 100644
index f643d8d..0000000
--- a/sound/dialogue/animalese/alphanumeric/g.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://6k1d81vrewm7"
-path="res://.godot/imported/g.wav-73da4ecac29d03a5e0c1bb2463bcb6cb.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/g.wav"
-dest_files=["res://.godot/imported/g.wav-73da4ecac29d03a5e0c1bb2463bcb6cb.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/h.wav b/sound/dialogue/animalese/alphanumeric/h.wav
deleted file mode 100644
index 545915f..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/h.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/h.wav.import b/sound/dialogue/animalese/alphanumeric/h.wav.import
deleted file mode 100644
index c33b3bf..0000000
--- a/sound/dialogue/animalese/alphanumeric/h.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://boatg77atys5a"
-path="res://.godot/imported/h.wav-44456bd4f004ee75088b13cb2069e3fb.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/h.wav"
-dest_files=["res://.godot/imported/h.wav-44456bd4f004ee75088b13cb2069e3fb.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/i.wav b/sound/dialogue/animalese/alphanumeric/i.wav
deleted file mode 100644
index f464cbc..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/i.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/i.wav.import b/sound/dialogue/animalese/alphanumeric/i.wav.import
deleted file mode 100644
index 51b5c36..0000000
--- a/sound/dialogue/animalese/alphanumeric/i.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cvru47ct1d7mj"
-path="res://.godot/imported/i.wav-8ce67b78d17a9ac111433177c3ed850b.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/i.wav"
-dest_files=["res://.godot/imported/i.wav-8ce67b78d17a9ac111433177c3ed850b.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/j.wav b/sound/dialogue/animalese/alphanumeric/j.wav
deleted file mode 100644
index 1381026..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/j.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/j.wav.import b/sound/dialogue/animalese/alphanumeric/j.wav.import
deleted file mode 100644
index 83202fe..0000000
--- a/sound/dialogue/animalese/alphanumeric/j.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://dwi1wq2dyli5y"
-path="res://.godot/imported/j.wav-c07aa6b0f20fa8fcc89a706a3391c11c.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/j.wav"
-dest_files=["res://.godot/imported/j.wav-c07aa6b0f20fa8fcc89a706a3391c11c.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/k.wav b/sound/dialogue/animalese/alphanumeric/k.wav
deleted file mode 100644
index 3995b27..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/k.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/k.wav.import b/sound/dialogue/animalese/alphanumeric/k.wav.import
deleted file mode 100644
index 8cbff42..0000000
--- a/sound/dialogue/animalese/alphanumeric/k.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://6dkc31pv1ey1"
-path="res://.godot/imported/k.wav-c6fde0aff33080ec99703bdd4fa87e80.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/k.wav"
-dest_files=["res://.godot/imported/k.wav-c6fde0aff33080ec99703bdd4fa87e80.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/l.wav b/sound/dialogue/animalese/alphanumeric/l.wav
deleted file mode 100644
index e0f7ae7..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/l.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/l.wav.import b/sound/dialogue/animalese/alphanumeric/l.wav.import
deleted file mode 100644
index 8c05113..0000000
--- a/sound/dialogue/animalese/alphanumeric/l.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://d2o1hk6e4bbss"
-path="res://.godot/imported/l.wav-722141f795e13543172ffd960308b24c.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/l.wav"
-dest_files=["res://.godot/imported/l.wav-722141f795e13543172ffd960308b24c.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/m.wav b/sound/dialogue/animalese/alphanumeric/m.wav
deleted file mode 100644
index 6ea69d4..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/m.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/m.wav.import b/sound/dialogue/animalese/alphanumeric/m.wav.import
deleted file mode 100644
index c5f005b..0000000
--- a/sound/dialogue/animalese/alphanumeric/m.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://c588ew7t8dwse"
-path="res://.godot/imported/m.wav-2ad62bfb1ad90a0667f708c5154de5dc.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/m.wav"
-dest_files=["res://.godot/imported/m.wav-2ad62bfb1ad90a0667f708c5154de5dc.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/n.wav b/sound/dialogue/animalese/alphanumeric/n.wav
deleted file mode 100644
index 636914e..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/n.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/n.wav.import b/sound/dialogue/animalese/alphanumeric/n.wav.import
deleted file mode 100644
index 455c90b..0000000
--- a/sound/dialogue/animalese/alphanumeric/n.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://mc1vnfuhpuwr"
-path="res://.godot/imported/n.wav-b995292a9ce9755047a5a6a6166a4a4c.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/n.wav"
-dest_files=["res://.godot/imported/n.wav-b995292a9ce9755047a5a6a6166a4a4c.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/o.wav b/sound/dialogue/animalese/alphanumeric/o.wav
deleted file mode 100644
index 0161399..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/o.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/o.wav.import b/sound/dialogue/animalese/alphanumeric/o.wav.import
deleted file mode 100644
index 655e8fe..0000000
--- a/sound/dialogue/animalese/alphanumeric/o.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://rkmixmwg8vgp"
-path="res://.godot/imported/o.wav-696f77252311afda46e416ece590eb55.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/o.wav"
-dest_files=["res://.godot/imported/o.wav-696f77252311afda46e416ece590eb55.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/p.wav b/sound/dialogue/animalese/alphanumeric/p.wav
deleted file mode 100644
index 99bf4fd..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/p.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/p.wav.import b/sound/dialogue/animalese/alphanumeric/p.wav.import
deleted file mode 100644
index 96ae91b..0000000
--- a/sound/dialogue/animalese/alphanumeric/p.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://blbcb70gci0q2"
-path="res://.godot/imported/p.wav-bc58581e4a5d8761e1c8b91f069f951d.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/p.wav"
-dest_files=["res://.godot/imported/p.wav-bc58581e4a5d8761e1c8b91f069f951d.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/q.wav b/sound/dialogue/animalese/alphanumeric/q.wav
deleted file mode 100644
index 80562cc..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/q.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/q.wav.import b/sound/dialogue/animalese/alphanumeric/q.wav.import
deleted file mode 100644
index 896c936..0000000
--- a/sound/dialogue/animalese/alphanumeric/q.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://bmu30s0vscbvg"
-path="res://.godot/imported/q.wav-b9d907f73d4a59dd6641ffe35c36aaac.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/q.wav"
-dest_files=["res://.godot/imported/q.wav-b9d907f73d4a59dd6641ffe35c36aaac.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/r.wav b/sound/dialogue/animalese/alphanumeric/r.wav
deleted file mode 100644
index b38b3a8..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/r.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/r.wav.import b/sound/dialogue/animalese/alphanumeric/r.wav.import
deleted file mode 100644
index 3007c69..0000000
--- a/sound/dialogue/animalese/alphanumeric/r.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://bgdec44ic6msd"
-path="res://.godot/imported/r.wav-d5ea52bfff9160d4d25d74dd94072208.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/r.wav"
-dest_files=["res://.godot/imported/r.wav-d5ea52bfff9160d4d25d74dd94072208.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/s.wav b/sound/dialogue/animalese/alphanumeric/s.wav
deleted file mode 100644
index 8ba7d24..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/s.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/s.wav.import b/sound/dialogue/animalese/alphanumeric/s.wav.import
deleted file mode 100644
index c48dad9..0000000
--- a/sound/dialogue/animalese/alphanumeric/s.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://draie1dh0muw"
-path="res://.godot/imported/s.wav-0dfb29a8b9a3bf4890d24e227cd0d0c4.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/s.wav"
-dest_files=["res://.godot/imported/s.wav-0dfb29a8b9a3bf4890d24e227cd0d0c4.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/t.wav b/sound/dialogue/animalese/alphanumeric/t.wav
deleted file mode 100644
index f34aa5d..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/t.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/t.wav.import b/sound/dialogue/animalese/alphanumeric/t.wav.import
deleted file mode 100644
index cd55195..0000000
--- a/sound/dialogue/animalese/alphanumeric/t.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://ybmvcpv7prx"
-path="res://.godot/imported/t.wav-d4e7e7dbe4c04e54e9c7edb0d85af81a.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/t.wav"
-dest_files=["res://.godot/imported/t.wav-d4e7e7dbe4c04e54e9c7edb0d85af81a.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/u.wav b/sound/dialogue/animalese/alphanumeric/u.wav
deleted file mode 100644
index c41b3fc..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/u.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/u.wav.import b/sound/dialogue/animalese/alphanumeric/u.wav.import
deleted file mode 100644
index 27b2e8b..0000000
--- a/sound/dialogue/animalese/alphanumeric/u.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://dn0hxgm1ueiv7"
-path="res://.godot/imported/u.wav-b8a534783b71ec701bb0925308d513a2.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/u.wav"
-dest_files=["res://.godot/imported/u.wav-b8a534783b71ec701bb0925308d513a2.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/v.wav b/sound/dialogue/animalese/alphanumeric/v.wav
deleted file mode 100644
index 78b81f9..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/v.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/v.wav.import b/sound/dialogue/animalese/alphanumeric/v.wav.import
deleted file mode 100644
index 294b852..0000000
--- a/sound/dialogue/animalese/alphanumeric/v.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://c5p70bkanhlua"
-path="res://.godot/imported/v.wav-0dc1396f424599662d6812e2e59a3b71.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/v.wav"
-dest_files=["res://.godot/imported/v.wav-0dc1396f424599662d6812e2e59a3b71.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/w.wav b/sound/dialogue/animalese/alphanumeric/w.wav
deleted file mode 100644
index 26cbdc9..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/w.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/w.wav.import b/sound/dialogue/animalese/alphanumeric/w.wav.import
deleted file mode 100644
index 5c13da4..0000000
--- a/sound/dialogue/animalese/alphanumeric/w.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://dr2025tcuq5lq"
-path="res://.godot/imported/w.wav-caf9c9b3fa280c326a6be0c426672562.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/w.wav"
-dest_files=["res://.godot/imported/w.wav-caf9c9b3fa280c326a6be0c426672562.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/x.wav b/sound/dialogue/animalese/alphanumeric/x.wav
deleted file mode 100644
index fa020f7..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/x.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/x.wav.import b/sound/dialogue/animalese/alphanumeric/x.wav.import
deleted file mode 100644
index 95a6a8e..0000000
--- a/sound/dialogue/animalese/alphanumeric/x.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://c2c6lwmfogv1o"
-path="res://.godot/imported/x.wav-6dec8d5d6aa916173970737d9db8f509.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/x.wav"
-dest_files=["res://.godot/imported/x.wav-6dec8d5d6aa916173970737d9db8f509.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/y.wav b/sound/dialogue/animalese/alphanumeric/y.wav
deleted file mode 100644
index 3acdc45..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/y.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/y.wav.import b/sound/dialogue/animalese/alphanumeric/y.wav.import
deleted file mode 100644
index 72ebe0c..0000000
--- a/sound/dialogue/animalese/alphanumeric/y.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://csjurthefub6u"
-path="res://.godot/imported/y.wav-82e4c89a86bc31782eaca5f5ee737bb1.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/y.wav"
-dest_files=["res://.godot/imported/y.wav-82e4c89a86bc31782eaca5f5ee737bb1.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/alphanumeric/z.wav b/sound/dialogue/animalese/alphanumeric/z.wav
deleted file mode 100644
index 779e725..0000000
Binary files a/sound/dialogue/animalese/alphanumeric/z.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/alphanumeric/z.wav.import b/sound/dialogue/animalese/alphanumeric/z.wav.import
deleted file mode 100644
index b089115..0000000
--- a/sound/dialogue/animalese/alphanumeric/z.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://bujyv5lsxarhr"
-path="res://.godot/imported/z.wav-38cc5c83dca1b80b731d307651219c08.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/alphanumeric/z.wav"
-dest_files=["res://.godot/imported/z.wav-38cc5c83dca1b80b731d307651219c08.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/0.wav b/sound/dialogue/animalese/kim/0.wav
deleted file mode 100644
index ed9af43..0000000
Binary files a/sound/dialogue/animalese/kim/0.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/0.wav.import b/sound/dialogue/animalese/kim/0.wav.import
deleted file mode 100644
index cc27672..0000000
--- a/sound/dialogue/animalese/kim/0.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://w8wvtbsovk1y"
-path="res://.godot/imported/0.wav-bbd392e8755139dca852d942c97afaa6.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/0.wav"
-dest_files=["res://.godot/imported/0.wav-bbd392e8755139dca852d942c97afaa6.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/1.wav b/sound/dialogue/animalese/kim/1.wav
deleted file mode 100644
index 102b4f8..0000000
Binary files a/sound/dialogue/animalese/kim/1.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/1.wav.import b/sound/dialogue/animalese/kim/1.wav.import
deleted file mode 100644
index 0169929..0000000
--- a/sound/dialogue/animalese/kim/1.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cnny27n4wbk6s"
-path="res://.godot/imported/1.wav-5303efd25be6d603f73c2c2d77759fce.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/1.wav"
-dest_files=["res://.godot/imported/1.wav-5303efd25be6d603f73c2c2d77759fce.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/2.wav b/sound/dialogue/animalese/kim/2.wav
deleted file mode 100644
index 5559b55..0000000
Binary files a/sound/dialogue/animalese/kim/2.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/2.wav.import b/sound/dialogue/animalese/kim/2.wav.import
deleted file mode 100644
index 26bb35a..0000000
--- a/sound/dialogue/animalese/kim/2.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://daw5c2h7p5yu6"
-path="res://.godot/imported/2.wav-a384b25ebb56567742354475fa551c97.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/2.wav"
-dest_files=["res://.godot/imported/2.wav-a384b25ebb56567742354475fa551c97.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/3.wav b/sound/dialogue/animalese/kim/3.wav
deleted file mode 100644
index 158485a..0000000
Binary files a/sound/dialogue/animalese/kim/3.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/3.wav.import b/sound/dialogue/animalese/kim/3.wav.import
deleted file mode 100644
index c4f479d..0000000
--- a/sound/dialogue/animalese/kim/3.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://dn24y7on5811q"
-path="res://.godot/imported/3.wav-da7f408c6ee782136a7225f6a5b4c2da.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/3.wav"
-dest_files=["res://.godot/imported/3.wav-da7f408c6ee782136a7225f6a5b4c2da.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/4.wav b/sound/dialogue/animalese/kim/4.wav
deleted file mode 100644
index 6d05ec7..0000000
Binary files a/sound/dialogue/animalese/kim/4.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/4.wav.import b/sound/dialogue/animalese/kim/4.wav.import
deleted file mode 100644
index c6a70b3..0000000
--- a/sound/dialogue/animalese/kim/4.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cpbt1fduqk2bc"
-path="res://.godot/imported/4.wav-710dd9e6b25e0147dec2f21cfae1a894.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/4.wav"
-dest_files=["res://.godot/imported/4.wav-710dd9e6b25e0147dec2f21cfae1a894.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/5.wav b/sound/dialogue/animalese/kim/5.wav
deleted file mode 100644
index 5671a38..0000000
Binary files a/sound/dialogue/animalese/kim/5.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/5.wav.import b/sound/dialogue/animalese/kim/5.wav.import
deleted file mode 100644
index bc739df..0000000
--- a/sound/dialogue/animalese/kim/5.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://chf4scjvimr1o"
-path="res://.godot/imported/5.wav-7adb723973f3005425a71be181ad28e9.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/5.wav"
-dest_files=["res://.godot/imported/5.wav-7adb723973f3005425a71be181ad28e9.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/6.wav b/sound/dialogue/animalese/kim/6.wav
deleted file mode 100644
index 379199e..0000000
Binary files a/sound/dialogue/animalese/kim/6.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/6.wav.import b/sound/dialogue/animalese/kim/6.wav.import
deleted file mode 100644
index 3674eb9..0000000
--- a/sound/dialogue/animalese/kim/6.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://phxil12i1qmt"
-path="res://.godot/imported/6.wav-1303e051aae59fdf169def7624379571.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/6.wav"
-dest_files=["res://.godot/imported/6.wav-1303e051aae59fdf169def7624379571.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/7.wav b/sound/dialogue/animalese/kim/7.wav
deleted file mode 100644
index ccfdd5f..0000000
Binary files a/sound/dialogue/animalese/kim/7.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/7.wav.import b/sound/dialogue/animalese/kim/7.wav.import
deleted file mode 100644
index a6cee56..0000000
--- a/sound/dialogue/animalese/kim/7.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://dn1pxla32ab23"
-path="res://.godot/imported/7.wav-cb4bd630248ebe745a1646dc7060d526.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/7.wav"
-dest_files=["res://.godot/imported/7.wav-cb4bd630248ebe745a1646dc7060d526.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/8.wav b/sound/dialogue/animalese/kim/8.wav
deleted file mode 100644
index d21e79c..0000000
Binary files a/sound/dialogue/animalese/kim/8.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/8.wav.import b/sound/dialogue/animalese/kim/8.wav.import
deleted file mode 100644
index 36a3e42..0000000
--- a/sound/dialogue/animalese/kim/8.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://dw3bqqcoeoelf"
-path="res://.godot/imported/8.wav-5f912e71babee0f928483bcc9e2fa9f3.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/8.wav"
-dest_files=["res://.godot/imported/8.wav-5f912e71babee0f928483bcc9e2fa9f3.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/9.wav b/sound/dialogue/animalese/kim/9.wav
deleted file mode 100644
index 70d4515..0000000
Binary files a/sound/dialogue/animalese/kim/9.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/9.wav.import b/sound/dialogue/animalese/kim/9.wav.import
deleted file mode 100644
index 8739b30..0000000
--- a/sound/dialogue/animalese/kim/9.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://b3xwpno2ky226"
-path="res://.godot/imported/9.wav-92fe849d00f43baa22f6338601438d71.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/9.wav"
-dest_files=["res://.godot/imported/9.wav-92fe849d00f43baa22f6338601438d71.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/a.wav b/sound/dialogue/animalese/kim/a.wav
deleted file mode 100644
index 78d2ae6..0000000
Binary files a/sound/dialogue/animalese/kim/a.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/a.wav.import b/sound/dialogue/animalese/kim/a.wav.import
deleted file mode 100644
index c0c8c4b..0000000
--- a/sound/dialogue/animalese/kim/a.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://c72cosdd78vhw"
-path="res://.godot/imported/a.wav-19230b3cafcf4c28b5dd1f5c6133e5a5.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/a.wav"
-dest_files=["res://.godot/imported/a.wav-19230b3cafcf4c28b5dd1f5c6133e5a5.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/b.wav b/sound/dialogue/animalese/kim/b.wav
deleted file mode 100644
index 440592d..0000000
Binary files a/sound/dialogue/animalese/kim/b.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/b.wav.import b/sound/dialogue/animalese/kim/b.wav.import
deleted file mode 100644
index 495146b..0000000
--- a/sound/dialogue/animalese/kim/b.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://caejv7ic8kc83"
-path="res://.godot/imported/b.wav-185deb7af67f5a2c71a0fcd8ad5c9ee8.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/b.wav"
-dest_files=["res://.godot/imported/b.wav-185deb7af67f5a2c71a0fcd8ad5c9ee8.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/c.wav b/sound/dialogue/animalese/kim/c.wav
deleted file mode 100644
index d7f2b03..0000000
Binary files a/sound/dialogue/animalese/kim/c.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/c.wav.import b/sound/dialogue/animalese/kim/c.wav.import
deleted file mode 100644
index 10afb6b..0000000
--- a/sound/dialogue/animalese/kim/c.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cpshexs36p2g0"
-path="res://.godot/imported/c.wav-bd1cbbf5862edfbaca6f9f8b0bb2990b.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/c.wav"
-dest_files=["res://.godot/imported/c.wav-bd1cbbf5862edfbaca6f9f8b0bb2990b.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/d.wav b/sound/dialogue/animalese/kim/d.wav
deleted file mode 100644
index 51b694b..0000000
Binary files a/sound/dialogue/animalese/kim/d.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/d.wav.import b/sound/dialogue/animalese/kim/d.wav.import
deleted file mode 100644
index 6654420..0000000
--- a/sound/dialogue/animalese/kim/d.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://bqhge2ptlm71d"
-path="res://.godot/imported/d.wav-dd6ab85372a6bd92c069003d8685a7bf.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/d.wav"
-dest_files=["res://.godot/imported/d.wav-dd6ab85372a6bd92c069003d8685a7bf.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/e.wav b/sound/dialogue/animalese/kim/e.wav
deleted file mode 100644
index cb2c1c8..0000000
Binary files a/sound/dialogue/animalese/kim/e.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/e.wav.import b/sound/dialogue/animalese/kim/e.wav.import
deleted file mode 100644
index 8a998e2..0000000
--- a/sound/dialogue/animalese/kim/e.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://crfyuh2rii14l"
-path="res://.godot/imported/e.wav-08036c9672bbd5dd6268ec8812ae775f.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/e.wav"
-dest_files=["res://.godot/imported/e.wav-08036c9672bbd5dd6268ec8812ae775f.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/f.wav b/sound/dialogue/animalese/kim/f.wav
deleted file mode 100644
index b1545ff..0000000
Binary files a/sound/dialogue/animalese/kim/f.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/f.wav.import b/sound/dialogue/animalese/kim/f.wav.import
deleted file mode 100644
index 03c1a4d..0000000
--- a/sound/dialogue/animalese/kim/f.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://uk0jqhph802c"
-path="res://.godot/imported/f.wav-82aa6f7c781c80928450893a029aac45.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/f.wav"
-dest_files=["res://.godot/imported/f.wav-82aa6f7c781c80928450893a029aac45.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/g.wav b/sound/dialogue/animalese/kim/g.wav
deleted file mode 100644
index a37ac01..0000000
Binary files a/sound/dialogue/animalese/kim/g.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/g.wav.import b/sound/dialogue/animalese/kim/g.wav.import
deleted file mode 100644
index a9b73ba..0000000
--- a/sound/dialogue/animalese/kim/g.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://1008bpcdr223"
-path="res://.godot/imported/g.wav-372aa2fc0531a484e3322ee3d3b48085.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/g.wav"
-dest_files=["res://.godot/imported/g.wav-372aa2fc0531a484e3322ee3d3b48085.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/h.wav b/sound/dialogue/animalese/kim/h.wav
deleted file mode 100644
index 0c19076..0000000
Binary files a/sound/dialogue/animalese/kim/h.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/h.wav.import b/sound/dialogue/animalese/kim/h.wav.import
deleted file mode 100644
index fb6ec91..0000000
--- a/sound/dialogue/animalese/kim/h.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://bw0ua5pignh1m"
-path="res://.godot/imported/h.wav-f16878738745ac8ee0150078290ea7c5.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/h.wav"
-dest_files=["res://.godot/imported/h.wav-f16878738745ac8ee0150078290ea7c5.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/i.wav b/sound/dialogue/animalese/kim/i.wav
deleted file mode 100644
index cd097c2..0000000
Binary files a/sound/dialogue/animalese/kim/i.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/i.wav.import b/sound/dialogue/animalese/kim/i.wav.import
deleted file mode 100644
index 724e4f5..0000000
--- a/sound/dialogue/animalese/kim/i.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cyv3yum38oqdm"
-path="res://.godot/imported/i.wav-c720580ef88ff0c17cc9e0a9daec4ec2.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/i.wav"
-dest_files=["res://.godot/imported/i.wav-c720580ef88ff0c17cc9e0a9daec4ec2.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/j.wav b/sound/dialogue/animalese/kim/j.wav
deleted file mode 100644
index 718d67e..0000000
Binary files a/sound/dialogue/animalese/kim/j.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/j.wav.import b/sound/dialogue/animalese/kim/j.wav.import
deleted file mode 100644
index 4cc5f64..0000000
--- a/sound/dialogue/animalese/kim/j.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://b7hjrilpfkuxx"
-path="res://.godot/imported/j.wav-e5a60e91add3cfd5587ee2205e401fef.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/j.wav"
-dest_files=["res://.godot/imported/j.wav-e5a60e91add3cfd5587ee2205e401fef.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/k.wav b/sound/dialogue/animalese/kim/k.wav
deleted file mode 100644
index fa1748d..0000000
Binary files a/sound/dialogue/animalese/kim/k.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/k.wav.import b/sound/dialogue/animalese/kim/k.wav.import
deleted file mode 100644
index 171b8a0..0000000
--- a/sound/dialogue/animalese/kim/k.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://n4n7gg0bqbbm"
-path="res://.godot/imported/k.wav-82cc304c4627c5fcf78403e0d82b4066.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/k.wav"
-dest_files=["res://.godot/imported/k.wav-82cc304c4627c5fcf78403e0d82b4066.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/l.wav b/sound/dialogue/animalese/kim/l.wav
deleted file mode 100644
index aea20f2..0000000
Binary files a/sound/dialogue/animalese/kim/l.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/l.wav.import b/sound/dialogue/animalese/kim/l.wav.import
deleted file mode 100644
index 1845e6d..0000000
--- a/sound/dialogue/animalese/kim/l.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://nle3bfrffs5t"
-path="res://.godot/imported/l.wav-4b9d875920578dd42b0265fdc42a4c0d.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/l.wav"
-dest_files=["res://.godot/imported/l.wav-4b9d875920578dd42b0265fdc42a4c0d.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/m.wav b/sound/dialogue/animalese/kim/m.wav
deleted file mode 100644
index 68a5ace..0000000
Binary files a/sound/dialogue/animalese/kim/m.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/m.wav.import b/sound/dialogue/animalese/kim/m.wav.import
deleted file mode 100644
index 5a81393..0000000
--- a/sound/dialogue/animalese/kim/m.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://wia38ojr60e6"
-path="res://.godot/imported/m.wav-f7d07c9e641beaa268666b6d6b350fb0.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/m.wav"
-dest_files=["res://.godot/imported/m.wav-f7d07c9e641beaa268666b6d6b350fb0.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/n.wav b/sound/dialogue/animalese/kim/n.wav
deleted file mode 100644
index b3996d0..0000000
Binary files a/sound/dialogue/animalese/kim/n.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/n.wav.import b/sound/dialogue/animalese/kim/n.wav.import
deleted file mode 100644
index 63058b2..0000000
--- a/sound/dialogue/animalese/kim/n.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://ci70fsfe28t2"
-path="res://.godot/imported/n.wav-19567bad745c477e3e8f72b9737adf99.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/n.wav"
-dest_files=["res://.godot/imported/n.wav-19567bad745c477e3e8f72b9737adf99.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/o.wav b/sound/dialogue/animalese/kim/o.wav
deleted file mode 100644
index 66dd126..0000000
Binary files a/sound/dialogue/animalese/kim/o.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/o.wav.import b/sound/dialogue/animalese/kim/o.wav.import
deleted file mode 100644
index 4ba4147..0000000
--- a/sound/dialogue/animalese/kim/o.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://droru2bo6m2ih"
-path="res://.godot/imported/o.wav-d2b3f3d92542aa1643abd9b106b457de.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/o.wav"
-dest_files=["res://.godot/imported/o.wav-d2b3f3d92542aa1643abd9b106b457de.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/p.wav b/sound/dialogue/animalese/kim/p.wav
deleted file mode 100644
index 25cbb55..0000000
Binary files a/sound/dialogue/animalese/kim/p.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/p.wav.import b/sound/dialogue/animalese/kim/p.wav.import
deleted file mode 100644
index ec826e3..0000000
--- a/sound/dialogue/animalese/kim/p.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://d2gyv7ugi1yam"
-path="res://.godot/imported/p.wav-f16816387176c36971c1a3eb52f1742e.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/p.wav"
-dest_files=["res://.godot/imported/p.wav-f16816387176c36971c1a3eb52f1742e.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/q.wav b/sound/dialogue/animalese/kim/q.wav
deleted file mode 100644
index 887f7d6..0000000
Binary files a/sound/dialogue/animalese/kim/q.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/q.wav.import b/sound/dialogue/animalese/kim/q.wav.import
deleted file mode 100644
index 0e794ee..0000000
--- a/sound/dialogue/animalese/kim/q.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://ci6h5d2oag4an"
-path="res://.godot/imported/q.wav-729a36d49b5b3bd23f1badb802667428.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/q.wav"
-dest_files=["res://.godot/imported/q.wav-729a36d49b5b3bd23f1badb802667428.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/r.wav b/sound/dialogue/animalese/kim/r.wav
deleted file mode 100644
index 1fa9937..0000000
Binary files a/sound/dialogue/animalese/kim/r.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/r.wav.import b/sound/dialogue/animalese/kim/r.wav.import
deleted file mode 100644
index 84c2887..0000000
--- a/sound/dialogue/animalese/kim/r.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://c03fj374ceaqc"
-path="res://.godot/imported/r.wav-74003e87654a77c6a0afc0d80fca9cf3.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/r.wav"
-dest_files=["res://.godot/imported/r.wav-74003e87654a77c6a0afc0d80fca9cf3.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/s.wav b/sound/dialogue/animalese/kim/s.wav
deleted file mode 100644
index 962fd46..0000000
Binary files a/sound/dialogue/animalese/kim/s.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/s.wav.import b/sound/dialogue/animalese/kim/s.wav.import
deleted file mode 100644
index c39714d..0000000
--- a/sound/dialogue/animalese/kim/s.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://d0yb6lesxgm8j"
-path="res://.godot/imported/s.wav-627d8bd9f4cee8c43ac9237705df784f.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/s.wav"
-dest_files=["res://.godot/imported/s.wav-627d8bd9f4cee8c43ac9237705df784f.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/t.wav b/sound/dialogue/animalese/kim/t.wav
deleted file mode 100644
index c5fcc51..0000000
Binary files a/sound/dialogue/animalese/kim/t.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/t.wav.import b/sound/dialogue/animalese/kim/t.wav.import
deleted file mode 100644
index b6d0ae9..0000000
--- a/sound/dialogue/animalese/kim/t.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://bcvrvl4mli82i"
-path="res://.godot/imported/t.wav-aec336b52646bd870831f9effe598c92.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/t.wav"
-dest_files=["res://.godot/imported/t.wav-aec336b52646bd870831f9effe598c92.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/u.wav b/sound/dialogue/animalese/kim/u.wav
deleted file mode 100644
index 56caa56..0000000
Binary files a/sound/dialogue/animalese/kim/u.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/u.wav.import b/sound/dialogue/animalese/kim/u.wav.import
deleted file mode 100644
index 7724a93..0000000
--- a/sound/dialogue/animalese/kim/u.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://mpw8xt0uxm5d"
-path="res://.godot/imported/u.wav-02b540f73efcb10ad4943d4f37bdf90b.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/u.wav"
-dest_files=["res://.godot/imported/u.wav-02b540f73efcb10ad4943d4f37bdf90b.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/v.wav b/sound/dialogue/animalese/kim/v.wav
deleted file mode 100644
index 4add64f..0000000
Binary files a/sound/dialogue/animalese/kim/v.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/v.wav.import b/sound/dialogue/animalese/kim/v.wav.import
deleted file mode 100644
index fb772de..0000000
--- a/sound/dialogue/animalese/kim/v.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://n0r81bxuadfo"
-path="res://.godot/imported/v.wav-d66f5caffb359fad446ec9ddbacdf071.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/v.wav"
-dest_files=["res://.godot/imported/v.wav-d66f5caffb359fad446ec9ddbacdf071.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/w.wav b/sound/dialogue/animalese/kim/w.wav
deleted file mode 100644
index 9020a84..0000000
Binary files a/sound/dialogue/animalese/kim/w.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/w.wav.import b/sound/dialogue/animalese/kim/w.wav.import
deleted file mode 100644
index b6f0a30..0000000
--- a/sound/dialogue/animalese/kim/w.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://dm742m0d4b8o7"
-path="res://.godot/imported/w.wav-46b37c23c004fe793f00e82c587efbcf.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/w.wav"
-dest_files=["res://.godot/imported/w.wav-46b37c23c004fe793f00e82c587efbcf.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/x.wav b/sound/dialogue/animalese/kim/x.wav
deleted file mode 100644
index f147ef3..0000000
Binary files a/sound/dialogue/animalese/kim/x.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/x.wav.import b/sound/dialogue/animalese/kim/x.wav.import
deleted file mode 100644
index 4fcd581..0000000
--- a/sound/dialogue/animalese/kim/x.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cn2my2uukyd8u"
-path="res://.godot/imported/x.wav-90efe4fb55c5c2061c2e9dbc89bfb532.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/x.wav"
-dest_files=["res://.godot/imported/x.wav-90efe4fb55c5c2061c2e9dbc89bfb532.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/y.wav b/sound/dialogue/animalese/kim/y.wav
deleted file mode 100644
index 7f818c8..0000000
Binary files a/sound/dialogue/animalese/kim/y.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/y.wav.import b/sound/dialogue/animalese/kim/y.wav.import
deleted file mode 100644
index 0cfb028..0000000
--- a/sound/dialogue/animalese/kim/y.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://djhq5e17gcqv1"
-path="res://.godot/imported/y.wav-0170ee159f2f8405596f47206d01cf8f.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/y.wav"
-dest_files=["res://.godot/imported/y.wav-0170ee159f2f8405596f47206d01cf8f.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/kim/z.wav b/sound/dialogue/animalese/kim/z.wav
deleted file mode 100644
index ed9af43..0000000
Binary files a/sound/dialogue/animalese/kim/z.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/kim/z.wav.import b/sound/dialogue/animalese/kim/z.wav.import
deleted file mode 100644
index 4a3b319..0000000
--- a/sound/dialogue/animalese/kim/z.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://bhhaol0imq60f"
-path="res://.godot/imported/z.wav-6daaf8e93675aba3840dbf16e9c93ebf.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/kim/z.wav"
-dest_files=["res://.godot/imported/z.wav-6daaf8e93675aba3840dbf16e9c93ebf.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/a.wav b/sound/dialogue/animalese/robot/a.wav
deleted file mode 100644
index 0e434f7..0000000
Binary files a/sound/dialogue/animalese/robot/a.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/a.wav.import b/sound/dialogue/animalese/robot/a.wav.import
deleted file mode 100644
index 6d33ed2..0000000
--- a/sound/dialogue/animalese/robot/a.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cg8vgijwjen1h"
-path="res://.godot/imported/a.wav-912469cf287b9f82df9e0084967c3138.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/a.wav"
-dest_files=["res://.godot/imported/a.wav-912469cf287b9f82df9e0084967c3138.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/b.wav b/sound/dialogue/animalese/robot/b.wav
deleted file mode 100644
index 8e24102..0000000
Binary files a/sound/dialogue/animalese/robot/b.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/b.wav.import b/sound/dialogue/animalese/robot/b.wav.import
deleted file mode 100644
index d858d56..0000000
--- a/sound/dialogue/animalese/robot/b.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://dbn3ilqo34j1l"
-path="res://.godot/imported/b.wav-482bec9aa59b1274ddc061acf0928cc3.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/b.wav"
-dest_files=["res://.godot/imported/b.wav-482bec9aa59b1274ddc061acf0928cc3.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/c.wav b/sound/dialogue/animalese/robot/c.wav
deleted file mode 100644
index 4eb4028..0000000
Binary files a/sound/dialogue/animalese/robot/c.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/c.wav.import b/sound/dialogue/animalese/robot/c.wav.import
deleted file mode 100644
index a32613d..0000000
--- a/sound/dialogue/animalese/robot/c.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://6tmnptljtqms"
-path="res://.godot/imported/c.wav-88fe8cecfa21a5d5cf845d6b8679d13c.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/c.wav"
-dest_files=["res://.godot/imported/c.wav-88fe8cecfa21a5d5cf845d6b8679d13c.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/d.wav b/sound/dialogue/animalese/robot/d.wav
deleted file mode 100644
index eba38a4..0000000
Binary files a/sound/dialogue/animalese/robot/d.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/d.wav.import b/sound/dialogue/animalese/robot/d.wav.import
deleted file mode 100644
index 3732bc7..0000000
--- a/sound/dialogue/animalese/robot/d.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://btrj6qtm84855"
-path="res://.godot/imported/d.wav-ef76f071f3356e954019c5494b963533.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/d.wav"
-dest_files=["res://.godot/imported/d.wav-ef76f071f3356e954019c5494b963533.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/e.wav b/sound/dialogue/animalese/robot/e.wav
deleted file mode 100644
index 4b54f04..0000000
Binary files a/sound/dialogue/animalese/robot/e.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/e.wav.import b/sound/dialogue/animalese/robot/e.wav.import
deleted file mode 100644
index ce7a9c8..0000000
--- a/sound/dialogue/animalese/robot/e.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://esdsylelog50"
-path="res://.godot/imported/e.wav-e44b4d22c43894915ef0134813dd6ec8.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/e.wav"
-dest_files=["res://.godot/imported/e.wav-e44b4d22c43894915ef0134813dd6ec8.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/f.wav b/sound/dialogue/animalese/robot/f.wav
deleted file mode 100644
index e30b889..0000000
Binary files a/sound/dialogue/animalese/robot/f.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/f.wav.import b/sound/dialogue/animalese/robot/f.wav.import
deleted file mode 100644
index c9a3d2f..0000000
--- a/sound/dialogue/animalese/robot/f.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://xb01jed4d7xl"
-path="res://.godot/imported/f.wav-06750a71b9dfd09a72792c85901ef2e0.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/f.wav"
-dest_files=["res://.godot/imported/f.wav-06750a71b9dfd09a72792c85901ef2e0.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/fallback.wav b/sound/dialogue/animalese/robot/fallback.wav
deleted file mode 100644
index 54ca7ca..0000000
Binary files a/sound/dialogue/animalese/robot/fallback.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/fallback.wav.import b/sound/dialogue/animalese/robot/fallback.wav.import
deleted file mode 100644
index a62bdeb..0000000
--- a/sound/dialogue/animalese/robot/fallback.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cebc27kmdv7bt"
-path="res://.godot/imported/fallback.wav-70adb8cf4fa8834ef769dc92854a5f89.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/fallback.wav"
-dest_files=["res://.godot/imported/fallback.wav-70adb8cf4fa8834ef769dc92854a5f89.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/g.wav b/sound/dialogue/animalese/robot/g.wav
deleted file mode 100644
index d659358..0000000
Binary files a/sound/dialogue/animalese/robot/g.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/g.wav.import b/sound/dialogue/animalese/robot/g.wav.import
deleted file mode 100644
index 1c619ad..0000000
--- a/sound/dialogue/animalese/robot/g.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cxnixxqcol7cf"
-path="res://.godot/imported/g.wav-bab051202eead7ae86eb50d16fe7ae4d.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/g.wav"
-dest_files=["res://.godot/imported/g.wav-bab051202eead7ae86eb50d16fe7ae4d.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/h.wav b/sound/dialogue/animalese/robot/h.wav
deleted file mode 100644
index 3363245..0000000
Binary files a/sound/dialogue/animalese/robot/h.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/h.wav.import b/sound/dialogue/animalese/robot/h.wav.import
deleted file mode 100644
index c7a868b..0000000
--- a/sound/dialogue/animalese/robot/h.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cyssmnjalcjh1"
-path="res://.godot/imported/h.wav-82eb606f12a9a3898f9adc04f47242bc.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/h.wav"
-dest_files=["res://.godot/imported/h.wav-82eb606f12a9a3898f9adc04f47242bc.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/i.wav b/sound/dialogue/animalese/robot/i.wav
deleted file mode 100644
index c0b5ce8..0000000
Binary files a/sound/dialogue/animalese/robot/i.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/i.wav.import b/sound/dialogue/animalese/robot/i.wav.import
deleted file mode 100644
index 94f36d8..0000000
--- a/sound/dialogue/animalese/robot/i.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cvng0omshxqb1"
-path="res://.godot/imported/i.wav-efaf75378a992fa714224dedfe31ed34.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/i.wav"
-dest_files=["res://.godot/imported/i.wav-efaf75378a992fa714224dedfe31ed34.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/j.wav b/sound/dialogue/animalese/robot/j.wav
deleted file mode 100644
index f5f2e24..0000000
Binary files a/sound/dialogue/animalese/robot/j.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/j.wav.import b/sound/dialogue/animalese/robot/j.wav.import
deleted file mode 100644
index b9a9adf..0000000
--- a/sound/dialogue/animalese/robot/j.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://b8roqgoeyo3l8"
-path="res://.godot/imported/j.wav-822afb34d9e86d0e6fb2f2e871c11a99.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/j.wav"
-dest_files=["res://.godot/imported/j.wav-822afb34d9e86d0e6fb2f2e871c11a99.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/k.wav b/sound/dialogue/animalese/robot/k.wav
deleted file mode 100644
index f9c6b5c..0000000
Binary files a/sound/dialogue/animalese/robot/k.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/k.wav.import b/sound/dialogue/animalese/robot/k.wav.import
deleted file mode 100644
index ca3086f..0000000
--- a/sound/dialogue/animalese/robot/k.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://b2x5ijnb38tnv"
-path="res://.godot/imported/k.wav-253c9eda1dc82ce4e496727f8805ed63.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/k.wav"
-dest_files=["res://.godot/imported/k.wav-253c9eda1dc82ce4e496727f8805ed63.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/l.wav b/sound/dialogue/animalese/robot/l.wav
deleted file mode 100644
index cc1d512..0000000
Binary files a/sound/dialogue/animalese/robot/l.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/l.wav.import b/sound/dialogue/animalese/robot/l.wav.import
deleted file mode 100644
index 7f77dac..0000000
--- a/sound/dialogue/animalese/robot/l.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://dtobkfled545r"
-path="res://.godot/imported/l.wav-74470cbddad2ea89893332702c74ae8d.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/l.wav"
-dest_files=["res://.godot/imported/l.wav-74470cbddad2ea89893332702c74ae8d.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/m.wav b/sound/dialogue/animalese/robot/m.wav
deleted file mode 100644
index 7359a06..0000000
Binary files a/sound/dialogue/animalese/robot/m.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/m.wav.import b/sound/dialogue/animalese/robot/m.wav.import
deleted file mode 100644
index cc0ebdf..0000000
--- a/sound/dialogue/animalese/robot/m.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://x53vy8vfb0v7"
-path="res://.godot/imported/m.wav-3282c0735e81b1924994c0f641854897.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/m.wav"
-dest_files=["res://.godot/imported/m.wav-3282c0735e81b1924994c0f641854897.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/n.wav b/sound/dialogue/animalese/robot/n.wav
deleted file mode 100644
index a505906..0000000
Binary files a/sound/dialogue/animalese/robot/n.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/n.wav.import b/sound/dialogue/animalese/robot/n.wav.import
deleted file mode 100644
index 3122cfb..0000000
--- a/sound/dialogue/animalese/robot/n.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cktk3r2ociatv"
-path="res://.godot/imported/n.wav-38bfd5021f583b1a472e0c265ddc01e0.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/n.wav"
-dest_files=["res://.godot/imported/n.wav-38bfd5021f583b1a472e0c265ddc01e0.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/o.wav b/sound/dialogue/animalese/robot/o.wav
deleted file mode 100644
index 7e8cb61..0000000
Binary files a/sound/dialogue/animalese/robot/o.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/o.wav.import b/sound/dialogue/animalese/robot/o.wav.import
deleted file mode 100644
index 58596f6..0000000
--- a/sound/dialogue/animalese/robot/o.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://exnucx2mi3nk"
-path="res://.godot/imported/o.wav-c2146198bb8a83542393735ccb97a983.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/o.wav"
-dest_files=["res://.godot/imported/o.wav-c2146198bb8a83542393735ccb97a983.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/p.wav b/sound/dialogue/animalese/robot/p.wav
deleted file mode 100644
index 770c2ea..0000000
Binary files a/sound/dialogue/animalese/robot/p.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/p.wav.import b/sound/dialogue/animalese/robot/p.wav.import
deleted file mode 100644
index 5b0c010..0000000
--- a/sound/dialogue/animalese/robot/p.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://jgnbqcbt0t64"
-path="res://.godot/imported/p.wav-b5d1c85200b8e57e540534fa6782857b.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/p.wav"
-dest_files=["res://.godot/imported/p.wav-b5d1c85200b8e57e540534fa6782857b.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/q.wav b/sound/dialogue/animalese/robot/q.wav
deleted file mode 100644
index f7ec879..0000000
Binary files a/sound/dialogue/animalese/robot/q.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/q.wav.import b/sound/dialogue/animalese/robot/q.wav.import
deleted file mode 100644
index a624666..0000000
--- a/sound/dialogue/animalese/robot/q.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://ycgqs14iexxe"
-path="res://.godot/imported/q.wav-27ccb3c315837cff45c092f2c543d9c3.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/q.wav"
-dest_files=["res://.godot/imported/q.wav-27ccb3c315837cff45c092f2c543d9c3.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/r.wav b/sound/dialogue/animalese/robot/r.wav
deleted file mode 100644
index 9a3b293..0000000
Binary files a/sound/dialogue/animalese/robot/r.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/r.wav.import b/sound/dialogue/animalese/robot/r.wav.import
deleted file mode 100644
index a64a6b2..0000000
--- a/sound/dialogue/animalese/robot/r.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://bun5h3babtwrw"
-path="res://.godot/imported/r.wav-7370fd302d45d0a79de1eb3caf1fa0e2.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/r.wav"
-dest_files=["res://.godot/imported/r.wav-7370fd302d45d0a79de1eb3caf1fa0e2.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/s.wav b/sound/dialogue/animalese/robot/s.wav
deleted file mode 100644
index 0847120..0000000
Binary files a/sound/dialogue/animalese/robot/s.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/s.wav.import b/sound/dialogue/animalese/robot/s.wav.import
deleted file mode 100644
index a65ebe9..0000000
--- a/sound/dialogue/animalese/robot/s.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://ds02uafo1fulk"
-path="res://.godot/imported/s.wav-d021f184e6f257c8c38e99166409779b.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/s.wav"
-dest_files=["res://.godot/imported/s.wav-d021f184e6f257c8c38e99166409779b.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/t.wav b/sound/dialogue/animalese/robot/t.wav
deleted file mode 100644
index fef9bf0..0000000
Binary files a/sound/dialogue/animalese/robot/t.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/t.wav.import b/sound/dialogue/animalese/robot/t.wav.import
deleted file mode 100644
index 86242c0..0000000
--- a/sound/dialogue/animalese/robot/t.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://ouvxq1dsnqsc"
-path="res://.godot/imported/t.wav-12b718527178b570cfd5eedb8c04297e.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/t.wav"
-dest_files=["res://.godot/imported/t.wav-12b718527178b570cfd5eedb8c04297e.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/u.wav b/sound/dialogue/animalese/robot/u.wav
deleted file mode 100644
index d2d148d..0000000
Binary files a/sound/dialogue/animalese/robot/u.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/u.wav.import b/sound/dialogue/animalese/robot/u.wav.import
deleted file mode 100644
index 2703c24..0000000
--- a/sound/dialogue/animalese/robot/u.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://dd3ece0ndg16y"
-path="res://.godot/imported/u.wav-06fb03902c67d2949aa71cdb8e1f8bf0.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/u.wav"
-dest_files=["res://.godot/imported/u.wav-06fb03902c67d2949aa71cdb8e1f8bf0.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/v.wav b/sound/dialogue/animalese/robot/v.wav
deleted file mode 100644
index 32d8341..0000000
Binary files a/sound/dialogue/animalese/robot/v.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/v.wav.import b/sound/dialogue/animalese/robot/v.wav.import
deleted file mode 100644
index a82ae8a..0000000
--- a/sound/dialogue/animalese/robot/v.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cn1reodu8saiy"
-path="res://.godot/imported/v.wav-d3ebbf2d472bce20fe0792395ed4d049.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/v.wav"
-dest_files=["res://.godot/imported/v.wav-d3ebbf2d472bce20fe0792395ed4d049.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/w.wav b/sound/dialogue/animalese/robot/w.wav
deleted file mode 100644
index 68d06c2..0000000
Binary files a/sound/dialogue/animalese/robot/w.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/w.wav.import b/sound/dialogue/animalese/robot/w.wav.import
deleted file mode 100644
index 76cbec4..0000000
--- a/sound/dialogue/animalese/robot/w.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://bfbipoalvo37y"
-path="res://.godot/imported/w.wav-fa8b04d920c43f155d3f4a909a2ee5e6.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/w.wav"
-dest_files=["res://.godot/imported/w.wav-fa8b04d920c43f155d3f4a909a2ee5e6.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/x.wav b/sound/dialogue/animalese/robot/x.wav
deleted file mode 100644
index 24e8604..0000000
Binary files a/sound/dialogue/animalese/robot/x.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/x.wav.import b/sound/dialogue/animalese/robot/x.wav.import
deleted file mode 100644
index d54fe68..0000000
--- a/sound/dialogue/animalese/robot/x.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://e4bx33654fbn"
-path="res://.godot/imported/x.wav-ce5e1553c073860351f5d4de3da6af28.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/x.wav"
-dest_files=["res://.godot/imported/x.wav-ce5e1553c073860351f5d4de3da6af28.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/y.wav b/sound/dialogue/animalese/robot/y.wav
deleted file mode 100644
index 414a176..0000000
Binary files a/sound/dialogue/animalese/robot/y.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/y.wav.import b/sound/dialogue/animalese/robot/y.wav.import
deleted file mode 100644
index 023e8ae..0000000
--- a/sound/dialogue/animalese/robot/y.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://b3ns0qyyvxqgt"
-path="res://.godot/imported/y.wav-f5e15ad92e6316d22a5263c8296dff17.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/y.wav"
-dest_files=["res://.godot/imported/y.wav-f5e15ad92e6316d22a5263c8296dff17.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/animalese/robot/z.wav b/sound/dialogue/animalese/robot/z.wav
deleted file mode 100644
index 243bab2..0000000
Binary files a/sound/dialogue/animalese/robot/z.wav and /dev/null differ
diff --git a/sound/dialogue/animalese/robot/z.wav.import b/sound/dialogue/animalese/robot/z.wav.import
deleted file mode 100644
index 1ee68f8..0000000
--- a/sound/dialogue/animalese/robot/z.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://cu8vaak45mcna"
-path="res://.godot/imported/z.wav-bde1bea473ae6ddcfb32432d5206e265.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/animalese/robot/z.wav"
-dest_files=["res://.godot/imported/z.wav-bde1bea473ae6ddcfb32432d5206e265.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/calls/bark.wav b/sound/dialogue/calls/bark.wav
deleted file mode 100644
index 2629fe7..0000000
Binary files a/sound/dialogue/calls/bark.wav and /dev/null differ
diff --git a/sound/dialogue/calls/bark.wav.import b/sound/dialogue/calls/bark.wav.import
deleted file mode 100644
index 702904d..0000000
--- a/sound/dialogue/calls/bark.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://c8x46srpghnil"
-path="res://.godot/imported/bark.wav-9da6fd5d720375f4a64b26e364fcd74c.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/calls/bark.wav"
-dest_files=["res://.godot/imported/bark.wav-9da6fd5d720375f4a64b26e364fcd74c.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/sound/dialogue/calls/meow.wav b/sound/dialogue/calls/meow.wav
deleted file mode 100644
index 662b416..0000000
Binary files a/sound/dialogue/calls/meow.wav and /dev/null differ
diff --git a/sound/dialogue/calls/meow.wav.import b/sound/dialogue/calls/meow.wav.import
deleted file mode 100644
index 058b09a..0000000
--- a/sound/dialogue/calls/meow.wav.import
+++ /dev/null
@@ -1,24 +0,0 @@
-[remap]
-
-importer="wav"
-type="AudioStreamWAV"
-uid="uid://y86nskf7ewrg"
-path="res://.godot/imported/meow.wav-eb7bd4377946745ae1566c72ca026fae.sample"
-
-[deps]
-
-source_file="res://sound/dialogue/calls/meow.wav"
-dest_files=["res://.godot/imported/meow.wav-eb7bd4377946745ae1566c72ca026fae.sample"]
-
-[params]
-
-force/8_bit=false
-force/mono=false
-force/max_rate=false
-force/max_rate_hz=44100
-edit/trim=false
-edit/normalize=false
-edit/loop_mode=0
-edit/loop_begin=0
-edit/loop_end=-1
-compress/mode=2
diff --git a/tests/android_readiness_validation.gd b/tests/android_readiness_validation.gd
deleted file mode 100644
index 7472063..0000000
--- a/tests/android_readiness_validation.gd
+++ /dev/null
@@ -1,110 +0,0 @@
-extends SceneTree
-
-const GameUIType = preload("res://ui/game_ui.gd")
-const PlayerType = preload("res://player/player.gd")
-
-
-func _init() -> void:
- assert(
- ProjectSettings.get_setting("display/window/handheld/orientation", -1) == 0
- )
- assert(
- ProjectSettings.get_setting(
- "input_devices/pointing/emulate_mouse_from_touch", false
- )
- )
- assert(_has_joypad_motion(&"move_forward", JOY_AXIS_LEFT_Y, -1.0))
- assert(_has_joypad_motion(&"move_backward", JOY_AXIS_LEFT_Y, 1.0))
- assert(_has_joypad_motion(&"move_left", JOY_AXIS_LEFT_X, -1.0))
- assert(_has_joypad_motion(&"move_right", JOY_AXIS_LEFT_X, 1.0))
- assert(_has_joypad_button(&"jump", JOY_BUTTON_A))
- assert(_has_joypad_button(&"ui_accept", JOY_BUTTON_A))
- assert(_has_joypad_button(&"ui_cancel", JOY_BUTTON_B))
- assert(_has_joypad_button(&"interact", JOY_BUTTON_Y))
- assert(_has_joypad_button(&"character_call", JOY_BUTTON_Y))
- assert(_has_joypad_button(&"fish_primary", JOY_BUTTON_RIGHT_SHOULDER))
- assert(not _has_joypad_motion(
- &"fish_primary", JOY_AXIS_TRIGGER_LEFT, 1.0
- ))
- assert(not _has_joypad_motion(
- &"fish_primary", JOY_AXIS_TRIGGER_RIGHT, 1.0
- ))
- assert(_has_joypad_button(&"sprint", JOY_BUTTON_LEFT_STICK))
- assert(not _has_joypad_button(
- &"camera_zoom_out", JOY_BUTTON_LEFT_SHOULDER
- ))
- assert(not _has_joypad_button(
- &"camera_zoom_in", JOY_BUTTON_RIGHT_SHOULDER
- ))
- assert(_has_joypad_button(&"hotbar_previous", JOY_BUTTON_DPAD_LEFT))
- assert(_has_joypad_button(&"hotbar_next", JOY_BUTTON_DPAD_RIGHT))
- assert(_has_joypad_button(&"open_backpack", JOY_BUTTON_X))
- assert(_has_joypad_button(&"open_system_menu", JOY_BUTTON_START))
- assert(_has_joypad_button(&"open_emotes", JOY_BUTTON_DPAD_UP))
- assert(_has_joypad_button(&"open_quick_actions", JOY_BUTTON_DPAD_DOWN))
- assert(_has_joypad_button(&"open_chat", JOY_BUTTON_BACK))
- assert(_has_joypad_button(&"focus_gameplay", JOY_BUTTON_LEFT_SHOULDER))
- assert(
- GameUIType.VIRTUAL_MOUSE_TRIGGER_AXIS == JOY_AXIS_TRIGGER_RIGHT
- )
- assert(
- GameUIType.VIRTUAL_MOUSE_SHARED_TRIGGER_AXIS
- == JOY_AXIS_TRIGGER_LEFT
- )
- assert(
- GameUIType.VIRTUAL_MOUSE_SECONDARY_CLICK_AXIS
- == JOY_AXIS_TRIGGER_LEFT
- )
- assert(PlayerType.CONTROLLER_ZOOM_TRIGGER_AXIS == JOY_AXIS_TRIGGER_LEFT)
- assert(is_zero_approx(
- GameUIType.normalized_trigger_strength(-1.0, -1.0)
- ))
- assert(is_equal_approx(
- GameUIType.normalized_trigger_strength(1.0, -1.0),
- 1.0,
- ))
- assert(is_equal_approx(
- GameUIType.normalized_trigger_strength(-1.0, 0.0),
- 1.0,
- ))
- assert(is_equal_approx(
- GameUIType.normalized_trigger_strength(-1.0, 1.0),
- 1.0,
- ))
- assert(is_equal_approx(
- GameUIType.directional_trigger_strength(-1.0, 0.0, -1.0),
- 1.0,
- ))
- assert(is_zero_approx(
- GameUIType.directional_trigger_strength(1.0, 0.0, -1.0)
- ))
- assert(is_equal_approx(
- GameUIType.directional_trigger_strength(1.0, 0.0, 1.0),
- 1.0,
- ))
- print("android readiness validation passed")
- quit()
-
-
-func _has_joypad_button(action: StringName, button_index: JoyButton) -> bool:
- for event: InputEvent in InputMap.action_get_events(action):
- var button_event: InputEventJoypadButton = event as InputEventJoypadButton
- if button_event != null and button_event.button_index == button_index:
- return true
- return false
-
-
-func _has_joypad_motion(
- action: StringName,
- axis: JoyAxis,
- direction: float,
-) -> bool:
- for event: InputEvent in InputMap.action_get_events(action):
- var motion_event: InputEventJoypadMotion = event as InputEventJoypadMotion
- if (
- motion_event != null
- and motion_event.axis == axis
- and is_equal_approx(motion_event.axis_value, direction)
- ):
- return true
- return false
diff --git a/tests/android_readiness_validation.gd.uid b/tests/android_readiness_validation.gd.uid
deleted file mode 100644
index f499a0b..0000000
--- a/tests/android_readiness_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cbqyx4mntux7l
diff --git a/tests/art_tools_validation.gd b/tests/art_tools_validation.gd
deleted file mode 100644
index 146fb4e..0000000
--- a/tests/art_tools_validation.gd
+++ /dev/null
@@ -1,1027 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-const TEST_PORT: int = 18136
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- assert(bool(main.call(
- "_apply_world", WorldLayout.STARTER_ISLAND, 1, true
- )))
- assert(bool(main.call(
- "_prepare_host_world", WorldLayout.STARTER_ISLAND, 1
- )))
- assert(bool(main.call("_prepare_private_host")))
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND))
- main.call("_enter_gameplay")
- await physics_frame
- await physics_frame
-
- var player := main.get("_player") as Player
- var session := main.get_node("%NetworkSession") as NetworkSession
- var sprint_dust := player.get_node("%SprintDust") as SprintDustTrail
- assert(sprint_dust != null)
- assert(sprint_dust.get_active_puff_count() == 0)
- sprint_dust.update_trail(
- 0.016,
- Vector3.ZERO,
- Vector3(4.5, 0.0, 0.0),
- true,
- )
- assert(sprint_dust.get_active_puff_count() == 0)
- sprint_dust.update_trail(
- 0.016,
- Vector3(0.5, 0.0, 0.0),
- Vector3(4.5, 0.0, 0.0),
- true,
- )
- assert(sprint_dust.get_active_puff_count() == 1)
- var planted_puff_position: Vector3 = (
- sprint_dust.get_first_active_puff_position()
- )
- sprint_dust.update_trail(
- 0.08,
- Vector3(0.75, 0.0, 0.0),
- Vector3(4.5, 0.0, 0.0),
- true,
- )
- assert(
- sprint_dust.get_first_active_puff_position()
- == planted_puff_position
- )
- sprint_dust.update_trail(
- 0.08,
- Vector3(0.75, 0.0, 0.0),
- Vector3.ZERO,
- false,
- )
- assert(sprint_dust.get_active_puff_count() == 1)
- sprint_dust.clear_trail()
- assert(sprint_dust.get_active_puff_count() == 0)
- sprint_dust.emit_landing_burst(Vector3.ZERO, Vector3.FORWARD)
- assert(sprint_dust.get_active_puff_count() == 5)
- sprint_dust.update_trail(
- 0.08,
- Vector3.ZERO,
- Vector3.ZERO,
- false,
- )
- assert(sprint_dust.get_active_puff_count() == 5)
- sprint_dust.clear_trail()
- var service := main.get_node(
- "%NetworkSurfaceDrawingService"
- ) as NetworkSurfaceDrawingService
- var game_ui := main.get_node("%GameUI") as GameUI
- var keyboard_mapping := main.get_node(
- "%KeyboardMouseMappingManager"
- ) as KeyboardMouseMappingManager
- var toolbar := game_ui.get_node(
- "%SurfaceDrawingToolbar"
- ) as SurfaceDrawingToolbar
- var chat_ui := game_ui.get_node("%ChatUI") as ChatUI
- assert(player != null and service != null and toolbar != null)
- assert(chat_ui != null)
- assert(keyboard_mapping != null)
- assert(keyboard_mapping.reset_mapping())
- await process_frame
- var shop_prompt_key := game_ui.get_node("%ShopPromptKey") as Label
- assert(shop_prompt_key.text == "E")
- var remapped_interact := InputEventKey.new()
- remapped_interact.physical_keycode = KEY_F
- remapped_interact.pressed = true
- assert(keyboard_mapping.set_binding(
- KeyboardMouseMappingManager.ROLE_INTERACT,
- keyboard_mapping.binding_from_event(remapped_interact),
- ))
- await process_frame
- assert(shop_prompt_key.text == "F")
- assert(keyboard_mapping.reset_mapping())
- await process_frame
- assert(toolbar.get_parent() == chat_ui.get_parent())
- assert(
- toolbar.get_index() > chat_ui.get_index(),
- "The art-kit toolbar must receive overlapping pointer input before Chat.",
- )
- assert(bool(game_ui.call("_can_start_virtual_mouse")))
- var virtual_pointer_states: Array[bool] = []
- game_ui.virtual_pointer_mode_changed.connect(
- func(active: bool) -> void: virtual_pointer_states.append(active)
- )
- game_ui.call("_begin_virtual_mouse", 0)
- var virtual_cursor := game_ui.get_node(
- "%ControllerVirtualCursor"
- ) as ControllerVirtualCursor
- assert(virtual_cursor.visible)
- assert(virtual_pointer_states == [true])
- # PopupMenu subwindows can consume the physical shoulder release. The
- # polled primary-button state must still release the synthetic mouse click.
- game_ui.set("_virtual_mouse_button_mask", MOUSE_BUTTON_MASK_LEFT)
- game_ui.call("_sync_virtual_mouse_primary_button", false)
- assert(int(game_ui.get("_virtual_mouse_button_mask")) == 0)
- assert(not player.is_camera_input_enabled())
- game_ui.call("_end_virtual_mouse")
- assert(not virtual_cursor.visible)
- assert(virtual_pointer_states == [true, false])
- assert(player.is_camera_input_enabled())
- var settings_panel := game_ui.get(
- "_pause_settings_panel"
- ) as SettingsPanel
- var chat_dock_selector := settings_panel.get_node(
- "%ChatDockSelector"
- ) as OptionButton
- var chat_mode_selector := settings_panel.get_node(
- "%ChatModeSelector"
- ) as OptionButton
- var paint_dock_selector := settings_panel.get_node(
- "%PaintDockSelector"
- ) as OptionButton
- assert(chat_dock_selector != null)
- assert(chat_mode_selector != null)
- assert(paint_dock_selector != null)
- assert(chat_dock_selector.custom_minimum_size.x == 300.0)
- assert(chat_mode_selector.custom_minimum_size.x == 300.0)
- assert(paint_dock_selector.custom_minimum_size.x == 300.0)
- assert(
- settings_panel.find_children(
- "*", "BubbleButton", true, false
- ).is_empty()
- )
- assert(not chat_ui.is_docked_right())
- assert(toolbar.is_docked_right())
- var settings_manager := main.get_node(
- "%PlayerSettingsManager"
- ) as PlayerSettingsManager
- assert(settings_manager != null)
- var changed_docks: PlayerSettings = settings_manager.current_settings.copy()
- changed_docks.chat_dock_right = true
- changed_docks.chat_mobile_mode = true
- changed_docks.paint_dock_right = false
- assert(settings_manager.apply_settings(changed_docks))
- await process_frame
- assert(not chat_ui.is_docked_right())
- assert(not chat_ui.is_mobile_mode())
- assert(toolbar.is_docked_right())
- changed_docks.presentation_layout_customized = true
- assert(settings_manager.apply_settings(changed_docks))
- await process_frame
- assert(chat_ui.is_docked_right())
- assert(chat_ui.is_mobile_mode())
- assert(not toolbar.is_docked_right())
- var chat_panel := chat_ui.get_node("ChatPanel") as PanelContainer
- var chat_height_button := chat_ui.get_node("ChatHeightButton") as Button
- var chat_status := chat_ui.find_child(
- "ChatStatus", true, false
- ) as Label
- assert(chat_status != null)
- assert(
- chat_status.autowrap_mode
- == TextServer.AUTOWRAP_WORD_SMART
- )
- assert(is_zero_approx(chat_panel.position.y))
- assert(is_equal_approx(chat_panel.size.x, ChatUI.MOBILE_COMPACT_WIDTH))
- assert(not chat_height_button.visible)
- var reloaded_settings := PlayerSettingsManager.new()
- root.add_child(reloaded_settings)
- assert(reloaded_settings.load_settings())
- assert(reloaded_settings.current_settings.chat_dock_right)
- assert(reloaded_settings.current_settings.chat_mobile_mode)
- assert(not reloaded_settings.current_settings.paint_dock_right)
- assert(reloaded_settings.current_settings.presentation_layout_customized)
- reloaded_settings.queue_free()
- var legacy_data: Dictionary = JSON.parse_string(
- FileAccess.get_file_as_string(PlayerSettingsManager.SETTINGS_PATH)
- ) as Dictionary
- var legacy_presentation := legacy_data.get("presentation", {}) as Dictionary
- legacy_presentation.erase("layout_customized")
- legacy_data["presentation"] = legacy_presentation
- var legacy_file := FileAccess.open(
- PlayerSettingsManager.SETTINGS_PATH,
- FileAccess.WRITE,
- )
- assert(legacy_file != null)
- legacy_file.store_string(JSON.stringify(legacy_data, "\t"))
- legacy_file.close()
- var migrated_legacy_settings := PlayerSettingsManager.new()
- root.add_child(migrated_legacy_settings)
- assert(migrated_legacy_settings.load_settings())
- assert(not migrated_legacy_settings.current_settings.chat_dock_right)
- assert(not migrated_legacy_settings.current_settings.chat_mobile_mode)
- assert(migrated_legacy_settings.current_settings.paint_dock_right)
- assert(
- not migrated_legacy_settings.current_settings.presentation_layout_customized
- )
- migrated_legacy_settings.queue_free()
- var default_docks: PlayerSettings = settings_manager.current_settings.copy()
- default_docks.chat_dock_right = false
- default_docks.chat_mobile_mode = false
- default_docks.paint_dock_right = true
- assert(settings_manager.apply_settings(default_docks))
- await process_frame
- chat_ui.call(
- "_set_status",
- (
- "Unknown command: /this-command-name-is-intentionally-long-"
- + "enough-to-require-smart-character-wrapping"
- ),
- )
- await process_frame
- assert(is_equal_approx(chat_panel.size.x, ChatUI.PANEL_WIDTH))
- assert(chat_status.size.x <= ChatUI.PANEL_WIDTH)
- chat_ui.call("_set_status", "")
- assert(not chat_ui.is_docked_right())
- assert(not chat_ui.is_mobile_mode())
- assert(toolbar.is_docked_right())
- assert(chat_height_button.visible)
- var select_button := InputEventJoypadButton.new()
- select_button.button_index = JOY_BUTTON_BACK
- select_button.pressed = true
- assert(bool(game_ui.call("_handle_controller_chat_controls", select_button)))
- await process_frame
- assert(chat_ui.is_open())
- assert(chat_panel.mouse_filter == Control.MOUSE_FILTER_STOP)
- var typed_chat_entry := chat_ui.find_child(
- "ChatEntry", true, false
- ) as LineEdit
- assert(typed_chat_entry != null)
- assert(not typed_chat_entry.virtual_keyboard_enabled)
- var accept_button := InputEventJoypadButton.new()
- accept_button.button_index = JOY_BUTTON_A
- accept_button.pressed = true
- assert(bool(game_ui.call("_handle_controller_chat_controls", accept_button)))
- var ui_pixelation := main.get("_ui_pixelation") as UIPixelationPresenter
- assert(ui_pixelation != null)
- var on_screen_keyboard := ui_pixelation.get(
- "_on_screen_keyboard"
- ) as OnScreenKeyboard
- assert(on_screen_keyboard != null)
- if DisplayServer.has_feature(DisplayServer.FEATURE_VIRTUAL_KEYBOARD):
- assert(typed_chat_entry.virtual_keyboard_enabled)
- assert(not on_screen_keyboard.is_open())
- await process_frame
- assert(typed_chat_entry.has_focus())
- else:
- assert(not typed_chat_entry.virtual_keyboard_enabled)
- assert(on_screen_keyboard.is_open())
-
- # A bite temporarily owns gameplay input without discarding an in-progress
- # message. The draft and insertion point return only after fishing releases
- # that ownership, and any controller keyboard follows the restored field.
- var chat_fishing_spot := main.get_node("%FishingSpot") as FishingSpot
- assert(chat_fishing_spot != null)
- var interrupted_draft := "finish this message after fishing"
- var interrupted_caret: int = 12
- typed_chat_entry.text = interrupted_draft
- typed_chat_entry.caret_column = interrupted_caret
- chat_fishing_spot.call("_set_fishing_input_priority", true)
- await process_frame
- assert(not chat_ui.is_open())
- assert(not bool(chat_ui.get("_input_lock_applied")))
- assert(chat_ui.has_fishing_resume_pending())
- assert(not on_screen_keyboard.is_open())
- chat_ui.open_chat()
- assert(not chat_ui.is_open())
- chat_fishing_spot.call("_set_fishing_input_priority", false)
- for _frame: int in 4:
- await process_frame
- assert(chat_ui.is_open())
- assert(typed_chat_entry.text == interrupted_draft)
- assert(typed_chat_entry.caret_column == interrupted_caret)
- assert(bool(chat_ui.get("_input_lock_applied")))
- if not DisplayServer.has_feature(DisplayServer.FEATURE_VIRTUAL_KEYBOARD):
- assert(on_screen_keyboard.is_open())
- on_screen_keyboard.call("_close_keyboard", true)
- assert(typed_chat_entry.has_focus())
-
- # A deferred Standby bite has not started the fishing minigame yet. It must
- # release fishing's input ownership so chat remains available until the
- # player confirms the bite.
- chat_fishing_spot.call("_set_fishing_input_priority", true)
- chat_fishing_spot.call("_set_bite_confirmation_pending", true)
- for _frame: int in 4:
- await process_frame
- assert(not chat_fishing_spot.is_fishing_input_priority_active())
- assert(chat_ui.is_open())
- chat_fishing_spot.call("_set_bite_confirmation_pending", false)
- var left_bumper := InputEventJoypadButton.new()
- left_bumper.button_index = JOY_BUTTON_LEFT_SHOULDER
- left_bumper.pressed = true
- assert(bool(game_ui.call("_handle_controller_chat_controls", left_bumper)))
- assert(not chat_ui.is_open())
- assert(chat_panel.mouse_filter == Control.MOUSE_FILTER_IGNORE)
- assert(not typed_chat_entry.virtual_keyboard_enabled)
- assert(bool(game_ui.call("_handle_controller_chat_controls", left_bumper)))
- assert(not chat_ui.is_open())
- assert(not typed_chat_entry.virtual_keyboard_enabled)
- assert(bool(game_ui.call("_handle_controller_chat_controls", select_button)))
- assert(not chat_ui.is_open())
- assert(chat_ui.is_collapsed())
- assert(bool(game_ui.call("_handle_controller_chat_controls", select_button)))
- assert(chat_ui.is_open())
- chat_ui.refocus_gameplay()
- chat_ui.open_chat()
- assert(chat_ui.is_open())
- typed_chat_entry.text = "validation message"
- chat_ui.call("_send")
- assert(not chat_ui.is_open())
- assert(not typed_chat_entry.has_focus())
- assert(not bool(chat_ui.get("_input_lock_applied")))
- assert(not chat_status.visible)
- assert(chat_status.text.is_empty())
- var quick_menu := game_ui.get_node(
- "%QuickRadialMenu"
- ) as QuickRadialMenu
- var emote_menu := game_ui.get_node(
- "%EmoteRadialMenu"
- ) as EmoteRadialMenu
- assert(quick_menu != null and emote_menu != null)
- assert(QuickRadialMenu.ACTIONS.size() == QuickRadialMenu.SECTOR_COUNT)
- assert(QuickRadialMenu.ACTIONS == [&"chat", &"freecam", &"hud"])
- game_ui.call("_on_quick_action_selected", &"freecam")
- assert(player.is_free_camera_active())
- var free_camera := player.get_active_gameplay_camera()
- assert(free_camera.name == &"FreeCamera")
- var free_camera_yaw_before: float = free_camera.global_rotation.y
- var mapping_manager := main.get(
- "_controller_mapping_manager"
- ) as ControllerMappingManager
- var right_stick_motion := InputEventJoypadMotion.new()
- right_stick_motion.device = mapping_manager.get_active_device_id()
- right_stick_motion.axis = JOY_AXIS_RIGHT_X
- right_stick_motion.axis_value = 0.9
- Input.parse_input_event(right_stick_motion)
- await create_timer(0.1).timeout
- assert(
- absf(angle_difference(
- free_camera.global_rotation.y,
- free_camera_yaw_before,
- )) > 0.02
- )
- right_stick_motion.axis_value = 0.0
- Input.parse_input_event(right_stick_motion)
- await process_frame
-
- var free_camera_body := player.get("_free_camera_body") as CharacterBody3D
- assert(free_camera_body != null)
- # A released fishing cast locks only the avatar. The detached camera must
- # continue to accept translation while that movement lock is active.
- player.set_movement_enabled(false)
- var cast_locked_camera_position: Vector3 = free_camera_body.global_position
- Input.action_press("move_forward")
- for _frame: int in 3:
- await physics_frame
- Input.action_release("move_forward")
- assert(not free_camera_body.global_position.is_equal_approx(
- cast_locked_camera_position
- ))
- player.set_movement_enabled(true)
-
- # Chat owns all movement while text entry is active, including developer
- # freecam movement which reads the same WASD actions as the player.
- chat_ui.open_chat()
- assert(chat_ui.is_open())
- var free_camera_position_before: Vector3 = free_camera_body.global_position
- Input.action_press("move_forward")
- for _frame: int in 3:
- await physics_frame
- Input.action_release("move_forward")
- assert(free_camera_body.global_position.is_equal_approx(
- free_camera_position_before
- ))
- chat_ui.refocus_gameplay()
-
- # Typewriter speech cycles the shared open-mouth faceplates, then restores
- # the player's authored mouth. World speech also follows the active freecam,
- # not the inactive normal gameplay camera.
- var normal_camera := player.get_gameplay_camera()
- var normal_camera_transform: Transform3D = normal_camera.global_transform
- var chat_anchor: Vector3 = player.get_chat_anchor_position()
- normal_camera.look_at(
- normal_camera.global_position
- - (chat_anchor - normal_camera.global_position),
- Vector3.UP,
- )
- assert(normal_camera.is_position_behind(chat_anchor))
- assert(not free_camera.is_position_behind(chat_anchor))
- var shop_prompt := game_ui.get_node("%ShopPrompt") as Control
- var storage_prompt := game_ui.get_node("%StoragePrompt") as Control
- game_ui.set_shop_prompt_visible(true, chat_anchor)
- game_ui.set_storage_prompt_visible(true, chat_anchor)
- assert(shop_prompt.visible)
- assert(storage_prompt.visible)
- chat_ui.show_local_speech("hello there friend")
- await process_frame
- assert(bool(player.get("_speech_mouth_active")))
- var first_speech_mouth: String = str(player.get("_speech_mouth_id"))
- await create_timer(0.12).timeout
- var second_speech_mouth: String = str(player.get("_speech_mouth_id"))
- assert(not first_speech_mouth.is_empty())
- assert(not second_speech_mouth.is_empty())
- assert(first_speech_mouth != second_speech_mouth)
- chat_ui.call("_update_speech")
- var speech: Dictionary = chat_ui.get("_speech")
- var local_speech: Dictionary = speech.get(session.get_local_peer_id(), {})
- var speech_bubble := local_speech.get("bubble") as PanelContainer
- assert(speech_bubble != null and speech_bubble.visible)
- normal_camera.global_transform = normal_camera_transform
- await create_timer(0.6).timeout
- assert(not bool(player.get("_speech_mouth_active")))
- chat_ui.call("_on_peer_removed", session.get_local_peer_id())
-
- game_ui.call("_on_quick_action_selected", &"freecam")
- assert(not player.is_free_camera_active())
- game_ui.call("_on_quick_action_selected", &"hud")
- await process_frame
- assert(game_ui.is_gameplay_hud_hidden())
- assert(chat_ui.is_hud_hidden())
- assert(not (
- game_ui.get_node("%GameplayTransientHUD") as Control
- ).visible)
- assert(not (
- game_ui.get_node("%ExperiencePresentation") as Control
- ).visible)
- var hud_fishing_spot := main.get_node("%FishingSpot") as FishingSpot
- assert(hud_fishing_spot != null)
- hud_fishing_spot.state = FishingSpot.FishingState.SHOWING_CATCH
- game_ui.call("_refresh_gameplay_hud_visibility")
- assert((
- game_ui.get_node("%GameplayTransientHUD") as Control
- ).visible)
- assert(not (
- game_ui.get_node("%ExperiencePresentation") as Control
- ).visible)
- assert(chat_ui.is_hud_hidden())
- hud_fishing_spot.state = FishingSpot.FishingState.READY
- game_ui.call("_refresh_gameplay_hud_visibility")
- assert(not (
- game_ui.get_node("%GameplayTransientHUD") as Control
- ).visible)
- quick_menu.open_menu(true)
- assert(quick_menu.visible and quick_menu.is_open())
- quick_menu.close_menu()
- emote_menu.open_menu(true)
- assert(emote_menu.visible and emote_menu.is_open())
- emote_menu.close_menu()
- chat_ui.open_chat()
- await process_frame
- assert(chat_ui.is_open() and chat_panel.visible)
- chat_ui.refocus_gameplay()
- assert(chat_ui.is_hud_hidden())
- var cancel_button := InputEventJoypadButton.new()
- cancel_button.button_index = JOY_BUTTON_B
- cancel_button.pressed = true
- assert(not bool(main.call("_is_pause_open_request", cancel_button)))
- var start_button := InputEventJoypadButton.new()
- start_button.button_index = JOY_BUTTON_START
- start_button.pressed = true
- assert(bool(main.call("_is_pause_open_request", start_button)))
- await _validate_pause_browser_transition(game_ui, player)
- var player_menu := game_ui.get_node("%PlayerMenu") as PlayerMenu
- player_menu.open_section(PlayerMenu.Section.PROFILE)
- for _frame: int in 12:
- await process_frame
- assert(game_ui.is_gameplay_hud_hidden())
- assert(player_menu.visible)
- assert(root.gui_get_focus_owner() is not LineEdit)
- player_menu.close_menu(PlayerMenu.CloseReason.SESSION_END)
- await process_frame
- game_ui.call("_on_quick_action_selected", &"hud")
- await process_frame
- assert(not game_ui.is_gameplay_hud_hidden())
- assert(not chat_ui.is_hud_hidden())
- assert(not service.can_activate())
- assert(not service.is_active() and not toolbar.visible)
- assert(player.bag.add_item(ArtShopStock.ART_KIT_ITEM_ID, 1))
- player.hotbar.select_slot(1)
- assert(player.hotbar.assign_item(0, ArtShopStock.ART_KIT_ITEM_ID))
- assert(not service.can_activate())
- var paint_key := InputEventKey.new()
- paint_key.physical_keycode = KEY_P
- paint_key.pressed = true
- assert(not service.handle_input(paint_key, true))
- assert(not service.is_active())
- player.hotbar.select_slot(0)
- await process_frame
- assert(service.is_active() and not service.is_placement_mode())
- assert(toolbar.visible)
- chat_ui.open_chat()
- assert(chat_ui.is_open())
- assert(not service.is_active() and not toolbar.visible)
- chat_ui.close_chat()
- assert(not chat_ui.is_open())
- assert(service.is_active() and toolbar.visible)
- var ui_root := game_ui.get_node("%UIRoot") as Control
- assert(toolbar.get_parent() == ui_root)
- assert(is_zero_approx(toolbar.position.y))
- assert(
- is_equal_approx(
- toolbar.get_global_rect().end.x,
- ui_root.get_global_rect().end.x,
- )
- )
- assert(toolbar.get_global_rect().end.x <= 1280.0)
- assert(toolbar.get_global_rect().end.y <= 720.0)
- var top_panel := toolbar.get_node("%TopPanel") as PanelContainer
- var color_panel := toolbar.get_node("%ColorPanel") as PanelContainer
- assert(is_equal_approx(toolbar.size.x, SurfaceDrawingToolbar.TOOLBAR_WIDTH))
- assert(is_equal_approx(top_panel.size.x, SurfaceDrawingToolbar.TOOLBAR_WIDTH))
- assert(top_panel.position.is_equal_approx(Vector2.ZERO))
- assert(color_panel.position.x > 0.0)
- assert(is_equal_approx(color_panel.get_rect().end.x, top_panel.get_rect().end.x))
- assert(color_panel.position.y < top_panel.get_rect().end.y)
- assert(color_panel.get_rect().end.y > top_panel.get_rect().end.y)
- var toolbar_origin: Vector2 = toolbar.get_global_rect().position
- var top_pointer := InputEventMouseMotion.new()
- top_pointer.position = toolbar_origin + Vector2(200.0, 24.0)
- assert(toolbar.owns_pointer_event(top_pointer))
- var rail_pointer := InputEventMouseMotion.new()
- rail_pointer.position = color_panel.get_global_rect().get_center()
- assert(toolbar.owns_pointer_event(rail_pointer))
- var empty_pointer := InputEventMouseMotion.new()
- empty_pointer.position = toolbar_origin + Vector2(200.0, 180.0)
- assert(not toolbar.owns_pointer_event(empty_pointer))
-
- var brush_option := toolbar.get_node("%BrushOption") as OptionButton
- var grid_option := toolbar.get_node("%GridOption") as OptionButton
- var mode_button := toolbar.get_node("%ModeButton") as Button
- var eraser_button := toolbar.get_node("%EraserButton") as Button
- var undo_button := toolbar.get_node("%UndoButton") as Button
- var hide_button := toolbar.get_node("%HideGuideButton") as Button
- var restore_button := toolbar.get_node("%RestoreGuideButton") as Button
- var finalize_button := toolbar.get_node("%FinalizeGuideButton") as Button
- var export_button := toolbar.get_node("%ExportButton") as Button
- var stamp_button := toolbar.get_node("%StampButton") as Button
- assert(
- mode_button != null
- and eraser_button != null
- and undo_button != null
- and hide_button != null
- and restore_button != null
- and finalize_button != null
- and export_button != null
- and stamp_button != null
- )
- assert(export_button.text == "png")
- assert(export_button.tooltip_text == "aim at artwork, then click to export PNG")
- assert(stamp_button.text == "stamp")
- assert(stamp_button.disabled)
- assert(toolbar.get_node_or_null("%CloseButton") == null)
- var expected_toolbar_icons: Dictionary[Button, String] = {
- mode_button: "/art/art_kit_marker.png",
- eraser_button: "/art/art_kit_eraser.png",
- hide_button: "/art/art_kit_grid_hidden_light.png",
- restore_button: "/art/art_kit_grid_restore_light.png",
- finalize_button: "/art/art_kit_grid_finish_light.png",
- }
- for icon_button: Button in expected_toolbar_icons:
- assert(icon_button.text.is_empty())
- assert(icon_button.icon != null)
- assert(
- icon_button.icon.resource_path.ends_with(
- expected_toolbar_icons[icon_button]
- )
- )
- for icon_button: Button in [
- eraser_button,
- undo_button,
- hide_button,
- restore_button,
- finalize_button,
- export_button,
- stamp_button,
- ]:
- assert(icon_button.custom_minimum_size == Vector2(48, 44))
- assert(icon_button.get_theme_constant("icon_max_width") == 40)
- var icon_style := (
- icon_button.get_theme_stylebox("normal") as StyleBoxFlat
- )
- assert(is_equal_approx(icon_style.content_margin_left, 4.0))
- assert(is_equal_approx(icon_style.content_margin_top, 2.0))
- assert(mode_button.custom_minimum_size == Vector2(48, 48))
- assert(mode_button.get_theme_constant("icon_max_width") == 40)
- assert(export_button.get_index() > finalize_button.get_index())
- assert(stamp_button.get_index() > export_button.get_index())
- assert(mode_button.get_index() > stamp_button.get_index())
- assert(hide_button.button_group != null)
- assert(hide_button.button_group == restore_button.button_group)
- assert(hide_button.button_group == finalize_button.button_group)
- assert(hide_button.button_group.allow_unpress)
- assert(brush_option.item_count == 4)
- assert(grid_option.item_count == 4)
- for pointer_only_control: Control in [
- brush_option,
- grid_option,
- mode_button,
- eraser_button,
- undo_button,
- hide_button,
- restore_button,
- finalize_button,
- export_button,
- stamp_button,
- ]:
- assert(pointer_only_control.focus_mode == Control.FOCUS_NONE)
- assert(brush_option.get_popup().unfocusable)
- assert(grid_option.get_popup().unfocusable)
- brush_option.get_popup().popup(Rect2i(100, 100, 220, 220))
- await process_frame
- game_ui.call("_begin_virtual_mouse", 0)
- game_ui.call("_update_virtual_cursor_position", Vector2(160.0, 160.0))
- var brush_popup_cursor := toolbar.get(
- "_brush_popup_cursor"
- ) as ControllerVirtualCursor
- assert(brush_popup_cursor != null and brush_popup_cursor.visible)
- assert(brush_popup_cursor.get_parent() == brush_option.get_popup())
- assert(brush_popup_cursor.z_index == RenderingServer.CANVAS_ITEM_Z_MAX)
- assert(not virtual_cursor.visible)
- brush_option.get_popup().hide()
- game_ui.call("_update_virtual_cursor_position", Vector2(160.0, 160.0))
- assert(not brush_popup_cursor.visible)
- assert(virtual_cursor.visible)
- game_ui.call("_end_virtual_mouse")
- var expected_brush_icons: Array[String] = [
- "art_kit_marker_tip_fine.png",
- "art_kit_marker_tip_thin.png",
- "art_kit_marker_tip_mid.png",
- "art_kit_marker_tip_thick.png",
- ]
- var expected_grid_icons: Array[String] = [
- "art_kit_grid_small_light.png",
- "art_kit_grid_medium_light.png",
- "art_kit_grid_large_light.png",
- "art_kit_grid_xl_light.png",
- ]
- for index: int in 4:
- assert(brush_option.get_item_text(index).is_empty())
- assert(grid_option.get_item_text(index).is_empty())
- assert(brush_option.get_item_icon(index) != null)
- assert(grid_option.get_item_icon(index) != null)
- var brush_unlocked: bool = index == 0
- var grid_unlocked: bool = index == 0
- if brush_unlocked:
- assert(
- str(brush_option.get_item_icon(index).get_meta(
- &"channel_mask_source", ""
- )).ends_with(
- expected_brush_icons[index]
- )
- )
- else:
- assert(bool(brush_option.get_item_icon(index).get_meta(
- &"locked_content_icon", false
- )))
- if grid_unlocked:
- assert(
- grid_option.get_item_icon(index).resource_path.ends_with(
- expected_grid_icons[index]
- )
- )
- else:
- assert(bool(grid_option.get_item_icon(index).get_meta(
- &"locked_content_icon", false
- )))
- var expected_lock_alpha: float = 1.0 if index == 0 else 0.72
- assert(is_equal_approx(
- brush_option.get_popup().get_item_icon_modulate(index).a,
- expected_lock_alpha,
- ))
- assert(is_equal_approx(
- grid_option.get_popup().get_item_icon_modulate(index).a,
- expected_lock_alpha,
- ))
- assert(not brush_option.get_popup().is_item_disabled(0))
- assert(brush_option.get_popup().is_item_disabled(1))
- assert(not grid_option.get_popup().is_item_disabled(0))
- assert(grid_option.get_popup().is_item_disabled(1))
- var color_buttons: Dictionary = toolbar.get("_color_buttons")
- assert(color_buttons.size() == SurfaceDrawingPalette.COLORS.size())
- for color_button: Button in color_buttons.values():
- assert(color_button.focus_mode == Control.FOCUS_NONE)
- assert(not (color_buttons[&"chalk_white"] as Button).disabled)
- assert((color_buttons[&"ocean_teal"] as Button).disabled)
- var base_color_lock := (
- (color_buttons[&"chalk_white"] as Button).get_node(
- "UnlockStateIcon"
- ) as TextureRect
- )
- var locked_color_lock := (
- (color_buttons[&"ocean_teal"] as Button).get_node(
- "UnlockStateIcon"
- ) as TextureRect
- )
- assert(not base_color_lock.visible)
- assert(locked_color_lock.visible)
- assert(locked_color_lock.size == Vector2(18.0, 18.0))
- assert(is_equal_approx(locked_color_lock.modulate.a, 0.34))
- var popup_lock_icon := toolbar.get("_popup_lock_icon") as Texture2D
- assert(popup_lock_icon.get_width() == 40)
- assert(popup_lock_icon.get_height() == 40)
- assert(int(popup_lock_icon.get_meta(
- &"locked_content_icon_size", 0
- )) == 24)
-
- for product_id: StringName in [
- &"marker_ocean_teal",
- &"brush_4x",
- &"grid_128x",
- PlayerArtUnlocks.STAMP_PRODUCT_ID,
- ]:
- assert(player.art_unlocks.unlock_product(product_id))
- await process_frame
- assert(not (color_buttons[&"ocean_teal"] as Button).disabled)
- assert(not brush_option.get_popup().is_item_disabled(3))
- assert(not grid_option.get_popup().is_item_disabled(3))
- assert(not stamp_button.disabled)
- var stamp_library_panel := toolbar.get_node(
- "%StampLibraryPanel"
- ) as PanelContainer
- var stamp_library_close := toolbar.get_node(
- "%StampLibraryClose"
- ) as Button
- assert(stamp_library_panel != null and not stamp_library_panel.visible)
- stamp_button.pressed.emit()
- assert(stamp_library_panel.visible)
- var library_pointer := InputEventMouseMotion.new()
- library_pointer.position = stamp_library_panel.get_global_rect().get_center()
- assert(toolbar.owns_pointer_event(library_pointer))
- stamp_library_close.pressed.emit()
- assert(not stamp_library_panel.visible)
- assert(not locked_color_lock.visible)
- assert(str(brush_option.get_item_icon(3).get_meta(
- &"channel_mask_source", ""
- )).ends_with(expected_brush_icons[3]))
- assert(grid_option.get_item_icon(3).resource_path.ends_with(
- expected_grid_icons[3]
- ))
- assert(is_equal_approx(
- brush_option.get_popup().get_item_icon_modulate(3).a, 1.0
- ))
- assert(is_equal_approx(
- grid_option.get_popup().get_item_icon_modulate(3).a, 1.0
- ))
- assert(service.set_color_id(&"ocean_teal"))
- assert(service.set_brush_size(4))
- assert(service.set_grid_size(128))
- assert(service.get_color_id() == &"ocean_teal")
- assert(service.get_brush_size() == 4)
- assert(service.get_grid_size() == 128)
- var marker_mode_material := mode_button.material as ShaderMaterial
- assert(marker_mode_material != null)
- assert(brush_option.material == null)
- assert(grid_option.material == null)
- assert(
- marker_mode_material.shader.resource_path.ends_with(
- "/ui/art_kit_marker_icon.gdshader"
- )
- )
- assert(
- marker_mode_material.get_shader_parameter("marker_color")
- == SurfaceDrawingPalette.get_color(&"ocean_teal")
- )
-
- mode_button.pressed.emit()
- assert(service.is_placement_mode())
- assert(
- mode_button.icon.resource_path.ends_with(
- "/art/art_kit_grid_light.png"
- )
- )
- eraser_button.pressed.emit()
- assert(service.is_eraser_mode())
- assert(eraser_button.button_pressed)
- hide_button.pressed.emit()
- assert(service.is_placement_mode())
- assert(not service.is_eraser_mode())
- assert(
- service.get_armed_guide_action()
- == NetworkSurfaceDrawingService.GuideAction.HIDE
- )
- var world_click := InputEventMouseButton.new()
- world_click.button_index = MOUSE_BUTTON_LEFT
- world_click.pressed = true
- assert(service.handle_input(world_click, true))
- assert(
- service.get_armed_guide_action()
- == NetworkSurfaceDrawingService.GuideAction.HIDE
- )
- assert(service.is_placement_mode())
- assert(not service.is_eraser_mode())
- assert(hide_button.button_pressed)
- hide_button.pressed.emit()
- assert(
- service.get_armed_guide_action()
- == NetworkSurfaceDrawingService.GuideAction.NONE
- )
- assert(not service.is_placement_mode() and service.is_eraser_mode())
-
- restore_button.pressed.emit()
- assert(
- service.get_armed_guide_action()
- == NetworkSurfaceDrawingService.GuideAction.RESTORE
- )
- finalize_button.pressed.emit()
- assert(
- service.get_armed_guide_action()
- == NetworkSurfaceDrawingService.GuideAction.FINALIZE
- )
- assert(not restore_button.button_pressed)
- assert(finalize_button.button_pressed)
- assert(service.handle_input(world_click, true))
- assert(
- service.get_armed_guide_action()
- == NetworkSurfaceDrawingService.GuideAction.FINALIZE
- )
- finalize_button.pressed.emit()
- assert(
- service.get_armed_guide_action()
- == NetworkSurfaceDrawingService.GuideAction.NONE
- )
- assert(not service.is_placement_mode() and service.is_eraser_mode())
- (color_buttons[&"ocean_teal"] as Button).pressed.emit()
- assert(not service.is_eraser_mode())
- undo_button.pressed.emit()
- assert(service.is_active())
- assert(not service.handle_input(paint_key, true))
- assert(service.is_active() and toolbar.visible)
- var fishing_spot := main.get_node("%FishingSpot") as FishingSpot
- await process_frame
- var held_art_kit_display := player.get("_held_art_kit_display") as Node3D
- var held_art_kit_sprite := player.get("_held_art_kit_sprite") as Sprite3D
- var pocket_animation_player := player.get(
- "_character_animation_player"
- ) as AnimationPlayer
- var pocket_animation_name: StringName = player.get(
- "_pocket_visual_animation"
- )
- if (
- not held_art_kit_display.visible
- and pocket_animation_player != null
- and not pocket_animation_name.is_empty()
- and pocket_animation_player.has_animation(pocket_animation_name)
- ):
- var pocket_animation: Animation = (
- pocket_animation_player.get_animation(pocket_animation_name)
- )
- await create_timer(pocket_animation.length * 0.55).timeout
- assert(held_art_kit_display.visible)
- assert(held_art_kit_sprite.visible and held_art_kit_sprite.texture != null)
- assert(held_art_kit_sprite.texture.resource_path.ends_with("/art/art_kit.png"))
- assert(is_zero_approx(angle_difference(
- held_art_kit_sprite.rotation.z,
- -PI * 0.5,
- )))
- assert(held_art_kit_sprite.flip_h)
- assert(not fishing_spot.has_signal(&"art_ui_toggle_requested"))
- assert(service.is_active() and toolbar.visible)
- assert(service.get_grid_size() == 128)
- assert(service.handle_input(world_click, true))
- assert(service.is_active() and toolbar.visible)
- var hotbar_ui := game_ui.get_node("%Hotbar") as HotbarUI
- var wheel_down := InputEventMouseButton.new()
- wheel_down.button_index = MOUSE_BUTTON_WHEEL_DOWN
- wheel_down.pressed = true
- assert(not service.handle_input(wheel_down, true))
- hotbar_ui._unhandled_input(wheel_down)
- await process_frame
- assert(player.hotbar.get_selected_slot() == 1)
- assert(not service.is_active() and not toolbar.visible)
- hotbar_ui.set_swap_hotbar_camera_scroll(true)
- hotbar_ui._unhandled_input(wheel_down)
- await process_frame
- assert(player.hotbar.get_selected_slot() == 1)
- var shifted_wheel_down := InputEventMouseButton.new()
- shifted_wheel_down.button_index = MOUSE_BUTTON_WHEEL_DOWN
- shifted_wheel_down.pressed = true
- shifted_wheel_down.shift_pressed = true
- hotbar_ui._unhandled_input(shifted_wheel_down)
- await process_frame
- assert(player.hotbar.get_selected_slot() == 2)
- hotbar_ui.set_swap_hotbar_camera_scroll(false)
-
- var number_one := InputEventKey.new()
- number_one.physical_keycode = KEY_1
- number_one.pressed = true
- assert(not service.handle_input(number_one, true))
- hotbar_ui._unhandled_input(number_one)
- await process_frame
- assert(service.is_active() and toolbar.visible)
- assert(player.hotbar.get_selected_slot() == 0)
- var number_two := InputEventKey.new()
- number_two.physical_keycode = KEY_2
- number_two.pressed = true
- assert(not service.handle_input(number_two, true))
- hotbar_ui._unhandled_input(number_two)
- await process_frame
- assert(player.hotbar.get_selected_slot() == 1)
- assert(not service.is_active() and not toolbar.visible)
-
- print("Art tools validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _validate_pause_browser_transition(
- game_ui: GameUI,
- player: Player,
-) -> void:
- var pause_menu := game_ui.get_pause_menu()
- var root_page := pause_menu.get_node("%RootPage") as Control
- var join_page := pause_menu.get_node("%JoinGamePage") as JoinGamePage
- pause_menu.open_menu()
- while bool(pause_menu.get("_root_transition_active")):
- await process_frame
- assert(pause_menu.visible and root_page.visible)
- assert(not bool(player.call("_is_movement_input_enabled")))
-
- pause_menu.call("_open_join_game")
- while not join_page.visible:
- await process_frame
- assert(bool(pause_menu.get("_root_transition_active")))
- pause_menu.call("_close_join_game")
- assert(not join_page.visible)
- # This is the issue #73 input: Escape arrives while the root is returning.
- assert(pause_menu.handle_escape())
- while bool(pause_menu.get("_root_transition_active")):
- await process_frame
- assert(pause_menu.visible and root_page.visible)
- assert(not bool(player.call("_is_movement_input_enabled")))
-
- assert(pause_menu.handle_escape())
- while pause_menu.visible:
- await process_frame
- assert(player.is_movement_enabled())
-
- # Fishing can complete while pause owns local input. Closing pause must only
- # release its own lock, not restore the stale movement value from open time.
- player.set_movement_enabled(false)
- pause_menu.open_menu()
- while bool(pause_menu.get("_root_transition_active")):
- await process_frame
- assert(not bool(player.call("_is_movement_input_enabled")))
- player.set_movement_enabled(true)
- assert(player.is_movement_enabled())
- assert(not bool(player.call("_is_movement_input_enabled")))
- pause_menu.close_menu(PauseMenu.CloseReason.USER_RETURN)
- assert(player.is_movement_enabled())
- assert(bool(player.call("_is_movement_input_enabled")))
-
- # Opening pause during a right-click camera drag must not snapshot and later
- # restore the drag's transient captured mouse mode after the drag is cancelled.
- assert(
- pause_menu.call(
- "_get_restorable_mouse_mode",
- Input.MOUSE_MODE_CAPTURED,
- ) == Input.MOUSE_MODE_VISIBLE
- )
- Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
- var drag_press := InputEventMouseButton.new()
- drag_press.button_index = MOUSE_BUTTON_RIGHT
- drag_press.button_mask = MOUSE_BUTTON_MASK_RIGHT
- drag_press.pressed = true
- player.call("_input", drag_press)
- assert(bool(player.get("_camera_dragging")))
- var camera_yaw := player.get_node("%CameraYaw") as Node3D
- var yaw_before_drag: float = camera_yaw.rotation.y
- var drag_motion := InputEventMouseMotion.new()
- drag_motion.button_mask = MOUSE_BUTTON_MASK_RIGHT
- drag_motion.relative = Vector2(24.0, -8.0)
- drag_motion.screen_relative = drag_motion.relative
- player.call("_input", drag_motion)
- assert(not is_equal_approx(camera_yaw.rotation.y, yaw_before_drag))
- await process_frame
- assert(bool(player.get("_camera_dragging")))
- if DisplayServer.get_name() != "headless":
- assert(Input.mouse_mode == Input.MOUSE_MODE_CAPTURED)
- var drag_release := InputEventMouseButton.new()
- drag_release.button_index = MOUSE_BUTTON_RIGHT
- drag_release.button_mask = 0
- drag_release.pressed = false
- player.call("_input", drag_release)
- assert(not bool(player.get("_camera_dragging")))
- player.call("_input", drag_press)
- assert(bool(player.get("_camera_dragging")))
- pause_menu.open_menu()
- while bool(pause_menu.get("_root_transition_active")):
- await process_frame
- assert(not bool(player.get("_camera_dragging")))
- assert(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE)
- pause_menu.close_menu(PauseMenu.CloseReason.USER_RETURN)
- assert(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE)
diff --git a/tests/art_tools_validation.gd.uid b/tests/art_tools_validation.gd.uid
deleted file mode 100644
index 7c7edc5..0000000
--- a/tests/art_tools_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dr671f64t58wc
diff --git a/tests/camera_drag_validation.gd b/tests/camera_drag_validation.gd
deleted file mode 100644
index a2dd410..0000000
--- a/tests/camera_drag_validation.gd
+++ /dev/null
@@ -1,80 +0,0 @@
-extends SceneTree
-
-const PlayerScene: PackedScene = preload("res://player/player.tscn")
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- root.size = Vector2i(1280, 720)
- var player := PlayerScene.instantiate() as Player
- root.add_child(player)
- await process_frame
- player.local_control_enabled = true
- player.set_camera_input_enabled(true)
- Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
-
- var press := InputEventMouseButton.new()
- press.button_index = MOUSE_BUTTON_RIGHT
- press.button_mask = MOUSE_BUTTON_MASK_RIGHT
- press.pressed = true
- Input.parse_input_event(press)
- await process_frame
- assert(bool(player.get("_camera_dragging")))
- if DisplayServer.get_name() != "headless":
- assert(Input.mouse_mode == Input.MOUSE_MODE_CAPTURED)
-
- var yaw := player.get_node("%CameraYaw") as Node3D
- var previous_yaw: float = yaw.rotation.y
- var motion := InputEventMouseMotion.new()
- motion.button_mask = MOUSE_BUTTON_MASK_RIGHT
- motion.relative = Vector2(18.0, -4.0)
- motion.screen_relative = motion.relative
- Input.parse_input_event(motion)
- await process_frame
- assert(not is_equal_approx(yaw.rotation.y, previous_yaw))
- assert(bool(player.get("_camera_dragging")))
- if DisplayServer.get_name() != "headless":
- assert(Input.mouse_mode == Input.MOUSE_MODE_CAPTURED)
-
- var release := InputEventMouseButton.new()
- release.button_index = MOUSE_BUTTON_RIGHT
- release.pressed = false
- Input.parse_input_event(release)
- await process_frame
- assert(not bool(player.get("_camera_dragging")))
- if DisplayServer.get_name() != "headless":
- assert(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE)
-
- Input.parse_input_event(press)
- await process_frame
- assert(bool(player.get("_camera_dragging")))
- player.notification(NOTIFICATION_APPLICATION_FOCUS_OUT)
- assert(not bool(player.get("_camera_dragging")))
-
- var plain_wheel_down := InputEventMouseButton.new()
- plain_wheel_down.button_index = MOUSE_BUTTON_WHEEL_DOWN
- plain_wheel_down.pressed = true
- var shifted_wheel_down := InputEventMouseButton.new()
- shifted_wheel_down.button_index = MOUSE_BUTTON_WHEEL_DOWN
- shifted_wheel_down.pressed = true
- shifted_wheel_down.shift_pressed = true
- player.apply_camera_settings(0.005, 2.5, false, false)
- var default_zoom: float = float(player.get("_target_zoom"))
- player._unhandled_input(plain_wheel_down)
- assert(is_equal_approx(float(player.get("_target_zoom")), default_zoom))
- player._unhandled_input(shifted_wheel_down)
- assert(float(player.get("_target_zoom")) > default_zoom)
- player.apply_camera_settings(0.005, 2.5, false, true)
- var swapped_zoom: float = float(player.get("_target_zoom"))
- player._unhandled_input(shifted_wheel_down)
- assert(is_equal_approx(float(player.get("_target_zoom")), swapped_zoom))
- player._unhandled_input(plain_wheel_down)
- assert(float(player.get("_target_zoom")) > swapped_zoom)
-
- player.queue_free()
- await process_frame
- print("Camera drag validation: PASS")
- quit()
diff --git a/tests/camera_drag_validation.gd.uid b/tests/camera_drag_validation.gd.uid
deleted file mode 100644
index a37fad9..0000000
--- a/tests/camera_drag_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dku3kyarg3fxu
diff --git a/tests/character_rig_validation.gd b/tests/character_rig_validation.gd
deleted file mode 100644
index 2aece2b..0000000
--- a/tests/character_rig_validation.gd
+++ /dev/null
@@ -1,139 +0,0 @@
-extends SceneTree
-
-
-const CHARACTER_SCENE: PackedScene = preload(
- "res://art/exported/characters/base/straywild_base_character.glb"
-)
-
-const EXPECTED_ANIMATIONS: Array[StringName] = [
- &"casting",
- &"casting_sit",
- &"draw",
- &"fighting",
- &"fighting_sit",
- &"fishing",
- &"fishing_sit",
- &"idle",
- &"idle_show",
- &"idle_sit",
- &"idle_sit_show",
- &"idle_sneak",
- &"pocket_idle_idle",
- &"pocket_idle_show",
- &"pocket_show_idle",
- &"pocket_show_show",
- &"pocket_sit_idle_idle",
- &"pocket_sit_idle_show",
- &"pocket_sit_show_idle",
- &"pocket_sit_show_show",
- &"pocket_walking_idle_idle",
- &"pocket_walking_idle_show",
- &"pocket_walking_show_idle",
- &"pocket_walking_show_show",
- &"release",
- &"release_sit",
- &"retract",
- &"retract_sit",
- &"running",
- &"running_show",
- &"sneaking",
- &"strike",
- &"walking",
- &"walking_show",
-]
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var character := CHARACTER_SCENE.instantiate()
- root.add_child(character)
- var skeleton := _find_first(character, "Skeleton3D") as Skeleton3D
- var animation_player := _find_first(
- character,
- "AnimationPlayer",
- ) as AnimationPlayer
- assert(skeleton != null)
- assert(animation_player != null)
-
- var socket_index := skeleton.find_bone("rod_socket")
- assert(socket_index >= 0)
- var socket_direction_index := skeleton.find_bone("rod_socket.001")
- assert(socket_direction_index >= 0)
- assert(skeleton.get_bone_parent(socket_direction_index) == socket_index)
- var animation_names := animation_player.get_animation_list()
- assert(animation_names.size() == EXPECTED_ANIMATIONS.size())
- for expected in EXPECTED_ANIMATIONS:
- assert(expected in animation_names, "Missing animation: %s" % expected)
- var animation := animation_player.get_animation(expected)
- if expected in [&"casting", &"draw", &"idle_sneak", &"sneaking", &"strike"]:
- var has_socket_track: bool = false
- for track_index in animation.get_track_count():
- has_socket_track = has_socket_track or String(
- animation.track_get_path(track_index)
- ).contains("rod_socket")
- assert(
- has_socket_track,
- "Animation %s lost its authored rod socket pose." % expected,
- )
-
- var arms := character.find_child(
- "body_arms",
- true,
- false,
- ) as MeshInstance3D
- assert(arms != null)
- var blended_arm_vertices := _validate_mesh_weights(arms)
- assert(blended_arm_vertices > 0, "body_arms has no blended joint weights.")
- var body := character.find_child(
- "body_main",
- true,
- false,
- ) as MeshInstance3D
- assert(body != null)
- _validate_mesh_weights(body)
-
- print(
- "Character rig validation: PASS "
- + "(animations=%d, blended arm vertices=%d)"
- % [EXPECTED_ANIMATIONS.size(), blended_arm_vertices]
- )
- character.queue_free()
- quit()
-
-
-func _validate_mesh_weights(mesh_instance: MeshInstance3D) -> int:
- var blended_vertices := 0
- for surface_index in mesh_instance.mesh.get_surface_count():
- var arrays := mesh_instance.mesh.surface_get_arrays(surface_index)
- var weights: PackedFloat32Array = arrays[Mesh.ARRAY_WEIGHTS]
- var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
- assert(not weights.is_empty())
- assert(not vertices.is_empty())
- var components_per_vertex := weights.size() / vertices.size()
- assert(components_per_vertex == 4 or components_per_vertex == 8)
- for offset in range(0, weights.size(), components_per_vertex):
- var total := 0.0
- var influence_count := 0
- for component in range(components_per_vertex):
- var weight := weights[offset + component]
- total += weight
- if weight > 0.000001:
- influence_count += 1
- assert(absf(total - 1.0) <= 0.0001)
- assert(influence_count <= 2)
- if influence_count == 2:
- blended_vertices += 1
- return blended_vertices
-
-
-func _find_first(parent: Node, class_name_to_find: String) -> Node:
- if parent.is_class(class_name_to_find):
- return parent
- for child in parent.get_children():
- var found := _find_first(child, class_name_to_find)
- if found != null:
- return found
- return null
diff --git a/tests/character_rig_validation.gd.uid b/tests/character_rig_validation.gd.uid
deleted file mode 100644
index f7bda09..0000000
--- a/tests/character_rig_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://di6r1gphajq8u
diff --git a/tests/character_scale_validation.gd b/tests/character_scale_validation.gd
deleted file mode 100644
index 213f64f..0000000
--- a/tests/character_scale_validation.gd
+++ /dev/null
@@ -1,117 +0,0 @@
-extends SceneTree
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var legacy_snapshot: Dictionary = (
- CharacterCustomizationCatalog.default_snapshot()
- )
- legacy_snapshot.erase(CharacterCustomizationCatalog.SCALE_CATEGORY_ID)
- var migrated: Dictionary = (
- CharacterCustomizationCatalog.sanitized_snapshot(legacy_snapshot)
- )
- assert(
- is_equal_approx(
- float(migrated[CharacterCustomizationCatalog.SCALE_CATEGORY_ID]),
- CharacterCustomizationCatalog.DEFAULT_CHARACTER_SCALE,
- )
- )
- assert(CharacterCustomizationCatalog.validate_snapshot(migrated))
-
- var small_snapshot := migrated.duplicate(true)
- small_snapshot[CharacterCustomizationCatalog.SCALE_CATEGORY_ID] = (
- CharacterCustomizationCatalog.MIN_CHARACTER_SCALE
- )
- assert(CharacterCustomizationCatalog.validate_snapshot(small_snapshot))
- var large_snapshot := migrated.duplicate(true)
- large_snapshot[CharacterCustomizationCatalog.SCALE_CATEGORY_ID] = (
- CharacterCustomizationCatalog.MAX_CHARACTER_SCALE
- )
- assert(CharacterCustomizationCatalog.validate_snapshot(large_snapshot))
- var invalid_snapshot := migrated.duplicate(true)
- invalid_snapshot[CharacterCustomizationCatalog.SCALE_CATEGORY_ID] = 2.0
- assert(not CharacterCustomizationCatalog.validate_snapshot(invalid_snapshot))
-
- var visuals: Node3D = PlayerVisualPresenter.instantiate_visuals()
- root.add_child(visuals)
- PlayerVisualPresenter.apply_appearance(visuals, large_snapshot)
- assert(visuals.scale.is_equal_approx(
- Vector3.ONE * CharacterCustomizationCatalog.MAX_CHARACTER_SCALE
- ))
- var skeleton := visuals.find_child("Skeleton3D", true, false) as Skeleton3D
- assert(skeleton != null)
- var body := skeleton.get_node_or_null("body_main") as MeshInstance3D
- assert(body != null)
- var body_material := body.material_override as StandardMaterial3D
- assert(body_material != null)
- assert(
- body_material.texture_filter
- == BaseMaterial3D.TEXTURE_FILTER_NEAREST
- )
- assert(body_material.shading_mode == BaseMaterial3D.SHADING_MODE_UNSHADED)
- assert(is_zero_approx(body_material.metallic))
- assert(is_zero_approx(body_material.metallic_specular))
- assert(is_equal_approx(body_material.roughness, 1.0))
- assert(body_material.diffuse_mode == BaseMaterial3D.DIFFUSE_LAMBERT)
- assert(not body_material.anisotropy_enabled)
- assert(not body_material.clearcoat_enabled)
- assert(not body_material.rim_enabled)
-
- var signed_small := {"appearance": small_snapshot}
- var signed_large := {"appearance": large_snapshot}
- assert(
- NetworkProfileProtocol.signature_fields(signed_small)
- != NetworkProfileProtocol.signature_fields(signed_large)
- )
-
- assert(
- ProfilePreview.calculate_pixelated_grid_size(
- Vector2(260.0, 330.0),
- Vector2(1280.0, 720.0),
- 3,
- ) == Vector2i(130, 165)
- )
- assert(
- ProfilePreview.calculate_pixelated_grid_size(
- Vector2(260.0, 330.0),
- Vector2(640.0, 480.0),
- 3,
- ) == Vector2i(65, 83)
- )
-
- var profile_page := ProfilePage.new()
- root.add_child(profile_page)
- await process_frame
- profile_page.call(
- "_select_category",
- CharacterCustomizationCatalog.SCALE_CATEGORY_ID,
- )
- await process_frame
- var slider := profile_page.find_child(
- "CharacterScaleSlider", true, false
- ) as HSlider
- assert(slider != null)
- assert(is_equal_approx(
- float(slider.min_value),
- CharacterCustomizationCatalog.MIN_CHARACTER_SCALE,
- ))
- assert(is_equal_approx(
- float(slider.max_value),
- CharacterCustomizationCatalog.MAX_CHARACTER_SCALE,
- ))
- slider.value = 1.15
- await process_frame
- var draft: Dictionary = profile_page.get("_draft_appearance")
- assert(is_equal_approx(
- float(draft[CharacterCustomizationCatalog.SCALE_CATEGORY_ID]),
- 1.15,
- ))
-
- print("Character scale validation: PASS")
- profile_page.queue_free()
- visuals.queue_free()
- await process_frame
- quit()
diff --git a/tests/character_scale_validation.gd.uid b/tests/character_scale_validation.gd.uid
deleted file mode 100644
index c74c5d0..0000000
--- a/tests/character_scale_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://d0w8gv1i4paii
diff --git a/tests/chat_privacy_multiplayer_validation.gd b/tests/chat_privacy_multiplayer_validation.gd
deleted file mode 100644
index e1d6f87..0000000
--- a/tests/chat_privacy_multiplayer_validation.gd
+++ /dev/null
@@ -1,124 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-const TEST_PORT: int = 18152
-const PREJOIN_MESSAGE: String = "private prejoin message"
-const LIVE_MESSAGE: String = "live message after join"
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var arguments: PackedStringArray = OS.get_cmdline_user_args()
- if arguments.has("host"):
- await _run_host()
- return
- if arguments.has("client"):
- await _run_client()
- return
- push_error("Chat privacy validation needs host or client mode.")
- quit(1)
-
-
-func _run_host() -> void:
- var main: Node = await _create_initialized_main()
- var session := main.get_node("%NetworkSession") as NetworkSession
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(bool(main.call(
- "_apply_world", WorldLayout.STARTER_ISLAND, 1, true
- )))
- assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
- assert(session.start_private_host(TEST_PORT))
- assert(save_manager.initialize_new_game())
- main.call("_enter_gameplay")
- for _frame: int in 4:
- await physics_frame
- assert(session.set_host_open(true))
- var chat_service := main.get_node(
- "%NetworkChatService"
- ) as NetworkChatService
- assert(chat_service.send_local_message(PREJOIN_MESSAGE))
- var remote_peer_id: int = await _wait_for_remote_peer(session)
- assert(remote_peer_id > 1)
- await create_timer(1.0).timeout
- assert(chat_service.send_local_message(LIVE_MESSAGE))
- var disconnect_deadline: int = Time.get_ticks_msec() + 8000
- while (
- Time.get_ticks_msec() < disconnect_deadline
- and session.is_authenticated_peer(remote_peer_id)
- ):
- await process_frame
- assert(not session.is_authenticated_peer(remote_peer_id))
- print("Chat privacy multiplayer host validation: PASS")
- await _session_cleanup(main, session)
-
-
-func _run_client() -> void:
- var main: Node = await _create_initialized_main()
- main.call("_on_title_join_game_requested", "127.0.0.1:%d" % TEST_PORT)
- var session := main.get_node("%NetworkSession") as NetworkSession
- var join_deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < join_deadline:
- await process_frame
- if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
- main.call("_confirm_server_trust")
- if session.is_joined_client() and bool(main.get("_gameplay_started")):
- break
- assert(session.is_joined_client())
- var chat_service := main.get_node(
- "%NetworkChatService"
- ) as NetworkChatService
- await create_timer(0.5).timeout
- assert(not _history_contains(chat_service, PREJOIN_MESSAGE))
- var live_deadline: int = Time.get_ticks_msec() + 8000
- while (
- Time.get_ticks_msec() < live_deadline
- and not _history_contains(chat_service, LIVE_MESSAGE)
- ):
- await process_frame
- assert(_history_contains(chat_service, LIVE_MESSAGE))
- assert(not _history_contains(chat_service, PREJOIN_MESSAGE))
- print("Chat privacy multiplayer client validation: PASS")
- await _session_cleanup(main, session)
-
-
-func _history_contains(service: NetworkChatService, body: String) -> bool:
- return service.get_history().any(
- func(message: Dictionary) -> bool:
- return str(message.get("body", "")) == body
- )
-
-
-func _create_initialized_main() -> Node:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- return main
-
-
-func _wait_for_remote_peer(session: NetworkSession) -> int:
- var deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- for peer_id: int in session.get_authenticated_peer_ids():
- if peer_id != session.get_local_peer_id():
- return peer_id
- return 0
-
-
-func _session_cleanup(main: Node, session: NetworkSession) -> void:
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
diff --git a/tests/chat_privacy_multiplayer_validation.gd.uid b/tests/chat_privacy_multiplayer_validation.gd.uid
deleted file mode 100644
index 33b210d..0000000
--- a/tests/chat_privacy_multiplayer_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ivtqrnppnmli
diff --git a/tests/controller_focus_presentation_validation.gd b/tests/controller_focus_presentation_validation.gd
deleted file mode 100644
index 7f59f70..0000000
--- a/tests/controller_focus_presentation_validation.gd
+++ /dev/null
@@ -1,478 +0,0 @@
-extends SceneTree
-
-const FocusPresentationType = preload(
- "res://ui/controller_focus_presentation.gd"
-)
-const FishBatchSelectionType = preload(
- "res://ui/fish_batch_selection.gd"
-)
-
-var _failures: Array[String] = []
-
-
-func _initialize() -> void:
- _run.call_deferred()
-
-
-func _run() -> void:
- _validate_controller_cooler_focus()
- root.size = Vector2i(1280, 720)
- var stage := Control.new()
- stage.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
- root.add_child(stage)
- var presentation := FocusPresentationType.new()
- stage.add_child(presentation)
- var standard_button := Button.new()
- standard_button.text = "standard"
- standard_button.position = Vector2(80.0, 60.0)
- standard_button.custom_minimum_size = Vector2(120.0, 60.0)
- var normal_style := _button_style(Color("123f4e"))
- var old_hover_style := _button_style(Color("58c6d4"))
- var old_focus_style := _button_style(Color("238697"))
- standard_button.add_theme_stylebox_override("normal", normal_style)
- standard_button.add_theme_stylebox_override("hover", old_hover_style)
- standard_button.add_theme_stylebox_override("focus", old_focus_style)
- stage.add_child(standard_button)
- var second_button := Button.new()
- second_button.text = "second"
- second_button.position = Vector2(260.0, 60.0)
- second_button.custom_minimum_size = Vector2(120.0, 60.0)
- # Legacy opt-outs must not prevent the new universal cursor from appearing.
- second_button.set_meta(&"controller_focus_inversion_disabled", true)
- stage.add_child(second_button)
- await process_frame
-
- standard_button.grab_focus()
- await process_frame
- _expect(
- root.gui_get_focus_owner() == null,
- "a menu's programmatic first focus is visible before navigation input",
- )
- _expect(
- presentation.get("_neutral_focus_seed") == standard_button,
- "the neutral menu focus seed did not remember the first action",
- )
- var player_menu_tab := InputEventKey.new()
- player_menu_tab.physical_keycode = KEY_TAB
- player_menu_tab.pressed = true
- presentation._input(player_menu_tab)
- await process_frame
- _expect(
- root.gui_get_focus_owner() == null,
- "the default player-menu Tab was consumed as focus navigation",
- )
- _expect(
- presentation.get("_neutral_focus_seed") == standard_button,
- "the default player-menu Tab discarded the neutral focus seed",
- )
- var first_navigation := InputEventJoypadButton.new()
- first_navigation.button_index = JOY_BUTTON_DPAD_DOWN
- first_navigation.pressed = true
- presentation._input(first_navigation)
- await process_frame
- _expect(
- root.gui_get_focus_owner() == standard_button,
- "the first directional input did not activate the neutral focus seed",
- )
-
- var controller_event := InputEventJoypadButton.new()
- controller_event.button_index = JOY_BUTTON_A
- controller_event.pressed = true
- presentation._input(controller_event)
- await process_frame
- var focus_arrow := presentation.get("_focus_arrow") as TextureRect
- var focus_shadow := presentation.get("_focus_arrow_shadow") as TextureRect
- _expect(focus_arrow != null, "controller focus arrow was not created")
- _expect(focus_shadow != null, "controller focus shadow was not created")
- _expect(focus_arrow.visible, "controller focus arrow is not visible")
- _expect(focus_shadow.visible, "controller focus shadow is not visible")
- _expect(
- focus_arrow.texture == FocusPresentationType.FOCUS_ARROW_TEXTURE,
- "controller focus arrow uses the wrong artwork",
- )
- _expect(
- focus_shadow.texture == focus_arrow.texture,
- "controller focus shadow does not follow the cursor silhouette",
- )
- _expect(
- focus_shadow.position.is_equal_approx(
- focus_arrow.position
- + FocusPresentationType.FOCUS_ARROW_SHADOW_OFFSET
- ),
- "controller focus shadow uses the wrong offset",
- )
- _expect(
- is_equal_approx(
- focus_arrow.rotation_degrees,
- FocusPresentationType.FOCUS_ARROW_ROTATION_DEGREES,
- ),
- "controller focus arrow does not point toward five o'clock",
- )
- _expect(
- standard_button.material == null,
- "controller focus still applies material inversion",
- )
- _expect(
- standard_button.get_theme_stylebox("focus") == normal_style,
- "native focus highlighting was not neutralized",
- )
- _expect(
- _arrow_marks_top_left(focus_arrow, standard_button.get_global_rect()),
- "controller focus arrow is not in the control's top-left corner",
- )
- stage.scale = Vector2.ONE * 0.5
- await process_frame
- _expect(
- focus_arrow.size.is_equal_approx(
- FocusPresentationType.FOCUS_ARROW_SIZE * 0.5
- ),
- "controller focus arrow does not follow canonical UI stage scale",
- )
- _expect(
- _arrow_marks_top_left(focus_arrow, standard_button.get_global_rect()),
- "scaled controller focus arrow left its control's top-left corner",
- )
- stage.scale = Vector2.ONE
- await process_frame
- var hover_motion := InputEventMouseMotion.new()
- hover_motion.position = standard_button.get_global_rect().get_center()
- hover_motion.global_position = hover_motion.position
- root.push_input(hover_motion, true)
- await process_frame
- _expect(
- root.gui_get_hovered_control() == standard_button,
- "the pointer did not reach the hover-suppression test button",
- )
- presentation._input(first_navigation)
- presentation._input(controller_event)
- await process_frame
- _expect(
- standard_button.get_theme_stylebox("hover") == normal_style,
- "controller use leaves a stale mouse-hover highlight visible",
- )
-
- second_button.grab_focus()
- await process_frame
- _expect(
- focus_arrow.visible,
- "legacy inversion metadata incorrectly hides the focus arrow",
- )
- _expect(
- _arrow_marks_top_left(focus_arrow, second_button.get_global_rect()),
- "controller focus arrow did not follow the focused control",
- )
- var bubble_button := BubbleButton.new()
- bubble_button.text = "bubble"
- bubble_button.position = Vector2(440.0, 60.0)
- bubble_button.size = Vector2(120.0, 120.0)
- stage.add_child(bubble_button)
- await process_frame
- bubble_button.grab_focus()
- await process_frame
- _expect(
- _arrow_overlaps_bubble_edge(focus_arrow, bubble_button),
- "controller focus arrow does not meet the visible bubble edge",
- )
-
- var pointer_motion := InputEventMouseMotion.new()
- pointer_motion.position = Vector2(4.0, 4.0)
- presentation._input(pointer_motion)
- await process_frame
- _expect(
- not focus_arrow.visible,
- "mouse motion does not clear controller focus presentation",
- )
- _expect(
- not focus_shadow.visible,
- "mouse motion leaves the controller focus shadow visible",
- )
- _expect(
- root.gui_get_focus_owner() == null,
- "mouse motion leaves a stale menu action focused",
- )
- _expect(
- standard_button.get_theme_stylebox("focus") == old_focus_style,
- "native theme overrides were not restored after controller use",
- )
- _expect(
- standard_button.get_theme_stylebox("hover") == old_hover_style,
- "mouse hover styling was not restored after controller use",
- )
- var item_list := ItemList.new()
- item_list.position = Vector2(80.0, 160.0)
- item_list.size = Vector2(240.0, 150.0)
- item_list.max_columns = 1
- for index: int in 4:
- item_list.add_item("list item %d" % index)
- stage.add_child(item_list)
- await process_frame
- item_list.select(0)
- item_list.grab_focus()
- await process_frame
- presentation._input(first_navigation)
- presentation._input(controller_event)
- await process_frame
- var first_item_arrow_y: float = focus_arrow.position.y
- item_list.select(2)
- await process_frame
- _expect(
- focus_arrow.position.y > first_item_arrow_y,
- "focus arrow does not follow the selected ItemList row",
- )
- _expect(
- item_list.get_theme_stylebox("selected") is StyleBoxEmpty,
- "ItemList still draws a selected-row cursor highlight",
- )
- _expect(
- item_list.get_theme_color("font_selected_color")
- == item_list.get_theme_color("font_color"),
- "ItemList still recolors the selected-row cursor text",
- )
-
- var tree := Tree.new()
- tree.position = Vector2(700.0, 160.0)
- tree.size = Vector2(240.0, 150.0)
- tree.hide_root = true
- var tree_root: TreeItem = tree.create_item()
- var first_tree_item: TreeItem = tree.create_item(tree_root)
- first_tree_item.set_text(0, "first folder")
- var second_tree_item: TreeItem = tree.create_item(tree_root)
- second_tree_item.set_text(0, "second folder")
- stage.add_child(tree)
- await process_frame
- first_tree_item.select(0)
- tree.grab_focus()
- await process_frame
- var first_tree_arrow_y: float = focus_arrow.position.y
- second_tree_item.select(0)
- await process_frame
- _expect(
- focus_arrow.position.y > first_tree_arrow_y,
- "focus arrow does not follow the selected Tree row",
- )
-
- var popup := PopupMenu.new()
- for index: int in 5:
- popup.add_item("popup item %d" % index)
- root.add_child(popup)
- popup.popup(Rect2i(500, 300, 0, 0))
- popup.set_focused_item(0)
- for _frame: int in 2:
- await process_frame
- _expect(
- presentation.get("_focused_popup") == popup,
- "focus arrow did not enter an open PopupMenu",
- )
- _expect(
- presentation.get("_focus_layer").custom_viewport == popup,
- "focus arrow is not rendered inside the PopupMenu viewport",
- )
- _expect(
- popup.get_theme_stylebox("hover") is StyleBoxEmpty,
- "PopupMenu still draws its native focus highlight",
- )
- _expect(
- _arrow_marks_top_left(
- focus_arrow,
- presentation.call("_popup_focus_target_rect", popup) as Rect2,
- ),
- "focus arrow does not mark the focused PopupMenu item",
- )
- var first_popup_arrow_y: float = focus_arrow.position.y
- popup.set_focused_item(2)
- await process_frame
- _expect(
- focus_arrow.position.y > first_popup_arrow_y,
- "focus arrow does not follow PopupMenu item navigation",
- )
- popup.hide()
- popup.queue_free()
- await process_frame
- item_list.grab_focus()
- await process_frame
-
- var dialog := FileDialog.new()
- dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
- dialog.access = FileDialog.ACCESS_FILESYSTEM
- dialog.size = Vector2i(720, 480)
- root.add_child(dialog)
- dialog.popup_centered()
- for _frame: int in 2:
- await process_frame
- var dialog_cancel := dialog.get_cancel_button()
- dialog_cancel.grab_focus()
- for _frame: int in 2:
- await process_frame
- _expect(
- focus_arrow.visible,
- "focus arrow is hidden for an embedded dialog control",
- )
- _expect(
- presentation.get("_focus_layer").custom_viewport
- == dialog_cancel.get_viewport(),
- "focus arrow is not rendered inside the dialog viewport",
- )
- _expect(
- _arrow_marks_top_left(
- focus_arrow,
- presentation.call("_focus_target_rect", dialog_cancel) as Rect2,
- ),
- "focus arrow does not follow an embedded dialog control",
- )
- dialog.hide()
- dialog.queue_free()
- await process_frame
- item_list.grab_focus()
- await process_frame
-
- var scroll := ScrollContainer.new()
- scroll.position = Vector2(420.0, 160.0)
- scroll.size = Vector2(180.0, 90.0)
- scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
- stage.add_child(scroll)
- var list := VBoxContainer.new()
- list.custom_minimum_size.x = 160.0
- scroll.add_child(list)
- var last_scroll_button: Button
- for index: int in 8:
- var scroll_button := Button.new()
- scroll_button.text = "scroll option %d" % index
- scroll_button.custom_minimum_size = Vector2(160.0, 40.0)
- list.add_child(scroll_button)
- last_scroll_button = scroll_button
- await process_frame
- last_scroll_button.grab_focus()
- for _frame: int in 2:
- await process_frame
- _expect(
- scroll.scroll_vertical > 0,
- "controller focus does not reveal an off-screen selection",
- )
- _expect(
- root.gui_get_focus_owner() == last_scroll_button,
- "scroll following does not preserve the focused control",
- )
- _expect(
- _arrow_marks_top_left(
- focus_arrow,
- last_scroll_button.get_global_rect(),
- ),
- "focus arrow does not follow a scrolled control",
- )
-
- last_scroll_button.focus_mode = Control.FOCUS_NONE
- await process_frame
- _expect(
- not focus_arrow.visible,
- "focus arrow remains after its control becomes unfocusable",
- )
- var event_focused_button := Button.new()
- event_focused_button.text = "event focused"
- event_focused_button.position = Vector2(700.0, 360.0)
- event_focused_button.custom_minimum_size = Vector2(160.0, 50.0)
- stage.add_child(event_focused_button)
- await process_frame
- presentation._input(first_navigation)
- event_focused_button.grab_focus()
- await process_frame
- _expect(
- root.gui_get_focus_owner() == event_focused_button,
- (
- "a menu focus established by the current directional event was "
- + "mistaken for an automatic default"
- ),
- )
-
- presentation.set_virtual_pointer_active(true)
- await process_frame
- _expect(
- not focus_arrow.visible,
- "focus arrow competes with active virtual-pointer presentation",
- )
- presentation._input(controller_event)
- await process_frame
- _expect(
- not focus_arrow.visible,
- "controller input stole presentation from the held virtual pointer",
- )
- presentation.set_virtual_pointer_active(false)
- presentation._input(controller_event)
- await process_frame
- _expect(
- focus_arrow.visible,
- "controller focus presentation did not resume after virtual-pointer use",
- )
-
- stage.queue_free()
- if _failures.is_empty():
- print("Controller focus presentation validation: PASS")
- quit(0)
- return
- for failure: String in _failures:
- push_error(failure)
- quit(1)
-
-
-func _validate_controller_cooler_focus() -> void:
- var selection := FishBatchSelectionType.new()
- selection.set_visible_order([&"first", &"second"])
- selection.select_only(&"first")
- selection.focus_only(&"second")
- _expect(
- selection.get_focused_id() == &"second",
- "cooler focus does not follow controller navigation",
- )
- _expect(
- selection.get_selected_ids() == [&"first"],
- "moving cooler focus unexpectedly changes the selected catch set",
- )
-
-
-func _button_style(color: Color) -> StyleBoxFlat:
- var style := StyleBoxFlat.new()
- style.bg_color = color
- style.set_corner_radius_all(8)
- return style
-
-
-func _arrow_marks_top_left(arrow: Control, target: Rect2) -> bool:
- var arrow_pivot: Vector2 = arrow.position + arrow.size * 0.5
- var arrow_scale: Vector2 = arrow.size / FocusPresentationType.FOCUS_ARROW_SIZE
- var tip_from_pivot: Vector2 = (
- (FocusPresentationType.FOCUS_ARROW_TIP_UV - Vector2.ONE * 0.5)
- * arrow.size
- ).rotated(deg_to_rad(
- FocusPresentationType.FOCUS_ARROW_ROTATION_DEGREES
- ))
- var actual_tip: Vector2 = arrow_pivot + tip_from_pivot
- var expected_tip: Vector2 = (
- target.position
- + FocusPresentationType.FOCUS_ARROW_TARGET_OVERLAP * arrow_scale
- )
- return actual_tip.distance_to(expected_tip) <= 0.5
-
-
-func _arrow_overlaps_bubble_edge(
- arrow: Control,
- bubble: BubbleButton,
-) -> bool:
- var arrow_pivot: Vector2 = arrow.position + arrow.size * 0.5
- var tip_from_pivot: Vector2 = (
- (FocusPresentationType.FOCUS_ARROW_TIP_UV - Vector2.ONE * 0.5)
- * arrow.size
- ).rotated(deg_to_rad(
- FocusPresentationType.FOCUS_ARROW_ROTATION_DEGREES
- ))
- var actual_tip: Vector2 = arrow_pivot + tip_from_pivot
- var local_tip: Vector2 = (
- bubble.get_global_transform_with_canvas().affine_inverse()
- * actual_tip
- )
- var radius: Vector2 = bubble.size * 0.5
- var normalized: Vector2 = (local_tip - radius) / radius
- return normalized.length() >= 0.75 and normalized.length() <= 1.0
-
-
-func _expect(condition: bool, message: String) -> void:
- if not condition:
- _failures.append(message)
diff --git a/tests/controller_focus_presentation_validation.gd.uid b/tests/controller_focus_presentation_validation.gd.uid
deleted file mode 100644
index 0cacf4a..0000000
--- a/tests/controller_focus_presentation_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bjay8ki3albk6
diff --git a/tests/controller_focus_recovery_validation.gd.uid b/tests/controller_focus_recovery_validation.gd.uid
deleted file mode 100644
index 7bcb1a2..0000000
--- a/tests/controller_focus_recovery_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cqk75fxykqrhs
diff --git a/tests/controller_mapping_validation.gd b/tests/controller_mapping_validation.gd
deleted file mode 100644
index a5938d6..0000000
--- a/tests/controller_mapping_validation.gd
+++ /dev/null
@@ -1,510 +0,0 @@
-extends SceneTree
-
-const ControllerMappingManagerType = preload(
- "res://settings/controller_mapping_manager.gd"
-)
-const ControllerMappingPanelType = preload(
- "res://ui/controller_mapping_panel.gd"
-)
-const GameUIType = preload("res://ui/game_ui.gd")
-
-
-func _init() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- if (
- ControllerMappingManagerType.ROLE_LABELS[
- ControllerMappingManagerType.ROLE_CAMERA_ZOOM
- ] != "zoom modifier (+RT)"
- ):
- push_error("controller zoom modifier label is misleading")
- quit(1)
- return
- if (
- ControllerMappingManagerType.ROLE_LABELS[
- ControllerMappingManagerType.ROLE_RIGHT_STICK_Y
- ] != "camera up / down (+RT to zoom)"
- ):
- push_error("controller vertical camera label omits the RT zoom chord")
- quit(1)
- return
- var manager := ControllerMappingManagerType.new()
- root.add_child(manager)
- await process_frame
- var failure: String = _validate_manager(manager)
- if failure.is_empty():
- failure = _validate_portmaster_launcher()
- if failure.is_empty():
- failure = _validate_auto_map(manager)
- if failure.is_empty():
- failure = _validate_virtual_mouse_bounds()
- if failure.is_empty():
- failure = _validate_controller_scroll_target()
- if failure.is_empty():
- print("controller mapping validation passed")
- quit(0)
- return
- push_error(failure)
- quit(1)
-
-
-func _validate_manager(manager: ControllerMappingManagerType) -> String:
- var muos_mapping: String = (
- ControllerMappingManagerType.build_muos_controller_mapping(
- "19000000010000000100000000010000",
- "muOS-Keys",
- )
- )
- for expected_binding: String in [
- "a:b0",
- "b:b1",
- "x:b3",
- "y:b2",
- "leftshoulder:b4",
- "rightshoulder:b5",
- "lefttrigger:b10",
- "righttrigger:b11",
- "back:b6",
- "start:b7",
- "guide:b8",
- "leftstick:b9",
- "rightstick:b12",
- "leftx:a0",
- "righty:a3",
- ]:
- if expected_binding not in muos_mapping:
- return "muOS compatibility mapping omitted " + expected_binding
- if not muos_mapping.begins_with(
- "19000000010000000100000000010000,muOS-Keys,"
- ):
- return "muOS compatibility mapping does not use the runtime guid"
- if not ControllerMappingManagerType.is_muos_controller_name(
- "muOS-Keys"
- ):
- return "muOS controller name was not recognized"
- if ControllerMappingManagerType.is_muos_controller_name(
- "ordinary controller"
- ):
- return "muOS mapping would affect unrelated controllers"
- if not ControllerMappingManagerType.should_install_muos_compatibility_mapping(
- "muOS-Keys",
- false,
- ):
- return "unknown legacy muOS controller did not receive fallback mapping"
- if ControllerMappingManagerType.should_install_muos_compatibility_mapping(
- "muOS-Keys",
- true,
- ):
- return "recognized muOS controller would be replaced at runtime"
- if ControllerMappingManagerType.should_install_muos_compatibility_mapping(
- "ordinary controller",
- false,
- ):
- return "muOS fallback would affect an unrelated unknown controller"
- var defaults: Dictionary = manager.get_active_bindings()
- if defaults.size() != ControllerMappingManagerType.ROLE_ORDER.size():
- return "default mapping covers %d of %d controller roles: %s" % [
- defaults.size(),
- ControllerMappingManagerType.ROLE_ORDER.size(),
- str(defaults.keys()),
- ]
- if ControllerMappingManagerType.ROLE_LABELS.get(
- ControllerMappingManagerType.ROLE_RIGHT_STICK_CLICK, ""
- ) != "sneak":
- return "right-stick click is not presented as sneak"
- if ControllerMappingManagerType.BUTTON_ACTION_ROLES.get(
- &"sneak", &""
- ) != ControllerMappingManagerType.ROLE_RIGHT_STICK_CLICK:
- return "right-stick click is not assigned to the sneak action"
- if ControllerMappingManagerType.ROLE_LABELS.get(
- ControllerMappingManagerType.ROLE_Y, ""
- ) != "interact / character call":
- return "Y does not present its contextual gameplay actions"
- if ControllerMappingManagerType.BUTTON_ACTION_ROLES.get(
- &"interact", &""
- ) != ControllerMappingManagerType.ROLE_Y:
- return "Y is not assigned to the interact action"
- if ControllerMappingManagerType.BUTTON_ACTION_ROLES.get(
- &"character_call", &""
- ) != ControllerMappingManagerType.ROLE_Y:
- return "Y is not assigned to the character-call action"
- if not _has_joy_button(&"character_call", JOY_BUTTON_Y):
- return "character call does not default to Y"
- if not _has_joy_button(&"interact", JOY_BUTTON_Y):
- return "interact does not default to Y"
- if not _has_joy_button(&"sneak", JOY_BUTTON_RIGHT_STICK):
- return "sneak does not default to right-stick click"
- var trigger_button := InputEventJoypadButton.new()
- trigger_button.device = manager.get_active_device_id()
- trigger_button.button_index = JOY_BUTTON_MISC1
- trigger_button.pressed = true
- var trigger_binding: Dictionary = manager.binding_from_event(
- ControllerMappingManagerType.ROLE_POINTER_MODIFIER,
- trigger_button,
- )
- if str(trigger_binding.get("kind", "")) != "button":
- return "trigger role did not accept a button-backed handheld trigger"
- var trigger_motion := InputEventJoypadMotion.new()
- trigger_motion.device = manager.get_active_device_id()
- trigger_motion.axis = JOY_AXIS_TRIGGER_RIGHT
- trigger_motion.axis_value = 1.0
- trigger_binding = manager.binding_from_event(
- ControllerMappingManagerType.ROLE_POINTER_MODIFIER,
- trigger_motion,
- )
- if str(trigger_binding.get("kind", "")) != "axis":
- return "trigger role did not accept an axis-backed controller trigger"
- var stick_button := manager.binding_from_event(
- ControllerMappingManagerType.ROLE_RIGHT_STICK_X,
- trigger_button,
- )
- if not stick_button.is_empty():
- return "stick axis role accepted a button"
- var keyboard_events_before: int = _keyboard_event_count(&"jump")
- var custom: Dictionary = defaults.duplicate(true)
- custom[str(ControllerMappingManagerType.ROLE_A)] = {
- "kind": "button",
- "button": int(JOY_BUTTON_X),
- }
- custom[str(ControllerMappingManagerType.ROLE_X)] = {
- "kind": "button",
- "button": int(JOY_BUTTON_A),
- }
- custom[str(ControllerMappingManagerType.ROLE_POINTER_MODIFIER)] = (
- trigger_binding
- )
- if not manager.replace_active_bindings(custom):
- return "valid custom mapping could not be saved"
- if not manager.has_custom_mapping():
- return "saved custom mapping did not become active"
- var duplicate: Dictionary = custom.duplicate(true)
- duplicate[str(ControllerMappingManagerType.ROLE_X)] = (
- duplicate[str(ControllerMappingManagerType.ROLE_A)] as Dictionary
- ).duplicate(true)
- if manager.replace_active_bindings(duplicate):
- return "duplicate controller buttons were accepted"
- var shared_trigger_axis: Dictionary = custom.duplicate(true)
- shared_trigger_axis[
- str(ControllerMappingManagerType.ROLE_POINTER_MODIFIER)
- ] = {
- "kind": "axis",
- "axis": int(JOY_AXIS_TRIGGER_LEFT),
- "direction": 1.0,
- "rest": 0.0,
- }
- shared_trigger_axis[
- str(ControllerMappingManagerType.ROLE_CAMERA_ZOOM)
- ] = {
- "kind": "axis",
- "axis": int(JOY_AXIS_TRIGGER_LEFT),
- "direction": -1.0,
- "rest": 0.0,
- }
- if ControllerMappingManagerType.bindings_have_conflicts(
- shared_trigger_axis
- ):
- return "opposite halves of a shared trigger axis conflict"
- var malformed_profiles: Dictionary = manager._validated_profiles({
- "name:controller": {
- "controller_name": "controller",
- "bindings": duplicate,
- },
- })
- if not malformed_profiles.is_empty():
- return "a stored controller profile with duplicate buttons was accepted"
- if _keyboard_event_count(&"jump") != keyboard_events_before:
- return "controller remapping changed keyboard bindings"
- var active_button := InputEventJoypadButton.new()
- active_button.device = manager.get_active_device_id()
- active_button.button_index = JOY_BUTTON_X
- active_button.pressed = true
- if not manager.event_matches_role(
- active_button,
- ControllerMappingManagerType.ROLE_A,
- ):
- return "active controller event did not match its custom role"
- var inactive_button := active_button.duplicate() as InputEventJoypadButton
- inactive_button.device = manager.get_active_device_id() + 1
- if manager.event_matches_role(
- inactive_button,
- ControllerMappingManagerType.ROLE_A,
- ):
- return "inactive controller event matched the active profile"
- var inactive_release := InputEventJoypadButton.new()
- inactive_release.device = manager.get_active_device_id() + 2
- inactive_release.button_index = JOY_BUTTON_A
- inactive_release.pressed = false
- var active_device_before_release: int = manager.get_active_device_id()
- manager._input(inactive_release)
- if manager.get_active_device_id() != active_device_before_release:
- return "inactive controller button release stole ownership"
- for event: InputEvent in InputMap.action_get_events(&"jump"):
- if (
- event is InputEventJoypadButton
- and event.device != manager.get_active_device_id()
- ):
- return "custom InputMap event was not scoped to the active device"
- manager._axis_rest_by_device["4"] = {str(JOY_AXIS_LEFT_X): 0.0}
- var drift := InputEventJoypadMotion.new()
- drift.device = 4
- drift.axis = JOY_AXIS_LEFT_X
- drift.axis_value = 0.12
- if manager._axis_motion_claims_device(drift):
- return "minor inactive-stick drift claimed controller ownership"
- manager._input(drift)
- if manager.get_active_device_id() != active_device_before_release:
- return "minor inactive-stick drift changed the active controller"
- drift.axis_value = 0.72
- if not manager._axis_motion_claims_device(drift):
- return "intentional inactive-stick motion did not claim ownership"
- manager._input(drift)
- if manager.get_active_device_id() != drift.device:
- return "intentional stick motion did not change controller ownership"
- for event: InputEvent in InputMap.action_get_events(&"jump"):
- if event is InputEventJoypadButton and event.device != drift.device:
- return "active-device change did not rescope InputMap events"
- manager._set_active_controller(active_device_before_release)
- manager._axis_rest_by_device["5"] = {str(JOY_AXIS_LEFT_X): 0.0}
- manager._on_joy_connection_changed(5, false)
- if manager._axis_rest_by_device.has("5"):
- return "disconnected controller retained stale axis calibration"
- if not FileAccess.file_exists(
- ControllerMappingManagerType.PROFILE_PATH
- ):
- return "controller mapping was not stored device-locally"
- return ""
-
-
-func _validate_portmaster_launcher() -> String:
- const launcher_path: String = "res://scripts/portmaster/straywild.sh"
- if not FileAccess.file_exists(launcher_path):
- return "PortMaster launcher template is missing"
- var launcher: String = FileAccess.get_file_as_string(launcher_path)
- for expected_fragment: String in [
- "19004ca6010000000100000000010000",
- "19000000010000000100000000010000",
- "a:b0,b:b1,x:b3,y:b2",
- "leftshoulder:b4,rightshoulder:b5",
- "lefttrigger:b10,righttrigger:b11",
- "guide:b8,start:b7,back:b6",
- "leftstick:b9",
- "rightstick:b12",
- "straywild_controllerconfig=\"$GODOT_MUOS_MAPPING\"",
- "GPTOKEYB_CONFIG=\"$GAMEDIR/straywild.gptk\"",
- "$GPTOKEYB \"straywild.aarch64\" -c \"$GPTOKEYB_CONFIG\" &",
- "pm_platform_helper \"$GAME_EXECUTABLE\"",
- "\"$GAME_LAUNCHER\"",
- "\"$CONTROLLER_MAPPING_FILE\"",
- "pm_finish",
- ]:
- if expected_fragment not in launcher:
- return "PortMaster launcher omitted " + expected_fragment
- if "$'\\n'" in launcher:
- return "PortMaster launcher appends mappings across a Weston-unsafe newline"
- const game_launcher_path: String = (
- "res://scripts/portmaster/launch-straywild.sh"
- )
- if not FileAccess.file_exists(game_launcher_path):
- return "PortMaster game launcher wrapper is missing"
- var game_launcher: String = FileAccess.get_file_as_string(
- game_launcher_path
- )
- for expected_fragment: String in [
- "SDL_GAMECONTROLLERCONFIG=\"$(<\"$CONTROLLER_MAPPING_FILE\")\"",
- "export SDL_GAMECONTROLLERCONFIG",
- "exec \"$@\"",
- ]:
- if expected_fragment not in game_launcher:
- return "PortMaster game launcher omitted " + expected_fragment
- return ""
-
-
-func _validate_auto_map(manager: ControllerMappingManagerType) -> String:
- if manager.get_role_prompt(ControllerMappingManagerType.ROLE_LT) != (
- "press or squeeze LT"
- ):
- return "auto-map does not identify the LT control"
- if manager.get_role_prompt(ControllerMappingManagerType.ROLE_RT) != (
- "press or squeeze RT"
- ):
- return "auto-map does not identify the RT control"
- var panel := ControllerMappingPanelType.new()
- root.add_child(panel)
- panel.setup(manager)
- panel.open_panel()
- var mapping_scrolls: Array[Node] = panel.find_children(
- "", "ScrollContainer", true, false
- )
- if mapping_scrolls.is_empty():
- return "controller mapper does not contain its mapping list"
- if not (mapping_scrolls[0] as ScrollContainer).follow_focus:
- return "controller mapper does not scroll to follow controller focus"
- var first_binding := panel._binding_buttons.get(
- ControllerMappingManagerType.ROLE_ORDER.front()
- ) as Button
- var last_binding := panel._binding_buttons.get(
- ControllerMappingManagerType.ROLE_ORDER.back()
- ) as Button
- if first_binding.get_node(first_binding.focus_neighbor_top) != first_binding:
- return "controller mapper loses focus above its first row"
- if last_binding.get_node(last_binding.focus_neighbor_bottom) != (
- panel._close_button
- ):
- return "controller mapper cannot reach Done from its last row"
- if panel._close_button.get_node(
- panel._close_button.focus_neighbor_top
- ) != last_binding:
- return "controller mapper cannot return from Done to its last row"
- panel._begin_auto_map()
- for role: StringName in ControllerMappingManagerType.ROLE_ORDER:
- panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS)
- var binding := (
- ControllerMappingManagerType.default_bindings()[str(role)]
- as Dictionary
- )
- if str(binding.get("kind", "")) == "button":
- var button := InputEventJoypadButton.new()
- button.device = manager.get_active_device_id()
- button.button_index = int(binding.get("button", -1))
- button.pressed = true
- manager.controller_input_observed.emit(button)
- else:
- var motion := InputEventJoypadMotion.new()
- motion.device = manager.get_active_device_id()
- motion.axis = int(binding.get("axis", -1))
- motion.axis_value = float(binding.get("direction", -1.0))
- manager.controller_input_observed.emit(motion)
- if panel._auto_map_active:
- return "auto-map did not complete after every requested input"
- if not manager.has_custom_mapping():
- return "completed auto-map did not activate its profile"
- if panel._binding_buttons.size() != (
- ControllerMappingManagerType.ROLE_ORDER.size()
- ):
- return "manual override list does not expose every mapped role"
- var duplicate_saved: bool = manager.set_binding(
- ControllerMappingManagerType.ROLE_B,
- manager.get_binding(ControllerMappingManagerType.ROLE_A),
- )
- if duplicate_saved:
- return "manual mapping accepted a controller button already in use"
- panel._begin_manual_capture(
- ControllerMappingManagerType.ROLE_POINTER_MODIFIER
- )
- panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS)
- if "left trigger" in panel._progress_label.text.to_lower():
- return "manual remapping still dictates a specific physical input"
- if "any button" not in panel._progress_label.text.to_lower():
- return "manual remapping does not request a generic controller input"
- panel._cancel_capture()
- panel._begin_auto_map()
- panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS)
- var lone_button := InputEventJoypadButton.new()
- lone_button.device = manager.get_active_device_id()
- lone_button.button_index = JOY_BUTTON_B
- lone_button.pressed = true
- manager.controller_input_observed.emit(lone_button)
- if not panel._auto_map_active or panel._auto_map_index != 1:
- return "a single controller button cancelled auto-map"
- panel._cancel_capture()
- panel._begin_auto_map()
- panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS)
- var left_bumper := InputEventJoypadButton.new()
- left_bumper.device = manager.get_active_device_id()
- left_bumper.button_index = JOY_BUTTON_LEFT_SHOULDER
- left_bumper.pressed = true
- manager.controller_input_observed.emit(left_bumper)
- var right_bumper := InputEventJoypadButton.new()
- right_bumper.device = manager.get_active_device_id()
- right_bumper.button_index = JOY_BUTTON_RIGHT_SHOULDER
- right_bumper.pressed = true
- manager.controller_input_observed.emit(right_bumper)
- panel._process(ControllerMappingPanelType.CANCEL_COMBO_HOLD_SECONDS - 0.01)
- if not panel._auto_map_active:
- return "bumper combo cancelled auto-map before the hold threshold"
- panel._process(0.02)
- if panel._auto_map_active or panel.is_capturing():
- return "sustained bumper combo did not cancel auto-map"
- panel._begin_auto_map()
- panel._process(ControllerMappingPanelType.CAPTURE_NEUTRAL_SECONDS)
- var held_left := InputEventJoypadMotion.new()
- held_left.device = manager.get_active_device_id()
- held_left.axis = JOY_AXIS_LEFT_X
- held_left.axis_value = -1.0
- for _index: int in 4:
- manager.controller_input_observed.emit(held_left)
- if panel._auto_map_index != 0:
- return "auto-map accepted repeated analog motion without a neutral gate"
- var first_button := InputEventJoypadButton.new()
- first_button.device = manager.get_active_device_id()
- first_button.button_index = JOY_BUTTON_A
- first_button.pressed = true
- manager.controller_input_observed.emit(first_button)
- if panel._auto_map_index != 1 or not panel._waiting_for_neutral:
- return "auto-map did not gate the next step after a captured input"
- manager.controller_input_observed.emit(first_button)
- if panel._auto_map_index != 1:
- return "held input advanced more than one auto-map step"
- panel._cancel_capture()
- return ""
-
-
-func _validate_virtual_mouse_bounds() -> String:
- var window_size := Vector2(3840.0, 2160.0)
- var bounds: Rect2 = GameUIType.get_virtual_mouse_window_bounds(window_size)
- var expected_margin := Vector2(42.0, 42.0)
- if not bounds.position.is_equal_approx(expected_margin):
- return "4K virtual cursor minimum bounds are incorrect: %s" % bounds
- if not bounds.end.is_equal_approx(window_size - expected_margin):
- return "4K virtual cursor maximum bounds are incorrect: %s" % bounds
- return ""
-
-
-func _validate_controller_scroll_target() -> String:
- var game_ui := GameUIType.new()
- var page := VBoxContainer.new()
- var scroll := ScrollContainer.new()
- var content := VBoxContainer.new()
- var button := Button.new()
- var footer_button := Button.new()
- root.add_child(page)
- page.add_child(scroll)
- scroll.add_child(content)
- content.add_child(button)
- page.add_child(footer_button)
- var target: ScrollContainer = game_ui._focused_scroll_container(button)
- if target != scroll:
- game_ui.free()
- page.free()
- return "focused menu content did not resolve its scroll container"
- target = game_ui._focused_scroll_container(footer_button)
- if target != scroll:
- game_ui.free()
- page.free()
- return "menu footer focus did not resolve the page's only scroll container"
- scroll.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
- if game_ui._focused_scroll_container(button) != null:
- game_ui.free()
- page.free()
- return "controller scrolling ignored a disabled scroll container"
- game_ui.free()
- page.free()
- return ""
-
-
-func _keyboard_event_count(action: StringName) -> int:
- var count: int = 0
- for event: InputEvent in InputMap.action_get_events(action):
- if event is InputEventKey or event is InputEventMouseButton:
- count += 1
- return count
-
-
-func _has_joy_button(action: StringName, button_index: JoyButton) -> bool:
- for event: InputEvent in InputMap.action_get_events(action):
- var button := event as InputEventJoypadButton
- if button != null and button.button_index == button_index:
- return true
- return false
diff --git a/tests/controller_mapping_validation.gd.uid b/tests/controller_mapping_validation.gd.uid
deleted file mode 100644
index 7840fe3..0000000
--- a/tests/controller_mapping_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dhwkjojmpjc13
diff --git a/tests/controller_menu_accessibility_validation.gd b/tests/controller_menu_accessibility_validation.gd
deleted file mode 100644
index 37e43d5..0000000
--- a/tests/controller_menu_accessibility_validation.gd
+++ /dev/null
@@ -1,2054 +0,0 @@
-extends SceneTree
-
-const JoinGamePageScene = preload(
- "res://ui/network/join_game_page.tscn"
-)
-const TitleScreenScene = preload("res://ui/title_screen.tscn")
-const PauseMenuScene = preload("res://ui/pause_menu.tscn")
-const SettingsPanelScene = preload("res://ui/settings_panel.tscn")
-const SaveSlotsPageScene = preload("res://ui/save_slots_page.tscn")
-const BubbleConfirmationScene = preload(
- "res://ui/components/bubble_menu/bubble_confirmation_page.tscn"
-)
-const TitleConfirmationScene = preload(
- "res://ui/title_confirmation_bubble_page.tscn"
-)
-const MailPageType = preload("res://ui/mail_page.gd")
-const LogbookPageType = preload("res://ui/logbook_page.gd")
-const ProfilePageType = preload("res://ui/profile_page.gd")
-const VoiceProfilesType = preload(
- "res://player/animalese_voice_profiles.gd"
-)
-const PlayerMenuScene = preload("res://ui/player_menu.tscn")
-const PlayerMenuType = preload("res://ui/player_menu.gd")
-const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
-const ItemCatalogResource: ItemCatalog = preload(
- "res://items/catalog/item_catalog.tres"
-)
-const PlayersPageType = preload("res://ui/players_page.gd")
-const TheNetPageType = preload("res://ui/the_net_page.gd")
-const ControllerMappingManagerType = preload(
- "res://settings/controller_mapping_manager.gd"
-)
-const DialogControllerNavigationType = preload(
- "res://ui/file_dialog_controller_navigation.gd"
-)
-
-var _failures: Array[String] = []
-
-
-func _initialize() -> void:
- _run.call_deferred()
-
-
-func _run() -> void:
- root.size = Vector2i(1280, 720)
- await _validate_primary_menu_navigation()
- await _validate_save_slots_navigation()
- await _validate_join_game_navigation()
- await _validate_data_settings_navigation()
- await _validate_settings_adjustment_navigation()
- await _validate_settings_presentation_requires_apply()
- await _validate_mail_navigation()
- await _validate_profile_confirmation_focus()
- await _validate_profile_voice_navigation()
- await _validate_player_menu_toggle_queue()
- await _validate_inventory_tab_zone_transitions()
- await _validate_player_menu_nested_controller_back()
- await _validate_confirmation_dialog_navigation()
- await _validate_bubble_confirmation_navigation()
- _validate_mapping_capture_contract()
- if _failures.is_empty():
- print("Controller menu accessibility validation: PASS")
- quit(0)
- return
- for failure: String in _failures:
- push_error(failure)
- quit(1)
-
-
-func _validate_player_menu_toggle_queue() -> void:
- var menu := PlayerMenuScene.instantiate() as Control
- root.add_child(menu)
- await process_frame
- menu.visible = true
- menu.set("_transitioning", true)
- var toggle := InputEventAction.new()
- toggle.action = &"open_backpack"
- toggle.pressed = true
- menu.call("_input", toggle)
- _expect(
- bool(menu.get("_backpack_toggle_queued")),
- "Player menu discarded TAB during its entry transition.",
- )
- menu.call("_input", toggle)
- _expect(
- not bool(menu.get("_backpack_toggle_queued")),
- "Repeated TAB presses did not preserve toggle parity.",
- )
- menu.call("_input", toggle)
- menu.call(
- "_finish_menu_entry",
- int(menu.get("_transition_generation")),
- )
- await create_timer(0.5).timeout
- _expect(
- not menu.visible,
- "A queued TAB press did not close the menu after entry.",
- )
- menu.queue_free()
- await process_frame
-
-
-func _validate_primary_menu_navigation() -> void:
- var title := TitleScreenScene.instantiate() as Control
- root.add_child(title)
- await process_frame
- (title.get_node("%ButtonCenter") as Control).show()
- (title.get_node("%BubbleField") as Control).show()
- title.call("_update_title_layout")
- title.call("_set_title_bubbles_interactive", true)
- var title_controls: Array[Control] = []
- for candidate: Variant in title.call("_get_title_buttons"):
- var control := candidate as Control
- if control is BaseButton:
- (control as BaseButton).disabled = false
- title_controls.append(control)
- _expect(
- title_controls.size() == 5,
- "Title menu does not expose its five primary actions.",
- )
- var play_button := title.get_node("%PlayButton") as Button
- _expect(
- play_button.icon != null
- and play_button.icon.resource_path
- == "res://ui/icons/new_menu/play.png"
- and play_button.text.is_empty(),
- "The Play action is not presented as the large square icon button.",
- )
- _expect(
- is_equal_approx(play_button.size.x, play_button.size.y),
- "The Play action is not square.",
- )
- var online_button := title.get_node("%JoinGameButton") as Button
- var settings_button := title.get_node("%SettingsButton") as Button
- var credits_button := title.get_node("%CreditsButton") as Button
- var close_button := title.get_node("%QuitButton") as Button
- for secondary_button: Button in [
- online_button,
- settings_button,
- credits_button,
- close_button,
- ]:
- _expect(
- is_equal_approx(
- secondary_button.size.x,
- secondary_button.size.y,
- ),
- "A secondary title action is not square.",
- )
- _expect(
- is_equal_approx(
- close_button.position.y + close_button.size.y
- - online_button.position.y,
- play_button.size.y,
- ),
- "The secondary title-action stack does not match Play's height.",
- )
- _expect(
- not (title.get_node("%NewGameButton") as Control).visible
- and not (title.get_node("%DeleteSaveButton") as Control).visible,
- "Legacy New/Delete title bubbles are still visible.",
- )
- _assert_neighbor(
- play_button,
- &"focus_neighbor_right",
- title.get_node("%JoinGameButton") as Control,
- )
- _assert_neighbor(
- title.get_node("%JoinGameButton") as Control,
- &"focus_neighbor_left",
- play_button,
- )
- _assert_neighbor(
- title.get_node("%JoinGameButton") as Control,
- &"focus_neighbor_bottom",
- title.get_node("%SettingsButton") as Control,
- )
- _assert_neighbor(
- title.get_node("%SettingsButton") as Control,
- &"focus_neighbor_bottom",
- title.get_node("%CreditsButton") as Control,
- )
- _assert_neighbor(
- title.get_node("%CreditsButton") as Control,
- &"focus_neighbor_bottom",
- title.get_node("%QuitButton") as Control,
- )
- for title_control: Control in title_controls:
- _assert_directionally_reachable(title_control, title_controls)
- title.queue_free()
- await process_frame
-
- var pause := PauseMenuScene.instantiate() as Control
- root.add_child(pause)
- pause.show()
- var root_page := pause.get_node("%RootPage") as Control
- root_page.call("show_page", false)
- for _frame: int in 2:
- await process_frame
- var pause_controls: Array[Control] = [
- pause.get_node("%ResumeButton") as Control,
- pause.get_node("%SaveButton") as Control,
- pause.get_node("%JoinGameButton") as Control,
- pause.get_node("%SettingsButton") as Control,
- pause.get_node("%ReturnToTitleButton") as Control,
- pause.get_node("%ResetProgressButton") as Control,
- pause.get_node("%QuitButton") as Control,
- ]
- _assert_directionally_reachable(pause_controls.front(), pause_controls)
- pause.queue_free()
- await process_frame
-
-
-func _validate_save_slots_navigation() -> void:
- var page := SaveSlotsPageScene.instantiate() as SaveSlotsPage
- root.add_child(page)
- await process_frame
- page.open_page()
- for _frame: int in 2:
- await process_frame
- await create_timer(0.25).timeout
- var saves_tab := page.get_node("%SavesTab") as Button
- var new_tab := page.get_node("%NewSlotTab") as Button
- var content_panel := page.get_node("%ContentPanel") as PanelContainer
- var tab_overlap: float = (
- saves_tab.get_global_rect().end.y
- - content_panel.get_global_rect().position.y
- )
- _expect(
- is_equal_approx(tab_overlap, saves_tab.size.y * 0.5),
- "Save-slot content does not cover the lower half of its organizer tabs.",
- )
- _expect(
- page.get_active_page_id() == &"new",
- "An empty save catalog does not open directly to New Slot.",
- )
- _assert_neighbor(saves_tab, &"focus_neighbor_right", new_tab)
- _assert_neighbor(new_tab, &"focus_neighbor_left", saves_tab)
- var new_controls: Array[Control] = [
- saves_tab,
- new_tab,
- page.get_node("%NewSlotName") as Control,
- page.get_node("%GeneratedButton") as Control,
- page.get_node("%StarterButton") as Control,
- page.get_node("%RandomSeedButton") as Control,
- page.get_node("%CustomSeedButton") as Control,
- page.get_node("%CreateSlotButton") as Control,
- page.get_node("%BackButton") as Control,
- ]
- _assert_directionally_reachable(new_tab, new_controls)
- page.call("_select_page", &"saves", false)
- await process_frame
- var import_button := page.get_node("%ImportSlotButton") as Button
- var actions := page.get_node("%Actions") as VBoxContainer
- var secondary_actions := page.get_node("%SecondaryActions") as GridContainer
- var play_slot := page.get_node("%PlaySlotButton") as Button
- var rename_slot := page.get_node("%RenameSlotButton") as Button
- var duplicate_slot := page.get_node("%DuplicateSlotButton") as Button
- var export_slot := page.get_node("%ExportSlotButton") as Button
- var delete_slot := page.get_node("%DeleteSlotButton") as Button
- _expect(
- saves_tab.find_valid_focus_neighbor(SIDE_BOTTOM) == import_button,
- "An empty Save Slots page does not lead from its tab to Import Save.",
- )
- _expect(
- import_button.find_valid_focus_neighbor(SIDE_BOTTOM)
- == page.get_node("%BackButton"),
- "An empty Save Slots page does not lead from Import Save to Back.",
- )
- _expect(
- is_equal_approx(play_slot.size.x, actions.size.x),
- "Save-slot Play does not span the full action width.",
- )
- _expect(
- secondary_actions.columns == 2
- and is_equal_approx(rename_slot.size.x, duplicate_slot.size.x)
- and is_equal_approx(export_slot.size.x, delete_slot.size.x)
- and is_equal_approx(rename_slot.position.y, duplicate_slot.position.y)
- and is_equal_approx(export_slot.position.y, delete_slot.position.y)
- and export_slot.position.y > rename_slot.position.y,
- "Save-slot secondary actions are not arranged as a 2 by 2 grid.",
- )
- var play_style := play_slot.get_theme_stylebox("normal") as StyleBoxFlat
- _expect(
- play_style != null and play_style.bg_color == UtilityPageStyle.GREEN,
- "Save-slot Play does not use the green primary-action style.",
- )
- _assert_neighbor(play_slot, &"focus_neighbor_bottom", rename_slot)
- _assert_neighbor(rename_slot, &"focus_neighbor_right", duplicate_slot)
- _assert_neighbor(rename_slot, &"focus_neighbor_bottom", export_slot)
- _assert_neighbor(duplicate_slot, &"focus_neighbor_bottom", delete_slot)
- _assert_neighbor(export_slot, &"focus_neighbor_right", delete_slot)
- page.queue_free()
- await process_frame
-
-
-func _validate_join_game_navigation() -> void:
- var page := JoinGamePageScene.instantiate() as Control
- root.add_child(page)
- await process_frame
- page.show()
- await process_frame
- var discover := page.get_node("%DiscoverButton") as Button
- var friends := page.get_node("%FriendsButton") as Button
- var direct := page.get_node("%DirectButton") as Button
- var saved := page.get_node("%SavedButton") as Button
- var recent := page.get_node("%RecentButton") as Button
- var address := page.get_node("%Address") as LineEdit
- var name_edit := page.get_node("%NameEdit") as LineEdit
- var server_list := page.get_node("%ServerList") as ItemList
- var presence := page.get_node("%PresenceButton") as Button
- var room_open := page.get_node("%RoomOpenButton") as Button
- var room_listing := page.get_node("%RoomListingButton") as Button
- var refresh := page.get_node("%RefreshButton") as Button
- var join := page.get_node("%JoinButton") as Button
- var save := page.get_node("%SaveButton") as Button
- var edit := page.get_node("%EditButton") as Button
- var favorite := page.get_node("%FavoriteButton") as Button
- var delete := page.get_node("%DeleteButton") as Button
- var cancel := page.get_node("%CancelButton") as Button
- var back := page.get_node("%BackButton") as Button
- var direct_content := page.get_node("%DirectContent") as Control
- var list_content := page.get_node("%ListContent") as Control
- var content_panel := page.get_node("%ContentPanel") as PanelContainer
- var actions := page.get_node("%Actions") as HBoxContainer
- var modes: Array[Control] = [discover, friends, direct, saved, recent]
- var tab_overlap: float = (
- discover.get_global_rect().end.y
- - content_panel.get_global_rect().position.y
- )
- _expect(
- is_equal_approx(tab_overlap, discover.size.y * 0.5),
- "Join-game content does not cover the lower half of its organizer tabs.",
- )
-
- direct_content.hide()
- list_content.show()
- address.hide()
- name_edit.hide()
- server_list.show()
- _set_button_state(refresh, true)
- _set_button_state(join, true)
- _set_button_state(save, false)
- _set_button_state(edit, false)
- _set_button_state(favorite, false)
- _set_button_state(delete, false)
- _set_button_state(cancel, false)
- _set_button_state(back, true)
- page.set("_mode", JoinGamePage.Mode.DISCOVER)
- page.call("_configure_controller_navigation")
- await process_frame
- _assert_neighbor(discover, &"focus_neighbor_bottom", server_list)
- _assert_neighbor(server_list, &"focus_neighbor_top", discover)
- _assert_neighbor(server_list, &"focus_neighbor_bottom", refresh)
- _assert_neighbor(refresh, &"focus_neighbor_right", join)
- _assert_neighbor(join, &"focus_neighbor_bottom", back)
- _assert_neighbor(back, &"focus_neighbor_top", join)
- _expect(
- is_equal_approx(
- join.get_global_rect().end.x,
- actions.get_global_rect().end.x,
- ),
- "Discover Refresh and Join are not right-justified.",
- )
- var discover_controls: Array[Control] = modes.duplicate()
- discover_controls.append_array([server_list, refresh, join, back])
- _assert_directionally_reachable(discover, discover_controls)
- room_open.show()
- room_listing.show()
- _set_button_state(room_open, true)
- _set_button_state(room_listing, true)
- page.call("_configure_controller_navigation")
- await process_frame
- _assert_neighbor(room_open, &"focus_neighbor_right", room_listing)
- _assert_neighbor(room_open, &"focus_neighbor_left", room_open)
- _assert_neighbor(room_listing, &"focus_neighbor_left", room_open)
- _assert_neighbor(room_listing, &"focus_neighbor_bottom", server_list)
- var hosted_discover_controls: Array[Control] = modes.duplicate()
- hosted_discover_controls.append_array([
- room_open,
- room_listing,
- server_list,
- refresh,
- join,
- back,
- ])
- _assert_directionally_reachable(discover, hosted_discover_controls)
- var rooms: Array[Dictionary] = [
- {
- "room_id": "controller-default-room",
- "room_name": "Controller default room",
- "current_players": 1,
- "max_players": 8,
- },
- {
- "room_id": "controller-second-room",
- "room_name": "Controller second room",
- "current_players": 2,
- "max_players": 8,
- },
- ]
- page.call("_on_discovery_rooms_updated", rooms)
- _expect(
- int(page.get("_selected_discovery_index")) == 0,
- "Discovery should select its first room as soon as results arrive.",
- )
- _expect(
- server_list.is_selected(0),
- "Discovery's visible cursor and selected room should agree immediately.",
- )
-
- page.set("_mode", JoinGamePage.Mode.FRIENDS)
- (page.get_node("%OnlineControls") as Control).hide()
- presence.show()
- page.call("_configure_controller_navigation")
- await process_frame
- _assert_neighbor(friends, &"focus_neighbor_bottom", server_list)
- _assert_neighbor(server_list, &"focus_neighbor_top", friends)
- _assert_neighbor(server_list, &"focus_neighbor_bottom", presence)
- _assert_neighbor(presence, &"focus_neighbor_right", refresh)
- _assert_neighbor(refresh, &"focus_neighbor_right", join)
- _expect(
- is_equal_approx(
- presence.get_global_rect().position.x,
- actions.get_global_rect().position.x,
- ),
- "Friend presence is not left-justified in the action row.",
- )
- _expect(
- is_equal_approx(
- join.get_global_rect().end.x,
- actions.get_global_rect().end.x,
- ),
- "Friends Refresh and Join are not right-justified.",
- )
- var friend_controls: Array[Control] = modes.duplicate()
- friend_controls.append_array([server_list, presence, refresh, join, back])
- _assert_directionally_reachable(friends, friend_controls)
-
- list_content.hide()
- direct_content.show()
- presence.hide()
- address.show()
- address.editable = true
- server_list.hide()
- _set_button_state(refresh, false)
- _set_button_state(join, true)
- _set_button_state(save, true)
- page.set("_mode", JoinGamePage.Mode.DIRECT)
- page.call("_configure_controller_navigation")
- await process_frame
- _assert_neighbor(direct, &"focus_neighbor_bottom", address)
- _assert_neighbor(address, &"focus_neighbor_top", direct)
- _assert_neighbor(address, &"focus_neighbor_bottom", join)
- _assert_neighbor(save, &"focus_neighbor_bottom", back)
- _assert_neighbor(back, &"focus_neighbor_top", save)
- var direct_controls: Array[Control] = modes.duplicate()
- direct_controls.append_array([address, join, save, back])
- _assert_directionally_reachable(direct, direct_controls)
-
- name_edit.show()
- name_edit.editable = true
- page.set("_name_entry_active", true)
- page.call("_configure_controller_navigation")
- await process_frame
- _assert_neighbor(address, &"focus_neighbor_bottom", name_edit)
- _assert_neighbor(name_edit, &"focus_neighbor_top", address)
- page.call("request_back")
- _expect(
- not bool(page.get("_name_entry_active")),
- "Join-game Back should leave the server-name edit substate.",
- )
- _expect(
- page.visible,
- "Join-game Back should not close the browser from an edit substate.",
- )
- await process_frame
-
- var saved_entries: Array[SavedServerEntry] = []
- server_list.clear()
- for index: int in 3:
- var entry := SavedServerEntry.new()
- entry.entry_id = "controller-saved-%d" % index
- entry.display_name = "Saved server %d" % index
- saved_entries.append(entry)
- server_list.add_item(entry.display_name)
- page.set("_mode", JoinGamePage.Mode.SAVED)
- page.set("_visible_entries", saved_entries)
- direct_content.hide()
- list_content.show()
- server_list.show()
- page.call("_configure_controller_navigation")
- page.call("_restore_entry_selection_and_focus", 1)
- await process_frame
- _expect(
- server_list.is_selected(1)
- and root.gui_get_focus_owner() == server_list,
- "Saved-server refresh did not restore the same list position.",
- )
- saved_entries.remove_at(1)
- server_list.remove_item(1)
- page.set("_visible_entries", saved_entries)
- page.call("_restore_entry_selection_and_focus", 1)
- await process_frame
- _expect(
- server_list.is_selected(1)
- and (
- page.get("_selected_entry") as SavedServerEntry
- ).entry_id == "controller-saved-2",
- "Deleting a saved server did not select the row that replaced it.",
- )
- saved_entries.clear()
- server_list.clear()
- page.set("_visible_entries", saved_entries)
- page.call("_restore_entry_selection_and_focus", 0)
- await process_frame
- _expect(
- root.gui_get_focus_owner() == saved,
- "An empty saved-server list did not return focus to its mode tab.",
- )
-
- page.queue_free()
- await process_frame
-
-
-func _validate_data_settings_navigation() -> void:
- var main_source: String = FileAccess.get_file_as_string("res://main/main.gd")
- var data_root_source: String = FileAccess.get_file_as_string(
- "res://network/player_data_root.gd"
- )
- var settings_source: String = FileAccess.get_file_as_string(
- "res://ui/settings_panel.gd"
- )
- _expect(
- not main_source.contains("OS.shell_open")
- and not main_source.contains(
- "status_changed.connect(_on_data_root_status)"
- )
- and not data_root_source.contains("OS.shell_open")
- and settings_source.count("OS.shell_open") == 1,
- (
- "Opening the data folder is not owned exclusively by the Settings "
- + "Data action."
- ),
- )
- var panel := SettingsPanelScene.instantiate() as SettingsPanel
- root.add_child(panel)
- await process_frame
- panel.show()
- for _frame: int in 2:
- await process_frame
- var data_tab := panel.get_node("%DataTab") as Button
- var open_data_folder := panel.get_node("%OpenDataFolder") as Button
- var content_panel := panel.get_node("%ContentPanel") as PanelContainer
- var tab_overlap: float = (
- data_tab.get_global_rect().end.y
- - content_panel.get_global_rect().position.y
- )
- _expect(
- is_equal_approx(tab_overlap, data_tab.size.y * 0.5),
- "Settings content does not cover the lower half of its organizer tabs.",
- )
- var open_activation := {"count": 0}
- open_data_folder.pressed.connect(func() -> void:
- open_activation["count"] = int(open_activation["count"]) + 1
- )
- data_tab.grab_focus()
- var accept_press := InputEventJoypadButton.new()
- accept_press.button_index = JOY_BUTTON_A
- accept_press.pressed = true
- Input.parse_input_event(accept_press)
- await process_frame
- var accept_release := InputEventJoypadButton.new()
- accept_release.button_index = JOY_BUTTON_A
- accept_release.pressed = false
- Input.parse_input_event(accept_release)
- for _frame: int in 2:
- await process_frame
- _expect(
- panel.get_active_page_id() == &"data",
- "Selecting the Data tab did not open the Data page.",
- )
- _expect(
- int(open_activation["count"]) == 0,
- "Selecting the Data tab also activated Open Data Folder.",
- )
- _expect(
- root.gui_get_focus_owner() == data_tab,
- "Selecting the Data tab transferred focus into its actions.",
- )
- open_data_folder.grab_focus()
- _expect(
- root.gui_get_focus_owner() == open_data_folder,
- "Open Data Folder did not accept controller focus.",
- )
- _expect(
- not open_data_folder.disabled,
- "Open Data Folder is unexpectedly disabled.",
- )
- var action_press := InputEventJoypadButton.new()
- action_press.button_index = JOY_BUTTON_A
- action_press.pressed = true
- Input.parse_input_event(action_press)
- await process_frame
- var action_release := InputEventJoypadButton.new()
- action_release.button_index = JOY_BUTTON_A
- action_release.pressed = false
- Input.parse_input_event(action_release)
- for _frame: int in 2:
- await process_frame
- _expect(
- int(open_activation["count"]) == 1,
- (
- "Explicitly accepting Open Data Folder pressed it %d times."
- % int(open_activation["count"])
- ),
- )
- _expect(
- panel.find_children(
- "*", "ConfirmationDialog", true, false
- ).is_empty(),
- "Open Data Folder left a controller-activatable dialog behind.",
- )
- var change_data_folder := panel.get_node("%ChangeDataFolder") as Button
- var copy_fingerprint := panel.get_node("%CopyPlayerFingerprint") as Button
- var export_player := panel.get_node("%ExportPlayerIdentity") as Button
- var import_player := panel.get_node("%ImportPlayerIdentity") as Button
- var export_host := panel.get_node("%ExportHostIdentity") as Button
- var import_host := panel.get_node("%ImportHostIdentity") as Button
- var controls: Array[Control] = [
- data_tab,
- open_data_folder,
- change_data_folder,
- copy_fingerprint,
- export_player,
- import_player,
- export_host,
- import_host,
- panel.get_node("%ApplySettingsButton") as Control,
- panel.get_node("%SettingsBackButton") as Control,
- ]
- for control: Control in controls:
- _expect(
- control.focus_mode == Control.FOCUS_ALL,
- "Data & Identity control %s is not controller-focusable."
- % control.name,
- )
- _assert_neighbor(data_tab, &"focus_neighbor_bottom", open_data_folder)
- _assert_neighbor(
- open_data_folder,
- &"focus_neighbor_right",
- change_data_folder,
- )
- _assert_neighbor(
- open_data_folder,
- &"focus_neighbor_bottom",
- copy_fingerprint,
- )
- _assert_neighbor(
- change_data_folder,
- &"focus_neighbor_left",
- open_data_folder,
- )
- _assert_neighbor(
- change_data_folder,
- &"focus_neighbor_bottom",
- copy_fingerprint,
- )
- _assert_neighbor(
- copy_fingerprint,
- &"focus_neighbor_top",
- open_data_folder,
- )
- _assert_neighbor(
- copy_fingerprint,
- &"focus_neighbor_bottom",
- export_player,
- )
- _assert_neighbor(export_player, &"focus_neighbor_right", import_player)
- _assert_neighbor(export_player, &"focus_neighbor_bottom", export_host)
- _assert_neighbor(import_player, &"focus_neighbor_left", export_player)
- _assert_neighbor(import_player, &"focus_neighbor_bottom", import_host)
- _assert_neighbor(export_host, &"focus_neighbor_top", export_player)
- _assert_neighbor(export_host, &"focus_neighbor_right", import_host)
- _assert_neighbor(import_host, &"focus_neighbor_top", import_player)
- _assert_neighbor(import_host, &"focus_neighbor_left", export_host)
- _assert_directionally_reachable(controls.front(), controls)
- await process_frame
- open_data_folder.grab_focus()
- panel.call("_select_page", &"sound", true)
- _expect(
- panel.get_active_page_id() == &"sound",
- "Settings did not leave the Data page.",
- )
- _expect(
- root.gui_get_focus_owner() == panel.get_node("%SoundTab"),
- "Leaving Data retained focus on a hidden Data action.",
- )
- var sound_down: Control = (
- (panel.get_node("%SoundTab") as Control).find_valid_focus_neighbor(
- SIDE_BOTTOM
- )
- )
- _expect(
- sound_down != null and not panel.get_node("%DataPage").is_ancestor_of(
- sound_down
- ),
- "Sound navigation still pointed into the hidden Data page.",
- )
- var hidden_data_activation_count: int = int(open_activation["count"])
- Input.parse_input_event(action_press)
- await process_frame
- Input.parse_input_event(action_release)
- for _frame: int in 2:
- await process_frame
- _expect(
- int(open_activation["count"]) == hidden_data_activation_count,
- "Controller A activated Open Data Folder from another settings page.",
- )
- open_data_folder.pressed.emit()
- await process_frame
- _expect(
- panel.find_children(
- "*", "ConfirmationDialog", true, false
- ).is_empty(),
- "A hidden Data action created a folder dialog.",
- )
- panel.call("_select_page", &"data", true)
- await process_frame
- copy_fingerprint.grab_focus()
- var feedback := panel.get_node("%SettingsFeedback") as Label
- feedback.text = "activation boundary intact"
- open_data_folder.pressed.emit()
- await process_frame
- _expect(
- feedback.text == "activation boundary intact",
- (
- "Open Data Folder crossed its activation boundary without owning "
- + "controller focus."
- ),
- )
- panel.hide()
- await process_frame
- var hidden_focus_owner: Control = root.gui_get_focus_owner()
- _expect(
- hidden_focus_owner == null
- or not panel.is_ancestor_of(hidden_focus_owner),
- "Hiding Settings retained focus on one of its controls.",
- )
- _expect(
- panel.find_children("*", "BubbleButton", true, false).is_empty(),
- "Settings children still contain bubble controls.",
- )
- panel.queue_free()
- await process_frame
-
-
-func _validate_settings_adjustment_navigation() -> void:
- var panel := SettingsPanelScene.instantiate() as SettingsPanel
- root.add_child(panel)
- await process_frame
- panel.show()
- panel.call("_select_page", &"sound", false)
- for _frame: int in 2:
- await process_frame
- var sound_controls: Array[Control] = [
- panel.get_node("%SoundTab") as Control,
- panel.get_node("%MasterVolumeSlider") as Control,
- panel.get_node("%MusicVolumeSlider") as Control,
- panel.get_node("%EffectsVolumeSlider") as Control,
- panel.get_node("%EnvironmentVolumeSlider") as Control,
- panel.get_node("%ApplySettingsButton") as Control,
- panel.get_node("%SettingsBackButton") as Control,
- ]
- _assert_directionally_reachable(sound_controls.front(), sound_controls)
- panel.call("_select_page", &"controls", false)
- for _frame: int in 2:
- await process_frame
- var mouse_slider := panel.get_node("%MouseSensitivitySlider") as HSlider
- var controller_slider := panel.get_node(
- "%ControllerSensitivitySlider"
- ) as HSlider
- var on_screen_keyboard := panel.get_node(
- "%OnScreenKeyboardToggle"
- ) as Button
- var swap_scroll := panel.get_node("%SwapScrollToggle") as Button
- var invert_y := panel.get_node("%InvertYToggle") as Button
- var controller_binds := panel.get_node("%ControllerMapping") as Button
- var keyboard_binds := panel.get_node("%KeyboardMapping") as Button
- _expect(
- mouse_slider.focus_mode == Control.FOCUS_ALL
- and controller_slider.focus_mode == Control.FOCUS_ALL,
- "Standard sensitivity sliders are not controller-focusable.",
- )
- _expect(
- is_equal_approx(mouse_slider.step, 0.0005)
- and is_equal_approx(controller_slider.step, 0.1),
- "Sensitivity sliders do not retain their authored increments.",
- )
- _assert_neighbor(
- invert_y,
- &"focus_neighbor_bottom",
- swap_scroll,
- )
- _assert_neighbor(
- swap_scroll,
- &"focus_neighbor_top",
- invert_y,
- )
- _assert_neighbor(
- swap_scroll,
- &"focus_neighbor_bottom",
- on_screen_keyboard,
- )
- _assert_neighbor(
- on_screen_keyboard,
- &"focus_neighbor_top",
- swap_scroll,
- )
- _assert_neighbor(
- on_screen_keyboard,
- &"focus_neighbor_bottom",
- controller_binds,
- )
- _assert_neighbor(
- controller_binds,
- &"focus_neighbor_top",
- on_screen_keyboard,
- )
- _assert_neighbor(
- controller_binds,
- &"focus_neighbor_right",
- keyboard_binds,
- )
- _assert_neighbor(
- keyboard_binds,
- &"focus_neighbor_top",
- on_screen_keyboard,
- )
- _assert_neighbor(
- keyboard_binds,
- &"focus_neighbor_left",
- controller_binds,
- )
- var control_inputs: Array[Control] = [
- panel.get_node("%ControlsTab") as Control,
- mouse_slider,
- controller_slider,
- invert_y,
- swap_scroll,
- on_screen_keyboard,
- controller_binds,
- keyboard_binds,
- panel.get_node("%ApplySettingsButton") as Control,
- panel.get_node("%SettingsBackButton") as Control,
- ]
- _assert_directionally_reachable(control_inputs.front(), control_inputs)
- panel.queue_free()
- await process_frame
-
-
-func _validate_settings_presentation_requires_apply() -> void:
- var panel := SettingsPanelScene.instantiate() as SettingsPanel
- var settings_manager := PlayerSettingsManager.new()
- root.add_child(settings_manager)
- root.add_child(panel)
- await process_frame
- panel.open_panel(settings_manager)
- await process_frame
- var chat_dock := panel.get_node("%ChatDockSelector") as OptionButton
- var chat_mode := panel.get_node("%ChatModeSelector") as OptionButton
- var paint_dock := panel.get_node("%PaintDockSelector") as OptionButton
- chat_dock.select(1)
- chat_dock.item_selected.emit(1)
- chat_mode.select(1)
- chat_mode.item_selected.emit(1)
- paint_dock.select(0)
- paint_dock.item_selected.emit(0)
- _expect(
- not settings_manager.current_settings.chat_dock_right
- and not settings_manager.current_settings.chat_mobile_mode
- and settings_manager.current_settings.paint_dock_right
- and not settings_manager.current_settings.presentation_layout_customized,
- (
- "Navigating presentation choices changed saved docking settings "
- + "before Apply."
- ),
- )
- panel.call("_apply_settings")
- _expect(
- settings_manager.current_settings.chat_dock_right
- and settings_manager.current_settings.chat_mobile_mode
- and not settings_manager.current_settings.paint_dock_right
- and settings_manager.current_settings.presentation_layout_customized,
- "Applying deliberate presentation choices did not retain them.",
- )
- panel.close_panel(true)
- panel.queue_free()
- settings_manager.queue_free()
- await process_frame
-
-
-func _validate_mail_navigation() -> void:
- var page := MailPageType.new() as Control
- root.add_child(page)
- await process_frame
- page.call("activate")
- page.call("set_interactive", true)
- var inbox_list := page.get("_inbox_list") as VBoxContainer
- var entry := Button.new()
- entry.text = "test letter"
- entry.custom_minimum_size = Vector2(1008.0, 54.0)
- inbox_list.add_child(entry)
- await process_frame
- page.call("_refresh_controller_navigation")
- var archive_view := page.get("_archive_view_button") as Button
- var send_mail := page.get("_send_mail_button") as Button
- _assert_neighbor(
- archive_view, &"focus_neighbor_right", send_mail
- )
- _assert_neighbor(archive_view, &"focus_neighbor_bottom", entry)
- _assert_neighbor(send_mail, &"focus_neighbor_bottom", entry)
- _assert_neighbor(entry, &"focus_neighbor_top", archive_view)
- _assert_directionally_reachable(
- archive_view, [archive_view, send_mail, entry]
- )
-
- var inbox := page.get("_inbox") as Control
- var compose := page.get("_compose") as Control
- var letter := page.get("_letter") as Control
- inbox.hide()
- compose.hide()
- letter.show()
- var accept := page.get("_accept") as Button
- var decline := page.get("_decline") as Button
- var close := page.get("_letter_close") as Button
- var archive := page.get("_archive") as Button
- var delete := page.get("_delete") as Button
- accept.show()
- decline.show()
- delete.disabled = false
- page.call("_refresh_controller_navigation")
- _assert_neighbor(accept, &"focus_neighbor_bottom", decline)
- _assert_neighbor(decline, &"focus_neighbor_bottom", delete)
- _assert_neighbor(delete, &"focus_neighbor_left", archive)
- _assert_neighbor(archive, &"focus_neighbor_left", close)
- _assert_neighbor(close, &"focus_neighbor_top", close)
- _assert_directionally_reachable(
- accept, [accept, decline, close, archive, delete]
- )
- page.queue_free()
- await process_frame
-
-
-func _validate_profile_confirmation_focus() -> void:
- var page := ProfilePageType.new() as Control
- root.add_child(page)
- await process_frame
- page.call("activate")
- page.call("set_interactive", true)
- page.call("reset_controller_zone")
- var name_edit := page.get("_name_edit") as LineEdit
- name_edit.grab_focus()
- page.call("_on_name_changed", "V")
- _expect(
- root.gui_get_focus_owner() == name_edit,
- "Typing a profile name released the active name field.",
- )
- var suggestions := page.get("_suggestions") as HBoxContainer
- var suggestion := Button.new()
- suggestion.text = "alternate name"
- suggestions.add_child(suggestion)
- page.call("_apply_controller_zone_focus")
- var account_controls: Array[Control] = []
- for item: Variant in page.call("_account_controller_controls"):
- account_controls.append(item as Control)
- var preview := page.get("_preview") as Control
- var reset_view := page.get("_reset_view_button") as Button
- var reset_view_hint := page.get("_reset_view_controller_hint") as Label
- _expect(
- suggestion in account_controls,
- "Profile name-conflict choices are outside the account zone.",
- )
- _expect(
- suggestion.focus_mode == Control.FOCUS_ALL,
- "Profile name-conflict choices are not controller-focusable.",
- )
- _expect(
- preview.focus_mode == Control.FOCUS_NONE,
- "The profile preview must not take controller focus.",
- )
- _expect(
- reset_view.focus_mode == Control.FOCUS_NONE,
- "The profile reset-view button must not take controller focus.",
- )
- _expect(
- reset_view_hint != null
- and reset_view_hint.text == "Reset View: RS"
- and reset_view_hint.focus_mode == Control.FOCUS_NONE,
- "The profile preview does not present its right-stick reset hint.",
- )
- preview.set("_camera_yaw", 1.0)
- preview.set("_camera_pitch", 0.4)
- preview.set("_camera_distance", 2.5)
- var reset_view_event := InputEventJoypadButton.new()
- reset_view_event.button_index = JOY_BUTTON_RIGHT_STICK
- reset_view_event.pressed = true
- _expect(
- bool(page.call("handle_controller_input", reset_view_event)),
- "Profile customization did not consume right-stick reset.",
- )
- _expect(
- is_equal_approx(float(preview.get("_camera_yaw")), 0.0)
- and is_equal_approx(
- float(preview.get("_camera_pitch")),
- ProfilePreview.DEFAULT_CAMERA_PITCH,
- )
- and is_equal_approx(
- float(preview.get("_camera_distance")),
- ProfilePreview.DEFAULT_CAMERA_DISTANCE,
- ),
- "Right-stick click did not restore the default profile preview view.",
- )
- page.call("_show_confirmation", "defaults")
- var confirmation := page.get("_discard_confirmation") as Control
- var confirm := page.get("_confirmation_confirm") as Button
- var keep_editing := page.get("_keep_editing_button") as Button
- _expect(confirmation.visible, "The profile confirmation did not open.")
- _expect(
- confirm.focus_mode == Control.FOCUS_ALL,
- "The profile confirmation action is not controller-focusable.",
- )
- _expect(
- keep_editing.focus_mode == Control.FOCUS_ALL,
- "The profile confirmation cancel action is not controller-focusable.",
- )
- for control: Control in account_controls:
- _expect(
- control.focus_mode == Control.FOCUS_NONE,
- "Profile confirmation leaked focus to %s." % control.name,
- )
- var cancel_event := InputEventAction.new()
- cancel_event.action = &"ui_cancel"
- cancel_event.pressed = true
- _expect(
- bool(page.call("handle_controller_input", cancel_event)),
- "Profile confirmation did not consume controller Back.",
- )
- _expect(
- not confirmation.visible,
- "Controller Back did not close the profile confirmation.",
- )
- for control: Control in account_controls:
- _expect(
- control.focus_mode == Control.FOCUS_ALL,
- "Profile account focus was not restored after confirmation.",
- )
- page.call("_enter_controller_customization")
- for _frame: int in 2:
- await process_frame
- var categories: Array[Control] = []
- var category_list := page.get("_category_list") as VBoxContainer
- for item: Variant in page.call("_controls_under", category_list):
- categories.append(item as Control)
- var initial_category_focus := root.gui_get_focus_owner() as Control
- _expect(
- initial_category_focus in categories,
- "Customize did not focus the appearance feature list.",
- )
- var down := InputEventAction.new()
- down.action = &"ui_down"
- down.pressed = true
- Input.parse_input_event(down)
- for _frame: int in 2:
- await process_frame
- var next_category_focus := root.gui_get_focus_owner() as Control
- _expect(
- next_category_focus in categories,
- "Moving down dropped focus from the appearance feature list.",
- )
- _expect(
- next_category_focus != initial_category_focus,
- "Moving down did not advance through appearance features.",
- )
- down.pressed = false
- Input.parse_input_event(down)
- var accept_event := InputEventAction.new()
- accept_event.action = &"ui_accept"
- accept_event.pressed = true
- _expect(
- bool(page.call("handle_controller_input", accept_event)),
- "Appearance feature selection did not consume controller Accept.",
- )
- for _frame: int in 3:
- await process_frame
- var option_groups: Array = page.call("_controller_option_groups")
- var option_controls: Array[Control] = []
- if not option_groups.is_empty():
- for item: Variant in option_groups.front():
- var option_control := item as Control
- if option_control != null:
- option_controls.append(option_control)
- _expect(
- root.gui_get_focus_owner() in option_controls,
- "Selecting an appearance feature did not focus its options.",
- )
- var rebuilt_option := root.gui_get_focus_owner() as BaseButton
- if rebuilt_option != null:
- rebuilt_option.pressed.emit()
- for _frame: int in 3:
- await process_frame
- option_groups = page.call("_controller_option_groups")
- option_controls.clear()
- if not option_groups.is_empty():
- for item: Variant in option_groups.front():
- var option_control := item as Control
- if option_control != null:
- option_controls.append(option_control)
- _expect(
- root.gui_get_focus_owner() in option_controls,
- "Rebuilding appearance options dropped controller focus.",
- )
- root.gui_release_focus()
- var controller_down := InputEventJoypadButton.new()
- controller_down.button_index = JOY_BUTTON_DPAD_DOWN
- controller_down.pressed = true
- page.call("handle_controller_input", controller_down)
- _expect(
- root.gui_get_focus_owner() in option_controls,
- "Appearance options did not recover a lost controller focus owner.",
- )
- var cancel_options := InputEventAction.new()
- cancel_options.action = &"ui_cancel"
- cancel_options.pressed = true
- _expect(
- bool(page.call("handle_controller_input", cancel_options)),
- "Appearance options did not consume controller Back.",
- )
- for _frame: int in 2:
- await process_frame
- _expect(
- root.gui_get_focus_owner() in categories,
- "Controller Back did not return to the appearance feature list.",
- )
- page.queue_free()
- await process_frame
-
-
-func _validate_profile_voice_navigation() -> void:
- var page := ProfilePageType.new() as Control
- page.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
- root.add_child(page)
- await process_frame
- page.call("activate")
- page.call("set_interactive", true)
- page.call("_select_category", "voice")
- await process_frame
- page.set(
- "_controller_zone",
- ProfilePageType.ControllerZone.OPTIONS,
- )
- page.set("_controller_option_depth", 0)
- page.call("_apply_controller_zone_focus")
- page.call("_focus_controller_zone")
- await process_frame
- var sample_grid := page.find_child(
- "VoiceSampleSetGrid", true, false
- ) as GridContainer
- var pitch_grid := page.find_child(
- "VoicePitchGrid", true, false
- ) as GridContainer
- var speed_grid := page.find_child(
- "VoiceSpeedGrid", true, false
- ) as GridContainer
- var call_grid := page.find_child(
- "VoiceCallGrid", true, false
- ) as GridContainer
- _expect(
- is_instance_valid(sample_grid)
- and is_instance_valid(pitch_grid)
- and is_instance_valid(speed_grid)
- and is_instance_valid(call_grid),
- "The voice page is missing a settings section.",
- )
- if (
- not is_instance_valid(sample_grid)
- or not is_instance_valid(pitch_grid)
- or not is_instance_valid(speed_grid)
- or not is_instance_valid(call_grid)
- ):
- page.queue_free()
- await process_frame
- return
- var sample_buttons: Array[Control] = []
- for item: Variant in page.call("_controls_under", sample_grid):
- sample_buttons.append(item as Control)
- _expect(
- sample_buttons.size() == 3,
- "The voice-set row must expose kat, robot, and kim.",
- )
- if sample_buttons.size() != 3:
- page.queue_free()
- await process_frame
- return
- var kat_button := sample_grid.get_node("VoiceSet_kat") as Button
- var robot_button := sample_grid.get_node("VoiceSet_robot") as Button
- var kim_button := sample_grid.get_node("VoiceSet_kim") as Button
- _expect(kat_button.text == "kat", "The default voice set is not labeled kat.")
- _expect(robot_button.text == "robot", "The tone voice set is not labeled robot.")
- _expect(kim_button.text == "kim", "The kim voice set is not labeled kim.")
- _expect(kat_button.button_pressed, "The kat voice set is not selected by default.")
- var option_controls: Array[Control] = []
- var option_groups: Array = page.call("_controller_option_groups")
- for item: Variant in option_groups[0]:
- option_controls.append(item as Control)
- _expect(
- option_controls.size()
- == (
- VoiceProfilesType.SAMPLE_SET_OPTIONS.size()
- + VoiceProfilesType.OPTIONS.size()
- + VoiceProfilesType.SPEED_OPTIONS.size()
- + VoiceProfilesType.CALL_OPTIONS.size()
- ),
- "The controller voice zone does not contain every voice setting.",
- )
- _assert_directionally_reachable(kat_button, option_controls)
- var kat_down := kat_button.get_node_or_null(
- kat_button.focus_neighbor_bottom
- ) as Control
- _expect(
- kat_down != null and pitch_grid.is_ancestor_of(kat_down),
- "Down from the voice-set row does not enter the pitch row.",
- )
- if OS.has_environment("straywild_PROFILE_VOICE_CAPTURE"):
- await RenderingServer.frame_post_draw
- var capture := root.get_viewport().get_texture().get_image()
- _expect(
- capture.save_png(OS.get_environment(
- "straywild_PROFILE_VOICE_CAPTURE"
- )) == OK,
- "The profile voice-page capture could not be saved.",
- )
- robot_button.grab_focus()
- robot_button.button_pressed = true
- robot_button.pressed.emit()
- await process_frame
- _expect(
- root.gui_get_focus_owner() == robot_button,
- "Selecting robot moved controller focus out of the voice-set row.",
- )
- _expect(
- str(page.get("_draft_sample_set_id")) == "robot",
- "Selecting robot did not update the profile draft.",
- )
- kim_button.grab_focus()
- kim_button.button_pressed = true
- kim_button.pressed.emit()
- await process_frame
- _expect(
- root.gui_get_focus_owner() == kim_button,
- "Selecting kim moved controller focus out of the voice-set row.",
- )
- _expect(
- str(page.get("_draft_sample_set_id")) == "kim",
- "Selecting kim did not update the profile draft.",
- )
- var category_controls: Array[Control] = []
- for item: Variant in page.call(
- "_controls_under", page.get("_category_list")
- ):
- category_controls.append(item as Control)
- for category_control: Control in category_controls:
- _expect(
- category_control.focus_mode == Control.FOCUS_NONE,
- "The voice options zone leaked focus into profile categories.",
- )
- page.queue_free()
- await process_frame
-
-
-func _validate_player_menu_nested_controller_back() -> void:
- var menu := PlayerMenuScene.instantiate() as Control
- root.add_child(menu)
- await process_frame
- menu.visible = true
- menu.set("_current_section", PlayerMenuType.Section.PROFILE)
- var profile_page := menu.get("_profile_page") as Control
- profile_page.call("activate")
- profile_page.call("set_interactive", true)
- profile_page.call("_enter_controller_customization")
- for _frame: int in 2:
- await process_frame
- var categories: Array[Control] = []
- var category_list := profile_page.get(
- "_category_list"
- ) as VBoxContainer
- for item: Variant in profile_page.call(
- "_controls_under", category_list
- ):
- categories.append(item as Control)
- var initial_focus := root.gui_get_focus_owner() as Control
- menu.call("_reserve_visible_secondary_navigation")
- await process_frame
- _expect(
- root.gui_get_focus_owner() in categories,
- "Player-menu focus reservation disabled appearance features.",
- )
- var down := InputEventAction.new()
- down.action = &"ui_down"
- down.pressed = true
- Input.parse_input_event(down)
- for _frame: int in 2:
- await process_frame
- _expect(
- root.gui_get_focus_owner() in categories,
- "Player-menu Down dropped appearance feature focus.",
- )
- _expect(
- root.gui_get_focus_owner() != initial_focus,
- "Player-menu Down did not advance appearance feature focus.",
- )
- down.pressed = false
- Input.parse_input_event(down)
- _expect(
- bool(menu.call("consume_escape")),
- "Global player-menu Back did not consume the Profile category zone.",
- )
- _expect(
- menu.visible,
- "Global player-menu Back closed Profile from a nested zone.",
- )
- _expect(
- int(profile_page.get("_controller_zone"))
- == ProfilePageType.ControllerZone.ACCOUNT,
- "Global player-menu Back did not return Profile to its account zone.",
- )
-
- var logbook_page := menu.get("_catalog_logbook") as Control
- logbook_page.set("_active", true)
- logbook_page.set("_interactive", true)
- logbook_page.set(
- "_controller_zone",
- LogbookPageType.ControllerZone.DETAILS,
- )
- menu.set("_current_section", PlayerMenuType.Section.LOGBOOK)
- _expect(
- bool(menu.call("consume_escape")),
- "Global player-menu Back did not consume the Logbook detail zone.",
- )
- _expect(
- menu.visible,
- "Global player-menu Back closed Logbook from a nested zone.",
- )
- _expect(
- int(logbook_page.get("_controller_zone"))
- == LogbookPageType.ControllerZone.ENTRIES,
- "Global player-menu Back did not return Logbook to creature selection.",
- )
- logbook_page.call("_set_index_open", true, false)
- logbook_page.set(
- "_controller_zone",
- LogbookPageType.ControllerZone.INDEX,
- )
- _expect(
- bool(menu.call("consume_escape")),
- "Global player-menu Back did not consume the Logbook index zone.",
- )
- _expect(
- menu.visible and not bool(logbook_page.get("_index_open")),
- "Global player-menu Back did not return from the Logbook index page.",
- )
- _expect(
- int(logbook_page.get("_controller_zone"))
- == LogbookPageType.ControllerZone.TABS,
- "Global player-menu Back did not return Logbook index focus to tabs.",
- )
-
- var net_page := menu.get("_the_net_page") as Control
- net_page.set("_active", true)
- net_page.set("_interactive", true)
- net_page.set(
- "_controller_zone",
- TheNetPageType.ControllerZone.CLAIMS,
- )
- menu.set("_current_section", PlayerMenuType.Section.NET)
- _expect(
- bool(menu.call("consume_escape")),
- "Global player-menu Back did not consume the Fishnet claims zone.",
- )
- _expect(
- menu.visible,
- "Global player-menu Back closed Fishnet from a nested zone.",
- )
- _expect(
- int(net_page.get("_controller_zone"))
- == TheNetPageType.ControllerZone.TABS,
- "Global player-menu Back did not return Fishnet to its tabs.",
- )
-
- var mail_page := menu.get("_mail_page") as Control
- mail_page.set("_active", true)
- mail_page.set("_interactive", true)
- (mail_page.get("_inbox") as Control).hide()
- (mail_page.get("_compose") as Control).show()
- (mail_page.get("_letter") as Control).hide()
- menu.set("_current_section", PlayerMenuType.Section.MAIL)
- _expect(
- bool(menu.call("consume_escape")),
- "Global player-menu Back did not consume the Mail compose zone.",
- )
- _expect(
- menu.visible,
- "Global player-menu Back closed Mail while composing.",
- )
- _expect(
- (mail_page.get("_inbox") as Control).visible,
- "Global player-menu Back did not return Mail to the inbox.",
- )
-
- var mapping_manager := ControllerMappingManagerType.new()
- var bindings: Dictionary = ControllerMappingManagerType.default_bindings()
- bindings[str(ControllerMappingManagerType.ROLE_B)] = {
- "kind": "button",
- "button": int(JOY_BUTTON_Y),
- }
- mapping_manager.set("_profiles", {
- "default": {
- "controller_name": "controller",
- "bindings": bindings,
- },
- })
- menu.call("setup_controller_mapping", mapping_manager)
- var players_page := menu.get("_players_page") as Control
- players_page.set("_active", true)
- players_page.set("_interactive", true)
- players_page.set(
- "_controller_zone", PlayersPageType.ControllerZone.BODY
- )
- menu.set("_current_section", PlayerMenuType.Section.PLAYERS)
- _expect(
- bool(menu.call("consume_escape")),
- "Global player-menu Back did not consume the Online body zone.",
- )
- _expect(
- menu.visible,
- "Global player-menu Back closed Online from its body zone.",
- )
- _expect(
- int(players_page.get("_controller_zone"))
- == PlayersPageType.ControllerZone.TABS,
- "Global player-menu Back did not return Online to its tabs.",
- )
- players_page.set(
- "_controller_zone", PlayersPageType.ControllerZone.BODY
- )
- var mapped_back := InputEventJoypadButton.new()
- mapped_back.device = 0
- mapped_back.button_index = JOY_BUTTON_Y
- mapped_back.pressed = true
- _expect(
- bool(menu.call(
- "_handle_controller_ownership_input", mapped_back
- )),
- "Player menu did not consume a mapped controller Back press.",
- )
- _expect(
- int(players_page.get("_controller_zone"))
- == PlayersPageType.ControllerZone.TABS,
- "Mapped controller Back skipped the previous player-page zone.",
- )
- menu.queue_free()
- await process_frame
- mapping_manager.free()
-
-
-func _validate_inventory_tab_zone_transitions() -> void:
- var menu := PlayerMenuScene.instantiate() as Control
- root.add_child(menu)
- await process_frame
- var inventory_state := Node.new()
- root.add_child(inventory_state)
- var bag := PlayerBag.new()
- var catches := FishInventory.new()
- var storage_capacity := PlayerCoolerCapacity.new()
- var layout := PlayerInventoryLayout.new()
- var hotbar := PlayerHotbarType.new()
- for node: Node in [bag, catches, storage_capacity, layout, hotbar]:
- inventory_state.add_child(node)
- bag.setup(ItemCatalogResource)
- layout.setup(bag, catches, ItemCatalogResource, storage_capacity)
- assert(layout.restore_backpack_level(1))
- bag.set_inventory_layout(layout)
- catches.set_inventory_layout(layout)
- hotbar.setup(bag, ItemCatalogResource, catches, layout)
- assert(bag.add_item(&"basic_fishing_rod", 1))
- assert(bag.add_item(&"coffee", 1))
- assert(bag.add_item(&"worms", 1))
- assert(layout.move_entry(
- PlayerInventoryLayout.EntryKind.ITEM,
- &"basic_fishing_rod",
- PlayerInventoryLayout.InventoryContainer.INVENTORY,
- 9,
- ))
- assert(hotbar.assign_item(0, &"coffee"))
- menu.set("_bag", bag)
- menu.set("_inventory", catches)
- menu.set("_hotbar", hotbar)
- menu.set("_inventory_layout", layout)
- menu.set("_item_catalog", ItemCatalogResource)
- var inventory_grid := menu.get("_general_inventory_grid") as GeneralInventoryGrid
- inventory_grid.setup(
- layout,
- bag,
- catches,
- hotbar,
- ItemCatalogResource,
- PlayerInventoryLayout.InventoryContainer.INVENTORY,
- )
- menu.visible = true
- menu.call(
- "_show_section_immediate",
- PlayerMenuType.Section.BAG,
- )
- menu.call("_set_content_interactive", true)
- menu.call("_enter_inventory_tabs_zone")
- for _frame: int in 2:
- await process_frame
- var inventory_tab := menu.get_node("%BagSubTab") as Button
- var tackle_tab := menu.get_node("%TackleSubTab") as Button
- _expect(
- root.gui_get_focus_owner() == inventory_tab,
- "Inventory tab zone did not begin on the unified Inventory tab.",
- )
- _expect(inventory_tab.text == "Inventory", "Unified tab is not labelled Inventory.")
- _expect(
- not (menu.get_node("%CoolerSubTab") as Button).visible
- and not (menu.get_node("%ItemsSubTab") as Button).visible,
- "Legacy Cooler or Items tabs remain visible.",
- )
-
- var right := InputEventAction.new()
- right.action = &"ui_right"
- right.pressed = true
- _expect(
- bool(menu.call("_handle_controller_ownership_input", right)),
- "Inventory tab zone did not consume controller Right.",
- )
- await _wait_for_player_menu_page_transition(menu)
- _expect(
- menu.get("_current_section") == PlayerMenuType.Section.TACKLE_BOX,
- "Controller Right did not switch from Inventory to Tackle.",
- )
- _expect(
- menu.get("_controller_ownership")
- == PlayerMenuType.ControllerOwnership.INVENTORY_TABS,
- "Changing Inventory tabs entered the item-content zone without A.",
- )
- _expect(
- root.gui_get_focus_owner() == tackle_tab,
- "Changing Inventory tabs did not keep focus on the selected tab.",
- )
- var down := InputEventAction.new()
- down.action = &"ui_down"
- down.pressed = true
- _expect(
- bool(menu.call("_handle_controller_ownership_input", down)),
- "Inventory tab zone did not consume controller Down.",
- )
- await process_frame
- _expect(
- root.gui_get_focus_owner() == tackle_tab,
- "Controller Down escaped the Inventory tab zone before A.",
- )
- var left := InputEventAction.new()
- left.action = &"ui_left"
- left.pressed = true
- _expect(
- bool(menu.call("_handle_controller_ownership_input", left)),
- "Tackle-to-Inventory navigation did not consume controller Left.",
- )
- _expect(
- menu.get("_current_section") == PlayerMenuType.Section.BAG
- or bool(menu.get("_page_transitioning")),
- "Tackle-to-Inventory input did not start the Inventory transition.",
- )
- await _wait_for_player_menu_page_transition(menu)
- _expect(
- root.gui_get_focus_owner() == inventory_tab,
- "Returning to Inventory did not retain tab focus before A.",
- )
- menu.call("_set_content_interactive", true)
- menu.call("_configure_bag_item_focus")
- var active_slots := inventory_grid.get_slots()
- for slot: GeneralInventorySlot in active_slots:
- _expect(
- slot.focus_mode == Control.FOCUS_NONE,
- "Inventory slots remained focusable while tabs owned the controller.",
- )
-
- var accept := InputEventJoypadButton.new()
- accept.button_index = JOY_BUTTON_A
- accept.pressed = true
- _expect(
- bool(menu.call("_handle_controller_ownership_input", accept)),
- "Inventory tab zone did not consume controller A.",
- )
- _expect(
- menu.get("_controller_ownership")
- == PlayerMenuType.ControllerOwnership.ITEM_LIST,
- "Controller A did not explicitly enter Inventory contents.",
- )
- for _frame: int in 2:
- await process_frame
- _expect(
- menu.get("_current_section") == PlayerMenuType.Section.BAG,
- "Entering Inventory contents changed section to %s."
- % menu.get("_current_section"),
- )
- var first_item := active_slots[0] as Control
- var second_item := active_slots[1] as Control
- for slot: GeneralInventorySlot in active_slots:
- _expect(
- slot.focus_mode == Control.FOCUS_ALL,
- "Accepting Inventory did not enable its content focus zone.",
- )
- _expect(
- root.gui_get_focus_owner() == first_item,
- "Entering Inventory did not focus its first slot.",
- )
- _expect(
- first_item.focus_neighbor_right == first_item.get_path_to(second_item),
- "Inventory slots do not provide horizontal controller navigation.",
- )
- Input.parse_input_event(right)
- for _frame: int in 2:
- await process_frame
- _expect(
- menu.get("_current_section") == PlayerMenuType.Section.BAG,
- "Inventory slot navigation changed section to %s."
- % menu.get("_current_section"),
- )
- _expect(
- root.gui_get_focus_owner() == second_item,
- "Controller Right did not move between Inventory slots.",
- )
- var right_release := InputEventAction.new()
- right_release.action = &"ui_right"
- right_release.pressed = false
- Input.parse_input_event(right_release)
- _expect(
- bool(menu.call("consume_escape")),
- "Global player-menu Back did not consume Inventory contents.",
- )
- _expect(
- menu.visible,
- "Global player-menu Back closed Inventory from its content zone.",
- )
- _expect(
- menu.get("_controller_ownership")
- == PlayerMenuType.ControllerOwnership.INVENTORY_TABS,
- "Global player-menu Back did not return Inventory contents to tabs.",
- )
- for _frame: int in 2:
- await process_frame
- _expect(
- menu.get("_current_section") == PlayerMenuType.Section.BAG,
- "Returning from Inventory contents changed section to %s."
- % menu.get("_current_section"),
- )
- _expect(
- bool(menu.call("_handle_controller_ownership_input", accept)),
- "Inventory tab zone did not re-enter Inventory contents.",
- )
- for _frame: int in 2:
- await process_frame
- var notepad_source := active_slots[9]
- notepad_source.grab_focus()
- var context_press := InputEventJoypadButton.new()
- context_press.button_index = JOY_BUTTON_Y
- context_press.pressed = true
- _expect(
- bool(menu.call(
- "_handle_controller_ownership_input", context_press
- )),
- "Controller Y did not open the selected Inventory notepad.",
- )
- _expect(
- menu.get("_controller_ownership")
- == PlayerMenuType.ControllerOwnership.NOTEPAD_ACTIONS,
- "Inventory notepad did not become the active controller zone.",
- )
- _expect(
- (menu.get_node("%BagDetailConstellation") as Control).visible,
- "Inventory notepad remained hidden after controller Y.",
- )
- _expect(
- bool(menu.call("consume_escape")),
- "Controller B did not close the Inventory notepad.",
- )
- _expect(
- not (menu.get_node("%BagDetailConstellation") as Control).visible
- and menu.get("_controller_ownership")
- == PlayerMenuType.ControllerOwnership.ITEM_LIST,
- "Closing the Inventory notepad did not restore the item zone.",
- )
- for _frame: int in 2:
- await process_frame
- var accept_release := InputEventJoypadButton.new()
- accept_release.button_index = JOY_BUTTON_A
- accept_release.pressed = false
- notepad_source.grab_focus()
- menu.call("_handle_controller_ownership_input", accept)
- menu.call("_handle_controller_ownership_input", accept_release)
- _expect(
- str(menu.get("_inventory_move_identity")) == "basic_fishing_rod",
- "Controller A did not pick up the focused Inventory item.",
- )
- var move_target := active_slots[10]
- move_target.grab_focus()
- menu.call("_handle_controller_ownership_input", accept)
- menu.call("_handle_controller_ownership_input", accept_release)
- _expect(
- layout.get_key_at(
- PlayerInventoryLayout.InventoryContainer.INVENTORY, 10
- ) == PlayerInventoryLayout.item_key(&"basic_fishing_rod"),
- "Controller A did not place the picked-up item in its target slot.",
- )
- var management_requests: Array[int] = [0]
- menu.controller_hotbar_management_requested.connect(
- func(_initial_slot: int) -> void:
- management_requests[0] += 1
- )
- menu.set(
- "_controller_ownership",
- PlayerMenuType.ControllerOwnership.ITEM_LIST,
- )
- menu.call("_apply_inventory_controller_zone_focus_modes")
- menu.call("_configure_bag_item_focus")
- var hotbar_source := active_slots[10]
- _expect(
- menu.get("_current_section") == PlayerMenuType.Section.BAG,
- "Hotbar navigation test left the unified Inventory section (%s)."
- % menu.get("_current_section"),
- )
- _expect(
- hotbar_source.focus_mode == Control.FOCUS_ALL,
- "Final active Inventory row was not controller-focusable.",
- )
- hotbar_source.grab_focus()
- _expect(
- root.gui_get_focus_owner() == hotbar_source,
- "Final Inventory row did not accept controller focus before Hotbar entry.",
- )
- _expect(
- bool(menu.call("_controller_focus_is_on_last_inventory_row")),
- "Focused Inventory slot was not recognized as part of the final row.",
- )
- _expect(
- bool(menu.call("_handle_controller_ownership_input", down)),
- "Down from the final Inventory row did not enter the hotbar zone.",
- )
- _expect(
- menu.get("_controller_ownership")
- == PlayerMenuType.ControllerOwnership.HOTBAR_MANAGEMENT,
- "Equipment-to-hotbar navigation did not transfer controller ownership.",
- )
- _expect(
- management_requests[0] == 1,
- "Equipment-to-hotbar navigation did not open hotbar management.",
- )
- var up := InputEventAction.new()
- up.action = &"ui_up"
- up.pressed = true
- _expect(
- bool(menu.call("_handle_controller_ownership_input", up)),
- "Up from hotbar management was not consumed.",
- )
- _expect(
- menu.get("_controller_ownership")
- == PlayerMenuType.ControllerOwnership.ITEM_LIST,
- "Up from hotbar management did not return to Inventory contents.",
- )
- for _frame: int in 2:
- await process_frame
- _expect(
- root.gui_get_focus_owner() == hotbar_source,
- "Up from hotbar management did not restore the source item focus.",
- )
- _expect(
- bool(menu.call("_handle_controller_ownership_input", down)),
- "Inventory contents could not re-enter hotbar management.",
- )
- _expect(
- management_requests[0] == 2,
- "Re-entering the hotbar did not reopen hotbar management.",
- )
- _expect(
- bool(menu.call("_handle_controller_ownership_input", accept)),
- "Hotbar management did not consume controller A.",
- )
- _expect(
- hotbar.get_item_id(0).is_empty(),
- "Controller A did not return the selected hotbar item to Inventory.",
- )
- _expect(layout.is_item_in_inventory(&"coffee"), "Cleared hotbar item was lost.")
- _expect(
- bool(menu.call("consume_escape")),
- "Global player-menu Back did not consume hotbar management.",
- )
- _expect(
- menu.visible,
- "Global player-menu Back closed Inventory from hotbar management.",
- )
- _expect(
- menu.get("_controller_ownership")
- == PlayerMenuType.ControllerOwnership.ITEM_LIST,
- "Global player-menu Back did not return the hotbar to Inventory contents.",
- )
- for _frame: int in 2:
- await process_frame
-
- menu.call("_show_section_immediate", PlayerMenuType.Section.TACKLE_BOX)
- menu.call("_set_content_interactive", true)
- menu.set(
- "_controller_ownership",
- PlayerMenuType.ControllerOwnership.ITEM_LIST,
- )
- menu.call("_apply_inventory_controller_zone_focus_modes")
- menu.call("_configure_tackle_item_focus")
- for _frame: int in 2:
- await process_frame
- var tackle_buttons: Dictionary = menu.get("_tackle_item_buttons")
- var worms_button := tackle_buttons.get(&"worms") as Button
- _expect(worms_button != null, "Tackle test could not find unlocked worms.")
- if worms_button != null:
- worms_button.grab_focus()
- menu.call("_handle_controller_ownership_input", accept)
- menu.call("_handle_controller_ownership_input", accept_release)
- _expect(
- StringName(menu.get("_selected_tackle_item_id")).is_empty(),
- "Controller A still activates a bait or lure inventory action.",
- )
- _expect(
- not (menu.get_node("%TackleDetailPanel") as Control).visible,
- "Controller A opened the tackle notepad reserved for Y.",
- )
- _expect(
- bool(menu.call(
- "_handle_controller_ownership_input", context_press
- )),
- "Controller Y did not open the bait/lure equip notepad.",
- )
- _expect(
- menu.get("_controller_ownership")
- == PlayerMenuType.ControllerOwnership.NOTEPAD_ACTIONS,
- "Tackle notepad did not take controller ownership after Y.",
- )
- _expect(
- (menu.get_node("%TackleDetailPanel") as Control).visible,
- "Controller Y left the bait/lure equip notepad hidden.",
- )
- menu.queue_free()
- inventory_state.queue_free()
- await process_frame
-
-
-func _wait_for_player_menu_page_transition(menu: Control) -> void:
- var frames_waited: int = 0
- while bool(menu.get("_page_transitioning")) and frames_waited < 120:
- await process_frame
- frames_waited += 1
- # Same-section Equipment/Items changes do not tween, but they restore the
- # active tab focus with a deferred call just like completed page changes.
- for _frame: int in 2:
- await process_frame
- _expect(
- not bool(menu.get("_page_transitioning")),
- "Inventory page transition did not finish within 120 frames.",
- )
-
-
-func _validate_mapping_capture_contract() -> void:
- var title_source: String = FileAccess.get_file_as_string(
- "res://ui/title_screen.gd"
- )
- var game_ui_source: String = FileAccess.get_file_as_string(
- "res://ui/game_ui.gd"
- )
- var chat_source: String = FileAccess.get_file_as_string(
- "res://ui/chat_ui.gd"
- )
- var main_source: String = FileAccess.get_file_as_string(
- "res://main/main.gd"
- )
- _expect(
- title_source.contains(
- "_settings_panel.is_input_mapping_capturing()"
- ),
- "Title input does not respect every active binding capture.",
- )
- _expect(
- game_ui_source.contains("or is_input_mapping_capturing()"),
- "Controller menu scrolling does not respect every binding capture.",
- )
- _expect(
- chat_source.contains("func _configure_controller_focus()"),
- "Chat does not author controller routes for its exterior controls.",
- )
- _expect(
- chat_source.contains(
- "Control.FOCUS_ALL if _opened else Control.FOCUS_NONE"
- ),
- "Passive chat controls can steal world controller focus.",
- )
- _expect(
- main_source.contains("func _configure_popup_dialog("),
- "Runtime popup dialogs do not receive authored controller routes.",
- )
-
-
-func _validate_confirmation_dialog_navigation() -> void:
- var dialog := ConfirmationDialog.new()
- dialog.ok_button_text = "continue"
- var fields := VBoxContainer.new()
- var first := LineEdit.new()
- first.placeholder_text = "passphrase"
- first.custom_minimum_size = Vector2(420.0, 42.0)
- fields.add_child(first)
- var second := LineEdit.new()
- second.placeholder_text = "confirm passphrase"
- second.custom_minimum_size = Vector2(420.0, 42.0)
- fields.add_child(second)
- dialog.add_child(fields)
- root.add_child(dialog)
- dialog.popup_centered(Vector2i(560, 300))
- for _frame: int in 2:
- await process_frame
- DialogControllerNavigationType.configure_scope(dialog, first)
- await process_frame
- var controls: Array[Control] = (
- DialogControllerNavigationType.interactive_controls(dialog)
- )
- _expect(
- controls.has(first) and controls.has(second),
- "Confirmation-dialog text fields are not controller reachable.",
- )
- _expect(
- controls.has(dialog.get_ok_button())
- and controls.has(dialog.get_cancel_button()),
- "Confirmation-dialog actions are not controller reachable.",
- )
- _assert_directionally_reachable(first, controls)
- _expect(
- dialog.gui_get_focus_owner() == first,
- "Confirmation dialog did not focus its requested entry control.",
- )
- dialog.hide()
- await process_frame
- root.gui_release_focus()
- dialog.popup_centered(Vector2i(560, 300))
- await process_frame
- DialogControllerNavigationType.configure_scope(dialog, first)
- await process_frame
- _expect(
- dialog.gui_get_focus_owner() == first,
- "A reopened dialog did not restore controller focus.",
- )
- dialog.queue_free()
- await process_frame
-
-
-func _validate_bubble_confirmation_navigation() -> void:
- for scene: PackedScene in [
- BubbleConfirmationScene,
- TitleConfirmationScene,
- ]:
- var page := scene.instantiate() as Control
- root.add_child(page)
- await process_frame
- page.show()
- page.call("_set_interactive", true)
- var confirm := page.get_node("BubbleCluster/ConfirmButton") as Button
- var cancel := page.get_node("BubbleCluster/CancelButton") as Button
- _assert_neighbor(confirm, &"focus_neighbor_left", cancel)
- _assert_neighbor(confirm, &"focus_neighbor_right", cancel)
- _assert_neighbor(confirm, &"focus_neighbor_top", confirm)
- _assert_neighbor(confirm, &"focus_neighbor_bottom", confirm)
- _assert_neighbor(cancel, &"focus_neighbor_left", confirm)
- _assert_neighbor(cancel, &"focus_neighbor_right", confirm)
- _assert_neighbor(cancel, &"focus_neighbor_top", cancel)
- _assert_neighbor(cancel, &"focus_neighbor_bottom", cancel)
- page.queue_free()
- await process_frame
-func _set_button_state(button: Button, shown: bool) -> void:
- button.visible = shown
- button.disabled = false
-
-
-func _assert_neighbor(
- origin: Control,
- property: StringName,
- expected: Control,
-) -> void:
- var path: NodePath = origin.get(property)
- _expect(
- not path.is_empty(),
- "%s has no %s neighbor." % [origin.name, property],
- )
- if path.is_empty():
- return
- var actual := origin.get_node_or_null(path) as Control
- _expect(
- actual == expected,
- "%s points %s to %s instead of %s."
- % [
- origin.name,
- property,
- actual.name if actual != null else "nothing",
- expected.name,
- ],
- )
-
-
-func _assert_directionally_reachable(
- start: Control,
- controls: Array[Control],
-) -> void:
- var expected: Dictionary[int, bool] = {}
- for control: Control in controls:
- expected[control.get_instance_id()] = true
- var visited: Dictionary[int, bool] = {start.get_instance_id(): true}
- var pending: Array[Control] = [start]
- while not pending.is_empty():
- var current: Control = pending.pop_front()
- for side: Side in [SIDE_LEFT, SIDE_RIGHT, SIDE_TOP, SIDE_BOTTOM]:
- var neighbor: Control = current.find_valid_focus_neighbor(side)
- if (
- neighbor == null
- or not expected.has(neighbor.get_instance_id())
- or visited.has(neighbor.get_instance_id())
- ):
- continue
- visited[neighbor.get_instance_id()] = true
- pending.append(neighbor)
- _expect(
- visited.size() == expected.size(),
- "Only %d of %d controls are directionally reachable from %s."
- % [visited.size(), expected.size(), start.name],
- )
-
-
-func _expect(condition: bool, message: String) -> void:
- if not condition:
- _failures.append(message)
diff --git a/tests/controller_menu_accessibility_validation.gd.uid b/tests/controller_menu_accessibility_validation.gd.uid
deleted file mode 100644
index a8b5c11..0000000
--- a/tests/controller_menu_accessibility_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cdvy3ln6d3b1v
diff --git a/tests/controller_ui_navigation_validation.gd b/tests/controller_ui_navigation_validation.gd
deleted file mode 100644
index 24f39f9..0000000
--- a/tests/controller_ui_navigation_validation.gd
+++ /dev/null
@@ -1,309 +0,0 @@
-extends SceneTree
-
-const ControllerFocusNavigationType = preload(
- "res://ui/controller_focus_navigation.gd"
-)
-const UIReferencePresentationType = preload(
- "res://ui/ui_reference_presentation.gd"
-)
-const FullscreenMenuPresentationType = preload(
- "res://ui/fullscreen_menu_presentation.gd"
-)
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- _validate_spatial_navigation()
- _validate_strict_directional_navigation()
- _validate_disabled_control_exclusion()
- _validate_traversal_cycle()
- _validate_controller_hierarchy_contract()
- _validate_player_page_zone_contracts()
- _validate_shop_navigation_contract()
- _validate_settings_navigation_contract()
- _validate_four_by_three_centering()
- _validate_low_end_profile_contract()
- print("Controller UI navigation validation: PASS")
- quit()
-
-
-func _validate_spatial_navigation() -> void:
- var host := Control.new()
- root.add_child(host)
- var center := _make_button(host, "center", Vector2(200.0, 200.0))
- var left := _make_button(host, "left", Vector2(50.0, 200.0))
- var right := _make_button(host, "right", Vector2(350.0, 200.0))
- var top := _make_button(host, "top", Vector2(200.0, 50.0))
- var bottom := _make_button(host, "bottom", Vector2(200.0, 350.0))
- var controls: Array[Control] = [center, left, right, top, bottom]
- ControllerFocusNavigationType.configure_spatial_neighbors(controls)
- assert(center.get_node(center.focus_neighbor_left) == left)
- assert(center.get_node(center.focus_neighbor_right) == right)
- assert(center.get_node(center.focus_neighbor_top) == top)
- assert(center.get_node(center.focus_neighbor_bottom) == bottom)
- host.queue_free()
-
-
-func _validate_strict_directional_navigation() -> void:
- var host := Control.new()
- root.add_child(host)
- var origin := _make_button(host, "origin", Vector2(200.0, 200.0))
- var mostly_below := _make_button(
- host, "mostly_below", Vector2(250.0, 400.0)
- )
- var controls: Array[Control] = [origin, mostly_below]
- ControllerFocusNavigationType.configure_spatial_neighbors(controls)
- assert(origin.get_node(origin.focus_neighbor_right) == origin)
- assert(origin.get_node(origin.focus_neighbor_top) == origin)
- assert(origin.get_node(origin.focus_neighbor_bottom) == mostly_below)
- host.queue_free()
-
-
-func _validate_disabled_control_exclusion() -> void:
- var host := Control.new()
- root.add_child(host)
- var left := _make_button(host, "left", Vector2(50.0, 100.0))
- var disabled := _make_button(host, "disabled", Vector2(200.0, 100.0))
- var right := _make_button(host, "right", Vector2(350.0, 100.0))
- disabled.disabled = true
- disabled.focus_next = disabled.get_path_to(right)
- ControllerFocusNavigationType.configure_spatial_neighbors(
- [left, disabled, right]
- )
- assert(left.get_node(left.focus_neighbor_right) == right)
- assert(right.get_node(right.focus_neighbor_left) == left)
- assert(disabled.focus_next.is_empty())
- host.queue_free()
-
-
-func _validate_traversal_cycle() -> void:
- var host := Control.new()
- root.add_child(host)
- var first := _make_button(host, "first", Vector2(50.0, 50.0))
- var second := _make_button(host, "second", Vector2(200.0, 50.0))
- var third := _make_button(host, "third", Vector2(50.0, 150.0))
- var controls: Array[Control] = [third, first, second]
- ControllerFocusNavigationType.configure_spatial_neighbors(controls)
- assert(first.get_node(first.focus_next) == second)
- assert(second.get_node(second.focus_next) == third)
- assert(third.get_node(third.focus_next) == first)
- assert(first.get_node(first.focus_previous) == third)
- host.queue_free()
-
-
-func _validate_controller_hierarchy_contract() -> void:
- var source: String = FileAccess.get_file_as_string(
- "res://ui/player_menu.gd"
- )
- assert(source.contains("ROLE_LB"))
- assert(source.contains("ROLE_RB"))
- assert(not source.contains("_handle_controller_secondary_switch"))
- assert(source.contains("ControllerOwnership.INVENTORY_TABS"))
- assert(source.contains("ControllerOwnership.SORT_FILTER"))
- assert(source.contains("_reset_controller_zone_for_section"))
- assert(source.contains("preserve_tab_zone"))
- assert(source.contains("active_tab.call_deferred(\"grab_focus\")"))
- assert(source.contains("CONTROLLER_PICKUP_HOLD_SECONDS"))
- assert(source.contains("_reserve_main_navigation_for_page_switching"))
- assert(source.contains(
- "if not _is_inventory_section(_current_section):\n\t\treturn"
- ))
- assert(source.contains(
- "_dispatch_active_page_controller_input(cancel_event)"
- ))
- assert(source.contains("configure_spatial_neighbors(candidates)"))
-
-
-func _validate_player_page_zone_contracts() -> void:
- for path: String in [
- "res://ui/logbook_page.gd",
- "res://ui/the_net_page.gd",
- "res://ui/mail_page.gd",
- "res://ui/profile_page.gd",
- "res://ui/players_page.gd",
- ]:
- var source: String = FileAccess.get_file_as_string(path)
- assert(source.contains("func reset_controller_zone()"))
- assert(source.contains("func handle_controller_input(event: InputEvent)"))
- var profile_source: String = FileAccess.get_file_as_string(
- "res://ui/profile_page.gd"
- )
- assert(profile_source.contains("ControllerZone.COLOR_PICKER"))
- assert(profile_source.contains("_adjust_controller_color_gamut"))
- assert(profile_source.contains("_customize_button.text = \"customize\""))
- assert(profile_source.contains("_enter_controller_customization"))
- assert(not profile_source.contains(
- "if event.is_action_pressed(\"ui_down\"):\n"
- + "\t\t\t_controller_zone = ControllerZone.CATEGORIES"
- ))
- assert(profile_source.contains(
- "_reset_view_button.focus_mode = Control.FOCUS_NONE"
- ))
- var preview_source: String = FileAccess.get_file_as_string(
- "res://ui/profile_preview.gd"
- )
- assert(preview_source.contains("focus_mode = Control.FOCUS_NONE"))
- assert(not preview_source.contains("grab_focus()"))
- var mail_source: String = FileAccess.get_file_as_string(
- "res://ui/mail_page.gd"
- )
- assert(mail_source.contains("_configure_compose_controller_navigation"))
- assert(mail_source.contains("_set_compose_neighbors"))
- var net_source: String = FileAccess.get_file_as_string(
- "res://ui/the_net_page.gd"
- )
- var game_ui_source: String = FileAccess.get_file_as_string(
- "res://ui/game_ui.gd"
- )
- assert(not net_source.contains("ROLE_RIGHT_STICK_Y"))
- assert(game_ui_source.contains("_update_controller_menu_scroll"))
-
-
-func _validate_shop_navigation_contract() -> void:
- var source: String = FileAccess.get_file_as_string(
- "res://ui/fishing_shop.gd"
- )
- assert(source.contains("ROLE_LB"))
- assert(source.contains("ROLE_RB"))
- assert(source.contains("_configure_controller_focus"))
-
-
-func _validate_settings_navigation_contract() -> void:
- var settings_scene := load("res://ui/settings_panel.tscn") as PackedScene
- var settings := settings_scene.instantiate() as SettingsPanel
- root.add_child(settings)
- var accessibility_tab := settings.get_node(
- "%AccessibilityTab"
- ) as Button
- var auto_click_toggle := settings.get_node("%AutoClickToggle") as Button
- var interval_slider := settings.get_node(
- "%AutoClickIntervalSlider"
- ) as HSlider
- var apply_button := settings.get_node("%ApplySettingsButton") as Button
- var back_button := settings.get_node("%SettingsBackButton") as Button
- assert(
- accessibility_tab.get_node(accessibility_tab.focus_neighbor_bottom)
- == auto_click_toggle
- )
- assert(
- auto_click_toggle.get_node(auto_click_toggle.focus_neighbor_bottom)
- == interval_slider
- )
- assert(
- interval_slider.get_node(interval_slider.focus_neighbor_top)
- == auto_click_toggle
- )
- assert(
- interval_slider.get_node(interval_slider.focus_neighbor_bottom)
- == apply_button
- )
- assert(apply_button.get_node(apply_button.focus_neighbor_right) == back_button)
- var reset_scene := load(
- "res://ui/pixelation_reset_overlay.tscn"
- ) as PackedScene
- var reset_overlay := reset_scene.instantiate() as PixelationResetOverlay
- root.add_child(reset_overlay)
- settings.show()
- reset_overlay.set_settings_open(true)
- var reset_focus_requested: Array[bool] = [false]
- settings.crisp_reset_focus_requested.connect(func() -> void:
- reset_focus_requested[0] = true
- )
- settings.crisp_reset_focus_requested.connect(
- reset_overlay.focus_reset_button
- )
- var reset_cursor_presentation := reset_overlay.get_node(
- "ControllerFocusPresentation"
- ) as ControllerFocusPresentation
- var controller_right := InputEventJoypadButton.new()
- controller_right.button_index = JOY_BUTTON_DPAD_RIGHT
- controller_right.pressed = true
- reset_cursor_presentation._input(controller_right)
- var focus_reset_event := InputEventAction.new()
- focus_reset_event.action = &"ui_right"
- focus_reset_event.pressed = true
- settings.call("_on_back_button_gui_input", focus_reset_event)
- assert(reset_focus_requested[0])
- assert(
- root.gui_get_focus_owner()
- == reset_overlay.get_node("%ResetPixelationButton")
- )
- assert(
- (reset_cursor_presentation.get("_focus_arrow") as TextureRect).visible
- )
- reset_overlay.return_to_settings_requested.connect(
- settings.focus_back_button
- )
- var return_event := InputEventAction.new()
- return_event.action = &"ui_left"
- return_event.pressed = true
- reset_overlay.call("_on_reset_button_gui_input", return_event)
- assert(root.gui_get_focus_owner() == back_button)
- reset_overlay.queue_free()
- settings.queue_free()
-
-
-func _validate_four_by_three_centering() -> void:
- var display_size := Vector2(640.0, 480.0)
- var stage_position: Vector2 = UIReferencePresentationType.get_stage_position(
- display_size
- )
- assert(stage_position.is_equal_approx(Vector2(0.0, 120.0)))
- assert(FullscreenMenuPresentationType.uses_compact_profile(display_size))
- assert(is_equal_approx(
- FullscreenMenuPresentationType.get_profile_scale(display_size),
- 4.0 / 3.0,
- ))
- assert(FullscreenMenuPresentationType.get_profile_position(
- display_size
- ).is_equal_approx(Vector2(-640.0 / 3.0, -120.0)))
- assert(FullscreenMenuPresentationType.get_source_rect(display_size)
- == Rect2(160.0, 0.0, 960.0, 720.0))
-
-
-func _validate_low_end_profile_contract() -> void:
- var launcher: String = FileAccess.get_file_as_string(
- "res://scripts/portmaster/straywild.sh"
- )
- assert(launcher.contains("straywild_PERFORMANCE_PROFILE:-"))
- assert(launcher.contains("$CONFDIR/performance_profile"))
- assert(launcher.contains("normal)"))
- assert(launcher.contains("light|\"\")"))
- assert(launcher.contains("straywild_PERFORMANCE_PROFILE=normal"))
- assert(launcher.contains("straywild_PERFORMANCE_PROFILE=light"))
- assert(launcher.contains("straywild_LOW_END=1"))
- assert(launcher.contains("--max-fps 30"))
- assert(launcher.contains("--audio-output-latency 40"))
- var builder: String = FileAccess.get_file_as_string(
- "res://scripts/build_portmaster.sh"
- )
- assert(builder.contains("prepare_portmaster_assets.sh"))
- assert(builder.contains("straywild-portmaster-export"))
- var asset_profile: String = FileAccess.get_file_as_string(
- "res://scripts/prepare_portmaster_assets.sh"
- )
- assert(asset_profile.contains("GENERAL_TEXTURE_LIMIT=128"))
- assert(asset_profile.contains("GAMEPLAY_TEXTURE_LIMIT=64"))
- assert(asset_profile.contains("AUDIO_SAMPLE_RATE=22050"))
- assert(asset_profile.contains("compress/mode=2"))
- assert(asset_profile.contains("fish/species"))
- assert(asset_profile.contains("art/exported/characters"))
- assert(builder.contains("--exclude '/audio/ambience/'"))
- assert(not builder.contains("--exclude '/audio/music/'"))
-
-
-func _make_button(
- host: Control,
- button_name: String,
- button_position: Vector2,
-) -> Button:
- var button := Button.new()
- button.name = button_name
- button.position = button_position
- button.size = Vector2(80.0, 80.0)
- button.focus_mode = Control.FOCUS_ALL
- host.add_child(button)
- return button
diff --git a/tests/controller_ui_navigation_validation.gd.uid b/tests/controller_ui_navigation_validation.gd.uid
deleted file mode 100644
index 73ef752..0000000
--- a/tests/controller_ui_navigation_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cbv0ia42rqu41
diff --git a/tests/controller_world_interaction_validation.gd b/tests/controller_world_interaction_validation.gd
deleted file mode 100644
index 3d684cb..0000000
--- a/tests/controller_world_interaction_validation.gd
+++ /dev/null
@@ -1,145 +0,0 @@
-extends SceneTree
-
-const MainScene = preload("res://main/main.tscn")
-
-var _failures: Array[String] = []
-
-
-func _initialize() -> void:
- _run.call_deferred()
-
-
-func _run() -> void:
- root.size = Vector2i(1280, 720)
- var main := MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- _expect(
- bool(main.get("_application_initialized")),
- "application initialization failed",
- )
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- var world_layout: StringName = WorldLayout.GENERATED
- var world_seed: int = PlayerSaveManager.DEFAULT_WORLD_SEED
- _expect(
- bool(main.call("_apply_world", world_layout, world_seed, true)),
- "generated-world preparation failed",
- )
- _expect(
- bool(main.call("_prepare_host_world", world_layout, world_seed)),
- "host-world preparation failed",
- )
- _expect(
- bool(main.call("_prepare_private_host")),
- "private host preparation failed",
- )
- _expect(
- save_manager != null
- and save_manager.initialize_new_game(world_seed, world_layout),
- "new-game initialization failed",
- )
- main.call("_enter_gameplay")
- for _frame: int in 8:
- await process_frame
-
- var player := main.get("_player") as Player
- var interaction := main.get("_shop_interaction") as FishingShopInteraction
- var game_ui := main.get("_game_ui") as GameUI
- var chat_service := main.get_node(
- "%NetworkChatService"
- ) as NetworkChatService
- _expect(player != null, "local player is unavailable")
- _expect(interaction != null, "shop interaction is unavailable")
- _expect(game_ui != null, "game UI is unavailable")
- _expect(chat_service != null, "network chat service is unavailable")
- if (
- player != null
- and interaction != null
- and game_ui != null
- and chat_service != null
- ):
- player.global_position = interaction.global_position
- for _frame: int in 6:
- await physics_frame
- _expect(
- interaction.is_local_player_in_range(),
- "local player did not enter the shop interaction range",
- )
- var character_calls: Array[String] = []
- var capture_call := func(
- _peer_id: int,
- call_id: String,
- _pitch_scale: float,
- ) -> void:
- character_calls.append(call_id)
- chat_service.character_call_received.connect(capture_call)
- var press := InputEventJoypadButton.new()
- press.device = 0
- press.button_index = JOY_BUTTON_Y
- press.pressed = true
- _expect(
- press.is_action_pressed("interact"),
- "Y does not resolve to interact",
- )
- _expect(
- press.is_action_pressed("character_call"),
- "Y does not resolve to character call",
- )
- game_ui.call("_input", press)
- main.call("_unhandled_input", press)
- await process_frame
- _expect(
- game_ui.get_fishing_shop().visible,
- "Y did not open the shop while the player was in range",
- )
- _expect(
- character_calls.is_empty(),
- "Y played a character call instead of opening the shop",
- )
- player.global_position = interaction.global_position + Vector3(20.0, 0.0, 0.0)
- for _frame: int in 6:
- await physics_frame
- for _frame: int in 60:
- if not game_ui.get_fishing_shop().visible:
- break
- await process_frame
- _expect(
- not interaction.is_local_player_in_range(),
- "local player did not leave the shop interaction range",
- )
- _expect(
- not game_ui.get_fishing_shop().visible,
- "shop did not close after leaving its interaction range",
- )
- game_ui.call("_input", press)
- await process_frame
- _expect(
- character_calls.size() == 1,
- "Y did not play a character call away from an interaction",
- )
- chat_service.character_call_received.disconnect(capture_call)
-
- var session := main.get_node("%NetworkSession") as NetworkSession
- if session != null:
- session.disconnect_session("Controller interaction validation complete.")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- if _failures.is_empty():
- print("Controller world interaction validation: PASS")
- quit(0)
- return
- for failure: String in _failures:
- push_error(failure)
- quit(1)
-
-
-func _expect(condition: bool, message: String) -> void:
- if not condition:
- _failures.append(message)
diff --git a/tests/controller_world_interaction_validation.gd.uid b/tests/controller_world_interaction_validation.gd.uid
deleted file mode 100644
index 8315ee5..0000000
--- a/tests/controller_world_interaction_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b6fg46sad1ma6
diff --git a/tests/dedicated_host_session_validation.gd b/tests/dedicated_host_session_validation.gd
deleted file mode 100644
index 2d098bd..0000000
--- a/tests/dedicated_host_session_validation.gd
+++ /dev/null
@@ -1,60 +0,0 @@
-extends SceneTree
-
-const TEST_PORT: int = 35777
-const OPERATOR_FINGERPRINT: String = (
- "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
-)
-
-
-func _init() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var harness := Node.new()
- root.add_child(harness)
- var players := Node3D.new()
- harness.add_child(players)
- var spawn_service := PlayerSpawnService.new()
- harness.add_child(spawn_service)
- spawn_service.setup(players, null, Transform3D.IDENTITY)
- var host_identity := HostIdentityStore.new()
- harness.add_child(host_identity)
- var known_players := KnownPlayerStore.new()
- harness.add_child(known_players)
- var host_bans := HostBanStore.new()
- harness.add_child(host_bans)
- var session := NetworkSession.new()
- harness.add_child(session)
- await process_frame
- session.setup(
- null,
- null,
- spawn_service,
- null,
- host_identity,
- known_players,
- null,
- host_bans,
- true,
- )
- assert(session.configure_dedicated_operators(
- PackedStringArray([OPERATOR_FINGERPRINT])
- ))
- assert(session.start_dedicated_host(TEST_PORT, 5, "127.0.0.1"))
- assert(session.is_dedicated_host())
- assert(session.get_local_peer_id() == 0)
- assert(session.get_player_count() == 0)
- assert(session.get_session_max_players() == 5)
- assert(session.get_host_port() == TEST_PORT)
- assert(bool(session.call(
- "_operator_for_fingerprint", OPERATOR_FINGERPRINT
- )))
- assert(session.set_host_open(true))
- assert(session.is_open_host())
- session.disconnect_session("Dedicated host validation complete.")
- assert(not session.is_session_active())
- assert(not session.is_dedicated_host())
- harness.queue_free()
- print("DEDICATED_HOST_SESSION_VALIDATION_OK")
- quit(0)
diff --git a/tests/dedicated_host_session_validation.gd.uid b/tests/dedicated_host_session_validation.gd.uid
deleted file mode 100644
index e50ebb3..0000000
--- a/tests/dedicated_host_session_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ctky0gkb63nqa
diff --git a/tests/dedicated_server_client_validation.gd b/tests/dedicated_server_client_validation.gd
deleted file mode 100644
index 9688af2..0000000
--- a/tests/dedicated_server_client_validation.gd
+++ /dev/null
@@ -1,49 +0,0 @@
-extends SceneTree
-
-const MainScene = preload("res://main/main.tscn")
-const TEST_ENDPOINT: String = "127.0.0.1:7777"
-const EXPECTED_SERVER_NAME: String = "Dedicated-Client-Test"
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- main.call("_on_title_join_game_requested", TEST_ENDPOINT)
- var session := main.get_node("%NetworkSession") as NetworkSession
- var joined: bool = false
- var deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
- main.call("_confirm_server_trust")
- if session.is_joined_client() and bool(main.get("_gameplay_started")):
- joined = true
- break
- assert(joined)
- assert(session.get_local_peer_id() > 1)
- assert(session.get_player_count() == 1)
- var metadata: Dictionary = session.get_last_server_metadata()
- assert(str(metadata.get("server_display_name", "")) == EXPECTED_SERVER_NAME)
- var spawn_service := main.get_node(
- "%PlayerSpawnService"
- ) as PlayerSpawnService
- assert(spawn_service.get_avatar(1) == null)
- assert(spawn_service.get_avatar(session.get_local_peer_id()) != null)
- print("DEDICATED_SERVER_CLIENT_VALIDATION_OK")
- session.disconnect_session("Dedicated client validation complete.")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- quit()
diff --git a/tests/dedicated_server_client_validation.gd.uid b/tests/dedicated_server_client_validation.gd.uid
deleted file mode 100644
index ed7a3b7..0000000
--- a/tests/dedicated_server_client_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cwum3r4puerll
diff --git a/tests/dedicated_server_config_validation.gd b/tests/dedicated_server_config_validation.gd
deleted file mode 100644
index 2125220..0000000
--- a/tests/dedicated_server_config_validation.gd
+++ /dev/null
@@ -1,279 +0,0 @@
-extends SceneTree
-
-const ConfigType = preload("res://server/dedicated_server_config.gd")
-const OPERATOR_A: String = (
- "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
-)
-const OPERATOR_B: String = (
- "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
-)
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var defaults := ConfigType.new()
- defaults.set("data_directory", "/tmp/straywild-server")
- defaults.call("_validate")
- assert(defaults.is_valid())
- assert(not bool(defaults.get("public_listing")))
- assert(not bool(defaults.get("chat_logging")))
- assert(str(defaults.get("chat_log_path")).is_empty())
- assert(NetworkChatService.BURST_COUNT == 3)
- assert(NetworkChatService.WINDOW_COUNT == 5)
- assert(is_equal_approx(NetworkChatService.WINDOW_SECONDS, 10.0))
- assert(NetworkChatService.CALL_COOLDOWN_MILLISECONDS == 90)
-
- var path: String = ProjectSettings.globalize_path(
- "user://dedicated-server-validation.cfg"
- )
- var file := ConfigFile.new()
- file.set_value("server", "name", "Configured Room")
- file.set_value("server", "bind_address", "127.0.0.1")
- file.set_value("server", "port", 17777)
- file.set_value("server", "max_players", 12)
- file.set_value("world", "seed", 928374)
- file.set_value("server", "public", true)
- file.set_value("server", "data_directory", "/tmp/configured-server")
- file.set_value("discovery", "url", "https://discovery.straywild.io/")
- file.set_value("privacy", "chat_logging", true)
- file.set_value(
- "privacy",
- "chat_log_path",
- "/tmp/configured-server/logs/chat.jsonl",
- )
- file.set_value(
- "moderation", "operators", PackedStringArray([OPERATOR_B, OPERATOR_A])
- )
- assert(file.save(path) == OK)
-
- var configured := ConfigType.new()
- assert(bool(configured.call("_load_file", path)))
- configured.call("_validate")
- assert(configured.is_valid())
- assert(str(configured.get("server_name")) == "Configured Room")
- assert(str(configured.get("bind_address")) == "127.0.0.1")
- assert(int(configured.get("port")) == 17777)
- assert(int(configured.get("max_players")) == 12)
- assert(int(configured.get("world_seed")) == 928374)
- assert(bool(configured.get("public_listing")))
- assert(str(configured.get("discovery_url")) == "https://discovery.straywild.io")
- assert(bool(configured.get("chat_logging")))
- assert(
- str(configured.get("chat_log_path"))
- == "/tmp/configured-server/logs/chat.jsonl"
- )
- assert(
- configured.get("operator_fingerprints")
- == PackedStringArray([OPERATOR_A, OPERATOR_B])
- )
- assert(DirAccess.remove_absolute(path) == OK)
-
- var parsed := ConfigType.new()
- parsed.set(
- "operator_fingerprints",
- ConfigType._parse_fingerprint_list(
- "%s, %s;%s" % [OPERATOR_B.to_upper(), OPERATOR_A, OPERATOR_A]
- ),
- )
- parsed.set("data_directory", "/tmp/parsed-server")
- parsed.call("_validate")
- assert(parsed.is_valid())
- assert(
- parsed.get("operator_fingerprints")
- == PackedStringArray([OPERATOR_A, OPERATOR_B])
- )
-
- var invalid_operator := ConfigType.new()
- invalid_operator.set(
- "operator_fingerprints", PackedStringArray(["not-a-fingerprint"])
- )
- invalid_operator.set("data_directory", "/tmp/invalid-operator-server")
- invalid_operator.call("_validate")
- assert(not invalid_operator.is_valid())
-
- var invalid := ConfigType.new()
- invalid.set("public_listing", true)
- invalid.set("data_directory", "/tmp/invalid-server")
- invalid.call("_validate")
- assert(not invalid.is_valid())
-
- var invalid_seed := ConfigType.new()
- invalid_seed.set("world_seed", 0)
- invalid_seed.set("data_directory", "/tmp/invalid-seed-server")
- invalid_seed.call("_validate")
- assert(not invalid_seed.is_valid())
-
- var default_chat_path := ConfigType.new()
- default_chat_path.set("data_directory", "/tmp/default-chat-path-server")
- default_chat_path.set("chat_logging", true)
- default_chat_path.call("_validate")
- assert(default_chat_path.is_valid())
- assert(
- str(default_chat_path.get("chat_log_path"))
- == "/tmp/default-chat-path-server/logs/chat.jsonl"
- )
-
- var invalid_chat_path := ConfigType.new()
- invalid_chat_path.set("data_directory", "/tmp/invalid-chat-path-server")
- invalid_chat_path.set("chat_logging", true)
- invalid_chat_path.set("chat_log_path", "relative/chat.jsonl")
- invalid_chat_path.call("_validate")
- assert(not invalid_chat_path.is_valid())
-
- var chat_log_validation_path: String = ProjectSettings.globalize_path(
- "user://dedicated-chat-log-validation.jsonl"
- )
- var chat_service := NetworkChatService.new()
- assert(not chat_service.configure_dedicated_history(
- true,
- "relative/chat.jsonl",
- ))
- assert(chat_service.configure_dedicated_history(
- true,
- chat_log_validation_path,
- ))
- assert(FileAccess.file_exists(chat_log_validation_path))
- chat_service.call("_append_chat_log", {
- "sender_display_name": "Privacy Tester",
- "body": "explicitly retained message",
- })
- var logged_message: Variant = JSON.parse_string(
- FileAccess.get_file_as_string(chat_log_validation_path).strip_edges()
- )
- assert(typeof(logged_message) == TYPE_DICTIONARY)
- assert(str(logged_message.get("recorded_at_utc", "")).ends_with("Z"))
- assert(logged_message.get("sender_display_name") == "Privacy Tester")
- assert(logged_message.get("body") == "explicitly retained message")
- assert(chat_service.configure_dedicated_history(false, ""))
- assert(DirAccess.remove_absolute(chat_log_validation_path) == OK)
- chat_service.free()
-
- var discovery := DiscoveryClient.new()
- assert(
- str(discovery.call("_default_room_name", "River"))
- == "River's Server"
- )
- assert(
- DiscoveryClient.UPNP_RENEW_INTERVAL_SECONDS
- < float(DiscoveryClient.UPNP_MAPPING_DURATION_SECONDS)
- )
- assert(
- DiscoveryClient.UPNP_RETRY_INTERVAL_SECONDS
- < DiscoveryClient.UPNP_RENEW_INTERVAL_SECONDS
- )
- var discovery_settings_path: String = ProjectSettings.globalize_path(
- DiscoveryClient.SETTINGS_PATH
- )
- assert(not FileAccess.file_exists(discovery_settings_path))
- assert(discovery.set_room_name("Client Room"))
- var client_settings: PackedByteArray = FileAccess.get_file_as_bytes(
- discovery_settings_path
- )
- assert(not client_settings.is_empty())
- var dedicated_discovery := DiscoveryClient.new()
- dedicated_discovery.call("_load_settings")
- assert(
- dedicated_discovery.configure_dedicated_runtime("Headless Room")
- )
- assert(dedicated_discovery.get_room_name() == "Headless Room")
- assert(
- FileAccess.get_file_as_bytes(discovery_settings_path)
- == client_settings
- )
- dedicated_discovery.free()
- assert(DirAccess.remove_absolute(discovery_settings_path) == OK)
- assert(
- discovery.get_host_state()
- == DiscoveryClient.HostState.CLOSED
- )
- assert(
- discovery.get_public_join_state()
- == DiscoveryClient.PublicJoinState.IDLE
- )
- discovery.set("_lease_room_id", "local-room")
- assert(discovery.is_own_room({"room_id": "local-room"}))
- assert(not discovery.is_own_room({"room_id": "another-room"}))
- discovery.set("_lease_room_id", "")
- assert(not discovery.is_own_room({"room_id": "local-room"}))
- var listed_room := {
- "room_id": "empty-dedicated-room",
- "room_name": "Empty Dedicated Room",
- "address": "203.0.113.10",
- "port": 7777,
- "current_players": 0,
- "max_players": 8,
- "game_version": str(ProjectSettings.get_setting(
- "application/config/version", "unknown"
- )),
- "protocol_version": NetworkProtocol.PROTOCOL_VERSION,
- }
- assert(bool(discovery.call("_valid_public_room", listed_room)))
- var incompatible_room: Dictionary = listed_room.duplicate(true)
- incompatible_room["game_version"] = "0.0.0-alpha"
- assert(not bool(discovery.call("_valid_public_room", incompatible_room)))
- assert(
- NetworkProtocol.game_version_rejection(
- "0.6.4-alpha", "0.6.5-alpha"
- ) == NetworkProtocol.RejectionCode.CLIENT_OUTDATED
- )
- assert(
- NetworkProtocol.game_version_rejection(
- "0.6.6-alpha", "0.6.5-alpha"
- ) == NetworkProtocol.RejectionCode.SERVER_OUTDATED
- )
- assert(
- NetworkProtocol.game_version_rejection(
- "0.6.5-alpha", "0.6.5-alpha"
- ) == NetworkProtocol.RejectionCode.NONE
- )
- assert(
- NetworkProtocol.game_version_rejection(
- "0.6.5-beta", "0.6.5-alpha"
- ) == NetworkProtocol.RejectionCode.SERVER_OUTDATED
- )
- var outdated_discovery_error := {
- "error": {
- "code": "game_version_mismatch",
- "message": "generic mismatch",
- "required_game_version": "99.0.0-alpha",
- },
- }
- var outdated_message: String = str(
- discovery.call("_request_failure", outdated_discovery_error)
- )
- assert("out of date" in outdated_message)
- assert("99.0.0-alpha" in outdated_message)
- assert("will not be listed" in outdated_message)
- discovery.set(
- "_host_request_kind",
- DiscoveryClient.HostRequestKind.CREATE,
- )
- discovery.set("_host_request_in_flight", true)
- discovery.call(
- "_on_host_request_completed",
- HTTPRequest.RESULT_SUCCESS,
- HTTPClient.RESPONSE_CONFLICT,
- PackedStringArray(),
- JSON.stringify(outdated_discovery_error).to_utf8_buffer(),
- )
- assert(discovery.get_host_state() == DiscoveryClient.HostState.ERROR)
- assert(discovery.host_status_is_error())
- assert(discovery.get_host_status_message() == outdated_message)
- var newer_discovery_error := {
- "error": {
- "code": "game_version_mismatch",
- "message": "generic mismatch",
- "required_game_version": "0.0.0-alpha",
- },
- }
- var newer_message: String = str(
- discovery.call("_request_failure", newer_discovery_error)
- )
- assert("discovery is updated" in newer_message)
- assert("will not be listed" in newer_message)
- discovery.free()
- print("DEDICATED_SERVER_CONFIG_VALIDATION_OK")
- quit()
diff --git a/tests/dedicated_server_config_validation.gd.uid b/tests/dedicated_server_config_validation.gd.uid
deleted file mode 100644
index 4bc60d1..0000000
--- a/tests/dedicated_server_config_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://vm5lql2w6h3e
diff --git a/tests/digging_prototype_validation.gd b/tests/digging_prototype_validation.gd
deleted file mode 100644
index 95917a0..0000000
--- a/tests/digging_prototype_validation.gd
+++ /dev/null
@@ -1,161 +0,0 @@
-extends SceneTree
-
-const StarterIslandScene = preload(
- "res://world/regions/starter_island_region.tscn"
-)
-const ShovelAttachmentScene = preload("res://player/shovel_attachment.tscn")
-const ItemCatalogResource: ItemCatalog = preload(
- "res://items/catalog/item_catalog.tres"
-)
-const Gatherables: GatherableCatalog = preload(
- "res://gathering/catalog/gatherable_catalog.tres"
-)
-
-class PrimaryTerrainProvider:
- extends Node3D
-
- var primary_meshes: Array[MeshInstance3D] = []
-
-
- func get_primary_terrain_meshes() -> Array[MeshInstance3D]:
- return primary_meshes
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- _validate_catalog_content()
- _validate_flat_shovel()
- await _validate_beach_authoring()
- _validate_primary_terrain_provider()
- print("Digging prototype validation: PASS")
- quit()
-
-
-func _validate_catalog_content() -> void:
- var shovel: ItemData = ItemCatalogResource.get_available_item_by_id(
- &"standard_shovel"
- )
- assert(shovel != null)
- assert(shovel.category == ItemData.Category.TOOL)
- assert(shovel.equippable and shovel.hotbar_allowed)
- var clam: GatherableData = Gatherables.get_entry(&"clam_manila")
- assert(clam != null and clam.is_available())
- assert(clam.required_tool_id == shovel.item_id)
- assert(clam.diggable_area_id == &"starter_beach")
- assert(clam.presentation_mode == GatherableData.PresentationMode.WATER_SPURT)
- assert(not clam.requires_sneaking)
- assert(is_equal_approx(clam.active_lifetime_seconds, 10.0))
-
-
-func _validate_flat_shovel() -> void:
- var attachment := ShovelAttachmentScene.instantiate() as BoneAttachment3D
- assert(attachment != null)
- var shovel := attachment.get_node("Shovel") as Node3D
- assert(shovel != null)
- var meshes: Array[MeshInstance3D] = []
- _collect_meshes(shovel, meshes)
- assert(meshes.size() == 5)
- for mesh_instance: MeshInstance3D in meshes:
- assert(
- mesh_instance.cast_shadow
- == GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
- )
- var material := mesh_instance.mesh.surface_get_material(0) as StandardMaterial3D
- assert(material != null)
- assert(material.shading_mode == BaseMaterial3D.SHADING_MODE_UNSHADED)
- assert(material.transparency == BaseMaterial3D.TRANSPARENCY_DISABLED)
- assert(material.albedo_texture == null)
- assert(is_equal_approx(material.albedo_color.a, 1.0))
- attachment.free()
-
-
-func _validate_beach_authoring() -> void:
- var region := StarterIslandScene.instantiate() as WorldRegion
- root.add_child(region)
- await process_frame
- var area: DiggableArea3D = region.get_diggable_area(&"starter_beach")
- assert(area != null)
- assert(area.terrain_source == NodePath("../../Terrain/Visual"))
- assert(area.surface_materials.size() == 1)
- assert(area.surface_materials[0] == &"sand")
- var triangles: Array[PackedVector3Array] = area.get_surface_triangles()
- assert(not triangles.is_empty())
- for triangle: PackedVector3Array in triangles:
- assert(triangle.size() == 3)
- var center := (triangle[0] + triangle[1] + triangle[2]) / 3.0
- assert(area.generation_bounds.has_point(Vector2(center.x, center.z)))
- assert(center.y <= area.maximum_global_y + 0.001)
- region.queue_free()
-
-
-func _validate_primary_terrain_provider() -> void:
- var fixture := Node3D.new()
- fixture.name = "PrimaryTerrainFixture"
- root.add_child(fixture)
- var provider := PrimaryTerrainProvider.new()
- provider.name = "Provider"
- fixture.add_child(provider)
- var clear_sand := _flat_surface_triangle(0.0, 0.0, &"sand")
- var covered_sand := _flat_surface_triangle(10.0, 0.0, &"sand")
- var covering_grass := _flat_surface_triangle(10.0, 0.01, &"grass_lite")
- for mesh_instance: MeshInstance3D in [
- clear_sand,
- covered_sand,
- covering_grass,
- ]:
- provider.add_child(mesh_instance)
- provider.primary_meshes.append(mesh_instance)
- var buried_base := _flat_surface_triangle(20.0, 0.0, &"sand")
- buried_base.name = "BuriedSandBase"
- provider.add_child(buried_base)
- var area := DiggableArea3D.new()
- area.name = "DiggableArea"
- area.area_id = &"provider_test"
- area.terrain_source = NodePath("../Provider")
- area.surface_materials = [&"sand"]
- area.generation_bounds = Rect2(-20.0, -20.0, 40.0, 40.0)
- fixture.add_child(area)
- var triangles := area.get_surface_triangles()
- assert(triangles.size() == 2)
- var clear_center := Vector3(1.0 / 3.0, 0.0, 1.0 / 3.0)
- var covered_center := Vector3(10.0 + 1.0 / 3.0, 0.0, 1.0 / 3.0)
- assert(area.is_surface_point_valid(clear_center))
- assert(not area.is_surface_point_valid(covered_center))
- fixture.free()
-
-
-func _flat_surface_triangle(
- x_offset: float,
- height: float,
- material_name: StringName,
-) -> MeshInstance3D:
- var arrays: Array = []
- arrays.resize(Mesh.ARRAY_MAX)
- arrays[Mesh.ARRAY_VERTEX] = PackedVector3Array([
- Vector3(x_offset, height, 0.0),
- Vector3(x_offset, height, 1.0),
- Vector3(x_offset + 1.0, height, 0.0),
- ])
- arrays[Mesh.ARRAY_INDEX] = PackedInt32Array([0, 1, 2])
- var mesh := ArrayMesh.new()
- mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
- var material := StandardMaterial3D.new()
- material.resource_name = str(material_name)
- mesh.surface_set_material(0, material)
- var mesh_instance := MeshInstance3D.new()
- mesh_instance.name = "Primary%s" % str(material_name).capitalize()
- mesh_instance.mesh = mesh
- return mesh_instance
-
-
-func _collect_meshes(
- root_node: Node,
- result: Array[MeshInstance3D],
-) -> void:
- for child: Node in root_node.get_children():
- if child is MeshInstance3D:
- result.append(child as MeshInstance3D)
- _collect_meshes(child, result)
diff --git a/tests/digging_prototype_validation.gd.uid b/tests/digging_prototype_validation.gd.uid
deleted file mode 100644
index 182ab02..0000000
--- a/tests/digging_prototype_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c1dt44ieacnvb
diff --git a/tests/economy_regression_validation.gd b/tests/economy_regression_validation.gd
deleted file mode 100644
index ff82495..0000000
--- a/tests/economy_regression_validation.gd
+++ /dev/null
@@ -1,1103 +0,0 @@
-extends SceneTree
-
-const MainScene = preload("res://main/main.tscn")
-const FishCatchType = preload("res://fish/fish_catch.gd")
-
-var _sale_result: Array[Variant] = []
-var _shop_result: Array[Variant] = []
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var arguments: PackedStringArray = OS.get_cmdline_user_args()
- if arguments.has("host"):
- await _run_multiplayer_host()
- return
- if arguments.has("client"):
- await _run_multiplayer_client()
- return
- root.size = Vector2i(1280, 720)
- var main := MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- # Use the same activation path as the real setup dialog so no modal is
- # left above the gameplay SubViewport during pointer validation.
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- var session := main.get_node("%NetworkSession") as NetworkSession
- assert(bool(main.call(
- "_apply_world", WorldLayout.STARTER_ISLAND, 1, true
- )))
- assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
- assert(bool(main.call("_prepare_private_host")))
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND))
- main.call("_enter_gameplay")
- for _frame: int in 8:
- await process_frame
-
- var player := main.get("_player") as Player
- var catalog := main.get("fish_catalog") as FishPool
- var sale_service := main.get_node("%NetworkSaleService") as NetworkSaleService
- var shop_service := main.get_node("%NetworkShopService") as NetworkShopService
- var reservations := (
- main.get_node("%PlayerAssetReservationService")
- as PlayerAssetReservationService
- )
- assert(player != null)
- assert(catalog != null and catalog.candidates.size() == 316)
- assert(LogbookCatalog.ordered_species(catalog.candidates).size() == 313)
- assert(sale_service != null)
- assert(shop_service != null)
- assert(session != null and session.is_host())
- assert(reservations != null)
- sale_service.local_sale_finished.connect(_on_sale_finished)
- shop_service.local_purchase_finished.connect(_on_shop_finished)
- await process_frame
-
- _test_each_species(player, catalog, sale_service)
- _test_multi_sale(player, catalog, sale_service)
- _test_reservations(player, catalog, sale_service, reservations)
- _test_anywhere_sale(player, catalog, sale_service)
- await _test_host_shop_sale(main, player, catalog, sale_service)
-
- assert(session.set_host_open(true))
- assert(session.state == NetworkSession.State.OPEN_HOST)
- var open_catch := _make_catch(catalog.candidates.front())
- player.inventory.add_catch(open_catch)
- _assert_sale(player, sale_service, [open_catch.catch_id], true)
- assert(session.set_host_open(false))
-
- await _test_host_shop_purchase(main, player, shop_service)
- await _test_host_rod_purchase(player, shop_service)
- await _test_host_art_shop_purchase(main, player, shop_service)
- await _test_fishing_shop_sale_ui(
- main, player, catalog, sale_service, reservations
- )
- _test_host_backpack_purchase(player, shop_service)
- _test_host_storage_purchase(player, shop_service)
- assert(not sale_service.is_local_sale_pending())
- assert(not shop_service.is_local_purchase_pending())
-
- print("Economy regression validation: PASS")
- session.disconnect_session("Economy validation complete.")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _run_multiplayer_host() -> void:
- root.size = Vector2i(1280, 720)
- var main: Node = await _create_initialized_main()
- var session := main.get_node("%NetworkSession") as NetworkSession
- assert(bool(main.call(
- "_apply_world", WorldLayout.STARTER_ISLAND, 1, true
- )))
- assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
- assert(bool(main.call("_prepare_private_host")))
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(save_manager.initialize_new_game())
- main.call("_enter_gameplay")
- assert(session.set_host_open(true))
- var client_connected := false
- # Loading the full client world can take longer on a cold import. Keep the
- # host available until the client has completed authentication rather than
- # timing out while the client is still starting.
- var connection_deadline: int = Time.get_ticks_msec() + 60000
- while Time.get_ticks_msec() < connection_deadline:
- await process_frame
- if session.get_authenticated_peer_ids().size() >= 2:
- client_connected = true
- break
- assert(client_connected)
- var remote_peer_id := 0
- for authenticated_peer_id: int in session.get_authenticated_peer_ids():
- if authenticated_peer_id != 1:
- remote_peer_id = authenticated_peer_id
- break
- assert(remote_peer_id > 1)
- var spawn_service := main.get("_player_spawn_service") as PlayerSpawnService
- var remote_avatar := spawn_service.get_avatar(remote_peer_id)
- assert(remote_avatar != null)
- # Pelican sales are available from the Cooler anywhere. The host-owned
- # avatar only moves when the later Fishing Shop purchase needs proximity.
- await create_timer(5.0).timeout
- var interaction := main.get("_shop_interaction") as FishingShopInteraction
- assert(interaction != null)
- remote_avatar.global_position = interaction.global_position
- # The client can finish loading its local presentation well after the host
- # has authenticated it, especially during a cold import. Keep the authority
- # alive until the client completes the transaction sequence or disconnects.
- var completion_deadline: int = Time.get_ticks_msec() + 90000
- while Time.get_ticks_msec() < completion_deadline:
- await process_frame
- if session.get_authenticated_peer_ids().size() < 2:
- break
- print("Economy multiplayer host validation: PASS")
- session.disconnect_session("Economy host validation complete.")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _run_multiplayer_client() -> void:
- root.size = Vector2i(1280, 720)
- var main: Node = await _create_initialized_main()
- main.call("_on_title_join_game_requested", "127.0.0.1:7777")
- var session := main.get_node("%NetworkSession") as NetworkSession
- var joined := false
- var join_deadline: int = Time.get_ticks_msec() + 30000
- while Time.get_ticks_msec() < join_deadline:
- await process_frame
- if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
- main.call("_confirm_server_trust")
- if session.is_joined_client() and bool(main.get("_gameplay_started")):
- joined = true
- break
- assert(joined)
- var player := main.get("_player") as Player
- var catalog := main.get("fish_catalog") as FishPool
- var sale_service := main.get_node("%NetworkSaleService") as NetworkSaleService
- var shop_service := main.get_node("%NetworkShopService") as NetworkShopService
- sale_service.local_sale_finished.connect(_on_sale_finished)
- shop_service.local_purchase_finished.connect(_on_shop_finished)
- assert(player.inventory_layout.restore_backpack_level(
- PlayerInventoryLayout.MAX_BACKPACK_LEVEL
- ))
- assert(player.cooler_capacity.restore_level(PlayerCoolerCapacity.MAX_LEVEL))
- player.inventory.set_inventory_layout(null)
- var catch_ids: Array[StringName] = []
- for fish: FishData in catalog.candidates:
- if not fish.active:
- continue
- var fish_catch := _make_catch(fish)
- assert(player.inventory.add_catch(fish_catch))
- catch_ids.append(fish_catch.catch_id)
- if catch_ids.size() == NetworkSaleProtocol.MAX_CATCHES_PER_REQUEST:
- break
- assert(catch_ids.size() == NetworkSaleProtocol.MAX_CATCHES_PER_REQUEST)
- player.inventory.set_inventory_layout(player.inventory_layout)
- await create_timer(1.0).timeout
- _sale_result.clear()
- assert(not sale_service.request_local_sale(catch_ids).is_empty())
- var sale_deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < sale_deadline:
- await process_frame
- if not _sale_result.is_empty():
- break
- print("Client sale result: ", _sale_result)
- assert(not _sale_result.is_empty() and bool(_sale_result[1]))
- for catch_id: StringName in catch_ids:
- assert(not player.inventory.contains_catch_id(catch_id))
- assert(not sale_service.is_local_sale_pending())
- assert(player.inventory_layout.restore_backpack_level(0))
- assert(player.cooler_capacity.restore_level(0))
-
- var interaction := main.get("_shop_interaction") as FishingShopInteraction
- player.global_position = interaction.global_position
- await create_timer(5.5).timeout
- var shop_catch := _make_catch(catalog.candidates.front())
- player.inventory.add_catch(shop_catch)
- var shop_sale_balance_before: int = player.wallet.get_balance()
- _sale_result.clear()
- assert(not sale_service.request_local_sale(
- [shop_catch.catch_id],
- NetworkSaleService.MAIN_SHOP_BUYER_ID,
- ).is_empty())
- var shop_sale_deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < shop_sale_deadline:
- await process_frame
- if not _sale_result.is_empty():
- break
- print("Client shop sale result: ", _sale_result)
- assert(not _sale_result.is_empty() and bool(_sale_result[1]))
- assert(not player.inventory.contains_catch_id(shop_catch.catch_id))
- assert(
- player.wallet.get_balance()
- == shop_sale_balance_before + shop_catch.sale_value
- )
- assert(not sale_service.is_local_sale_pending())
-
- assert(player.wallet.credit(100))
- _shop_result.clear()
- assert(not shop_service.request_supply(&"coffee").is_empty())
- var shop_deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < shop_deadline:
- await process_frame
- if not _shop_result.is_empty():
- break
- print("Client shop result: ", _shop_result)
- assert(not _shop_result.is_empty() and bool(_shop_result[1]))
- assert(not shop_service.is_local_purchase_pending())
- var backpack_cost := player.inventory_layout.get_next_backpack_cost()
- if player.wallet.get_balance() < backpack_cost:
- assert(player.wallet.credit(
- backpack_cost - player.wallet.get_balance()
- ))
- _shop_result.clear()
- assert(not shop_service.request_backpack_capacity_upgrade().is_empty())
- while _shop_result.is_empty():
- await process_frame
- assert(bool(_shop_result[1]))
- assert(player.inventory_layout.get_inventory_capacity() == 18)
- var required_art_balance: int = (
- ArtShopStock.ART_KIT_PRICE + ArtShopStock.UPGRADE_PRICE
- )
- if player.wallet.get_balance() < required_art_balance:
- assert(player.wallet.credit(
- required_art_balance - player.wallet.get_balance()
- ))
- _shop_result.clear()
- assert(not shop_service.request_art_kit().is_empty())
- while _shop_result.is_empty():
- await process_frame
- assert(bool(_shop_result[1]))
- _shop_result.clear()
- assert(not shop_service.request_art_upgrade(&"grid_32x").is_empty())
- while _shop_result.is_empty():
- await process_frame
- assert(bool(_shop_result[1]))
- assert(player.bag.owns_item(ArtShopStock.ART_KIT_ITEM_ID))
- assert(player.art_unlocks.is_grid_size_unlocked(32))
- print("Economy multiplayer client validation: PASS")
- session.disconnect_session("Economy client validation complete.")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _create_initialized_main() -> Node:
- var main := MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- return main
-
-
-func _test_each_species(
- player: Player,
- catalog: FishPool,
- sale_service: NetworkSaleService,
-) -> void:
- for fish: FishData in catalog.candidates:
- if not fish.active:
- continue
- assert(fish != null)
- var fish_catch := _make_catch(fish)
- player.inventory.add_catch(fish_catch)
- _assert_sale(player, sale_service, [fish_catch.catch_id], true)
-
-
-func _test_multi_sale(
- player: Player,
- catalog: FishPool,
- sale_service: NetworkSaleService,
-) -> void:
- var catches: Array[FishCatch] = [
- _make_catch(catalog.candidates[0]),
- _make_catch(catalog.candidates[1]),
- _make_catch(catalog.candidates[2]),
- ]
- var catch_ids: Array[StringName] = []
- for fish_catch: FishCatch in catches:
- player.inventory.add_catch(fish_catch)
- catch_ids.append(fish_catch.catch_id)
- _assert_sale(player, sale_service, catch_ids, true)
-
-
-func _test_reservations(
- player: Player,
- catalog: FishPool,
- sale_service: NetworkSaleService,
- reservations: PlayerAssetReservationService,
-) -> void:
- var reserved := _make_catch(catalog.candidates[3])
- var unreserved := _make_catch(catalog.candidates[4])
- player.inventory.add_catch(reserved)
- player.inventory.add_catch(unreserved)
- var reservation_id := "economy-test-reservation"
- assert(reservations.reserve(reservation_id, {
- "type": PlayerAssetReservationService.AttachmentType.FISH,
- "catch_id": str(reserved.catch_id),
- "catch": reserved.to_network_dict(),
- }))
- _assert_sale(player, sale_service, [reserved.catch_id], false, false)
- _assert_sale(
- player,
- sale_service,
- [reserved.catch_id, unreserved.catch_id],
- false,
- false,
- )
- assert(player.inventory.contains_catch_id(reserved.catch_id))
- assert(player.inventory.contains_catch_id(unreserved.catch_id))
- assert(reservations.release(reservation_id))
- _assert_sale(
- player,
- sale_service,
- [reserved.catch_id, unreserved.catch_id],
- true,
- )
-
-
-func _test_anywhere_sale(
- player: Player,
- catalog: FishPool,
- sale_service: NetworkSaleService,
-) -> void:
- var fish_catch := _make_catch(catalog.candidates[5])
- player.inventory.add_catch(fish_catch)
- # The spawn point is intentionally far from the Pelican landmark. Discounted
- # Pelican sales are a convenience action and do not require proximity.
- player.global_position = Vector3(0.0, 3.95, 13.0)
- _assert_sale(player, sale_service, [fish_catch.catch_id], true)
-
-
-func _test_host_shop_sale(
- main: Node,
- player: Player,
- catalog: FishPool,
- sale_service: NetworkSaleService,
-) -> void:
- var interaction := main.get("_shop_interaction") as FishingShopInteraction
- assert(interaction != null)
- var out_of_range_catch := _make_catch(catalog.candidates[6])
- player.inventory.add_catch(out_of_range_catch)
- player.global_position = Vector3(0.0, 3.95, 13.0)
- for _frame: int in 4:
- await physics_frame
- _assert_sale(
- player,
- sale_service,
- [out_of_range_catch.catch_id],
- false,
- true,
- NetworkSaleService.MAIN_SHOP_BUYER_ID,
- )
- player.global_position = interaction.global_position
- for _frame: int in 4:
- await physics_frame
- assert(interaction.is_avatar_in_range(player))
- var balance_before: int = player.wallet.get_balance()
- _assert_sale(
- player,
- sale_service,
- [out_of_range_catch.catch_id],
- true,
- true,
- NetworkSaleService.MAIN_SHOP_BUYER_ID,
- )
- assert(
- player.wallet.get_balance()
- == balance_before + out_of_range_catch.sale_value
- )
- var species_ids: Array[StringName] = []
- var expected_payout: int = 0
- var species_balance_before: int = player.wallet.get_balance()
- for fish: FishData in catalog.candidates:
- if not fish.active:
- continue
- var fish_catch := _make_catch(fish)
- assert(player.inventory.add_catch(fish_catch))
- species_ids.append(fish_catch.catch_id)
- expected_payout += fish_catch.sale_value
- if species_ids.size() >= 5:
- _assert_sale(
- player,
- sale_service,
- species_ids,
- true,
- true,
- NetworkSaleService.MAIN_SHOP_BUYER_ID,
- )
- species_ids.clear()
- if not species_ids.is_empty():
- _assert_sale(
- player,
- sale_service,
- species_ids,
- true,
- true,
- NetworkSaleService.MAIN_SHOP_BUYER_ID,
- )
- assert(
- player.wallet.get_balance()
- == species_balance_before + expected_payout
- )
-
-
-func _activate_pointer_control(
- control: Control,
- ui_viewport: SubViewport,
-) -> void:
- var presenter := ui_viewport.get_parent() as SubViewportContainer
- assert(presenter != null)
- var local_center: Vector2 = control.get_global_transform_with_canvas() * (
- control.size * 0.5
- )
- var center: Vector2 = presenter.position + local_center * presenter.scale
- var motion := InputEventMouseMotion.new()
- motion.position = center
- motion.global_position = center
- root.push_input(motion, true)
- await process_frame
- assert(ui_viewport.gui_get_hovered_control() == control)
- for is_pressed: bool in [true, false]:
- var click := InputEventMouseButton.new()
- click.button_index = MOUSE_BUTTON_LEFT
- click.position = center
- click.global_position = center
- click.pressed = is_pressed
- root.push_input(click, true)
- await process_frame
-
-
-func _test_host_shop_purchase(
- main: Node,
- player: Player,
- shop_service: NetworkShopService,
-) -> void:
- var interaction := main.get("_shop_interaction") as FishingShopInteraction
- assert(interaction != null)
- player.global_position = interaction.global_position
- for _frame: int in 4:
- await physics_frame
- assert(interaction.is_avatar_in_range(player))
- var quantity_before: int = player.bag.get_quantity(&"coffee")
- _shop_result.clear()
- var request_id: String = shop_service.request_supply(&"coffee")
- assert(not request_id.is_empty())
- assert(not _shop_result.is_empty() and bool(_shop_result[1]))
- assert(player.bag.get_quantity(&"coffee") == quantity_before + 1)
- assert(not shop_service.is_local_purchase_pending())
-
-
-func _test_host_art_shop_purchase(
- main: Node,
- player: Player,
- shop_service: NetworkShopService,
-) -> void:
- var interaction := main.get("_shop_interaction") as FishingShopInteraction
- assert(interaction != null)
- player.global_position = interaction.global_position
- for _frame: int in 4:
- await physics_frame
- var required_balance: int = (
- ArtShopStock.ART_KIT_PRICE + ArtShopStock.UPGRADE_PRICE * 4
- )
- if player.wallet.get_balance() < required_balance:
- assert(player.wallet.credit(required_balance - player.wallet.get_balance()))
- _shop_result.clear()
- assert(not shop_service.request_art_kit().is_empty())
- assert(not _shop_result.is_empty() and bool(_shop_result[1]))
- assert(player.bag.owns_item(ArtShopStock.ART_KIT_ITEM_ID))
- var item_catalog := main.get("item_catalog") as ItemCatalog
- assert(item_catalog != null)
- var art_item: ItemData = item_catalog.get_item_by_id(
- ArtShopStock.ART_KIT_ITEM_ID
- )
- assert(art_item != null and art_item.hotbar_allowed and art_item.equippable)
- assert(art_item.icon != null)
- assert(art_item.icon.resource_path.ends_with("/art/art_kit.png"))
- assert(player.hotbar.assign_item(0, ArtShopStock.ART_KIT_ITEM_ID))
- for product_id: StringName in [
- &"marker_ocean_teal",
- &"brush_2x",
- &"grid_32x",
- PlayerArtUnlocks.STAMP_PRODUCT_ID,
- ]:
- _shop_result.clear()
- assert(not shop_service.request_art_upgrade(product_id).is_empty())
- assert(not _shop_result.is_empty() and bool(_shop_result[1]))
- assert(player.art_unlocks.owns_product(product_id))
- assert(not shop_service.is_local_purchase_pending())
-
-
-func _test_host_rod_purchase(
- player: Player,
- shop_service: NetworkShopService,
-) -> void:
- assert(player.bag.remove_item(&"basic_fishing_rod", 1))
- _shop_result.clear()
- assert(not shop_service.request_rod(&"basic_fishing_rod").is_empty())
- assert(not _shop_result.is_empty() and bool(_shop_result[1]))
- assert(player.bag.owns_item(&"basic_fishing_rod"))
- assert(not shop_service.is_local_purchase_pending())
-
-
-func _test_host_backpack_purchase(
- player: Player,
- shop_service: NetworkShopService,
-) -> void:
- var layout := player.inventory_layout
- assert(layout.get_inventory_capacity() == 9)
- var total_cost: int = 0
- for cost: int in PlayerInventoryLayout.BACKPACK_EXPANSION_COSTS:
- total_cost += cost
- if player.wallet.get_balance() < total_cost:
- assert(player.wallet.credit(total_cost - player.wallet.get_balance()))
- var balance_before := player.wallet.get_balance()
- for expected_capacity: int in [18, 27, 36]:
- _shop_result.clear()
- assert(not shop_service.request_backpack_capacity_upgrade().is_empty())
- assert(
- not _shop_result.is_empty() and bool(_shop_result[1]),
- "Backpack purchase failed: %s" % [_shop_result],
- )
- assert(layout.get_inventory_capacity() == expected_capacity)
- assert(player.wallet.get_balance() == balance_before - total_cost)
- assert(layout.get_next_backpack_cost() == -1)
-
-
-func _test_host_storage_purchase(
- player: Player,
- shop_service: NetworkShopService,
-) -> void:
- var capacity := player.cooler_capacity
- assert(capacity.get_capacity() == 9)
- var total_cost: int = 0
- for cost: int in PlayerCoolerCapacity.EXPANSION_COSTS:
- total_cost += cost
- if player.wallet.get_balance() < total_cost:
- assert(player.wallet.credit(total_cost - player.wallet.get_balance()))
- var balance_before := player.wallet.get_balance()
- for expected_capacity: int in [18, 27, 36, 45, 54, 63, 72]:
- _shop_result.clear()
- assert(not shop_service.request_cooler_capacity_upgrade().is_empty())
- assert(
- not _shop_result.is_empty() and bool(_shop_result[1]),
- "Cooler purchase failed: %s" % [_shop_result],
- )
- assert(capacity.get_capacity() == expected_capacity)
- assert(player.wallet.get_balance() == balance_before - total_cost)
- assert(capacity.get_next_cost() == -1)
-
-
-func _test_fishing_shop_sale_ui(
- main: Node,
- player: Player,
- catalog: FishPool,
- sale_service: NetworkSaleService,
- reservations: PlayerAssetReservationService,
-) -> void:
- var interaction := main.get("_shop_interaction") as FishingShopInteraction
- var game_ui := main.get_node("%GameUI") as GameUI
- var shop := game_ui.get_node("%FishingShop") as FishingShop
- var player_menu := game_ui.get_node("%PlayerMenu") as PlayerMenu
- var shop_backdrop := main.get_node("%ShopBackdrop") as ColorRect
- var ui_viewport := main.get_node(
- "UIPresentation/UIViewport"
- ) as SubViewport
- assert(
- interaction != null
- and shop != null
- and player_menu != null
- and shop_backdrop != null
- and ui_viewport != null
- )
- var item_catalog := main.get("item_catalog") as ItemCatalog
- var worms: ItemData = item_catalog.get_item_by_id(&"worms")
- assert(worms != null and worms.max_stack == 10)
- if player.bag.get_quantity(worms.item_id) == 0:
- assert(player.bag.add_item(worms.item_id, 4))
- assert(player.equip_bait(worms))
- await process_frame
- var expected_bait_supply := UtilityPageStyle.supply_quantity_text(
- player.bag.get_quantity(worms.item_id),
- worms.max_stack,
- )
- var hud_bait_quantity := game_ui.get_node(
- "%ActiveBaitQuantity"
- ) as Label
- assert(hud_bait_quantity.text == expected_bait_supply)
- var hud_bait_badge := hud_bait_quantity.get_parent() as Panel
- assert(
- is_equal_approx(
- hud_bait_badge.rotation_degrees,
- UtilityPageStyle.SUPPLY_BADGE_ROTATION_DEGREES,
- )
- )
- player_menu.call("_refresh_tackle_box")
- await process_frame
- var tackle_bait_button: Button
- for child: Node in player_menu.get_node("%BaitItemList").get_children():
- var candidate := child as Button
- if (
- candidate != null
- and candidate.get_meta(&"controller_tackle_item_id", StringName())
- == worms.item_id
- ):
- tackle_bait_button = candidate
- break
- assert(tackle_bait_button != null)
- var tackle_quantity := tackle_bait_button.get_node(
- "QuantityBadge/Quantity"
- ) as Label
- assert(tackle_quantity.text == expected_bait_supply)
- assert(
- str(tackle_bait_button.get_meta(
- &"inventory_context_text", ""
- )).contains(expected_bait_supply)
- )
- var tackle_badge := tackle_quantity.get_parent() as Panel
- assert(
- is_equal_approx(
- tackle_badge.rotation_degrees,
- UtilityPageStyle.SUPPLY_BADGE_ROTATION_DEGREES,
- )
- )
- player.global_position = interaction.global_position
- for _frame: int in 4:
- await physics_frame
- var fish_catch := _make_catch(catalog.candidates[7])
- var reserved_catch := _make_catch(catalog.candidates[6])
- player.inventory.add_catch(fish_catch)
- player.inventory.add_catch(reserved_catch)
- var reservation_id := "shop-ui-reservation"
- assert(reservations.reserve(reservation_id, {
- "type": PlayerAssetReservationService.AttachmentType.FISH,
- "catch_id": str(reserved_catch.catch_id),
- "catch": reserved_catch.to_network_dict(),
- }))
- var balance_before: int = player.wallet.get_balance()
- assert(shop.open_shop())
- for _frame: int in 2:
- await process_frame
- assert(shop_backdrop.visible)
- assert(shop_backdrop.material is ShaderMaterial)
- assert(shop.has_node("InputBlocker") and not shop.has_node("Dimmer"))
- var shop_panel := shop.get_node("%ShopPanel") as PanelContainer
- assert(shop_panel != null and shop_panel.size == Vector2(980.0, 600.0))
- var shop_panel_style := shop_panel.get_theme_stylebox("panel") as StyleBoxFlat
- assert(shop_panel_style != null)
- assert(shop_panel_style.corner_radius_top_left == 24)
- var upgrade_grid := shop.get_node("%UpgradeGrid") as GridContainer
- assert(upgrade_grid != null and upgrade_grid.columns == 3)
- assert(
- upgrade_grid.get_theme_constant("h_separation")
- == FishingShop.SHOP_SLOT_SEPARATION
- )
- assert(
- upgrade_grid.get_theme_constant("v_separation")
- == FishingShop.SHOP_SLOT_SEPARATION
- )
- assert((shop.get_node("%Upgrades") as Control).visible)
- assert(not (shop.get_node("%Supplies") as Control).visible)
- for upgrade_name: String in [
- "ReelPurchase",
- "BarrierPurchase",
- "CoolerPurchase",
- "BackpackPurchase",
- ]:
- var upgrade_button := shop.get_node("%%%s" % upgrade_name) as Button
- assert(upgrade_button != null)
- assert(upgrade_button.size == Vector2(144.0, 144.0))
- assert(upgrade_button.text.is_empty())
- assert(upgrade_button.icon != null)
- assert(upgrade_button.tooltip_text.contains("level"))
- for placeholder_name: String in ["CoolerPurchase", "BackpackPurchase"]:
- var placeholder_button := shop.get_node(
- "%%%s" % placeholder_name
- ) as Button
- assert(
- placeholder_button.icon.resource_path.ends_with(
- "/pictograms/x_light.png"
- )
- )
- assert(not shop.has_node("%ReelLevel"))
- assert(not shop.has_node("%BarrierEffect"))
- assert(not shop.has_node("%CoolerLevel"))
- var balance_coin := shop.get_node(
- "ShopPanel/Margin/Layout/Header/BalanceDisplay/CoinIcon"
- ) as TextureRect
- assert(balance_coin != null and balance_coin.texture != null)
- assert(
- balance_coin.texture.resource_path.ends_with(
- "/shop/32_currency.png"
- )
- )
- for cost_name: String in [
- "ReelCost", "BarrierCost", "CoolerCost", "BackpackCost"
- ]:
- var cost_display := shop.get_node("%%%s" % cost_name) as CurrencyAmount
- assert(cost_display != null)
- var cost_icon := cost_display.get_node("Icon") as TextureRect
- assert(cost_icon != null and cost_icon.texture != null)
- assert(
- cost_icon.texture.resource_path.ends_with(
- "/shop/32_currency.png"
- )
- )
- assert(not shop.has_node("ShopPanel/Margin/Layout/ModeTabs"))
- var shop_tabs: Array = shop.get("_shop_tabs") as Array
- assert(shop_tabs.size() == 6)
- var art_supplies_tab := shop_tabs[4] as Button
- assert(art_supplies_tab != null and art_supplies_tab.text == "Art Supplies")
- var sell_mode := shop_tabs[5] as Button
- assert(sell_mode != null and sell_mode.text == "Sell")
- var equipment_tab := shop_tabs[3] as Button
- assert(equipment_tab != null and equipment_tab.text == "Equipment")
- var supplies_list := shop.get_node("%SuppliesList") as VBoxContainer
- var bait_tab := shop_tabs[1] as Button
- assert(bait_tab != null and bait_tab.text == "Bait and Lures")
- await _activate_pointer_control(bait_tab, ui_viewport)
- await process_frame
- var worms_button := supplies_list.find_child(
- "Supply_worms", true, false
- ) as Button
- var snails_button := supplies_list.find_child(
- "Supply_snails", true, false
- ) as Button
- assert(worms_button != null and snails_button != null)
- var bait_grid := worms_button.get_parent().get_parent() as GridContainer
- assert(bait_grid != null)
- assert(
- bait_grid.get_theme_constant("h_separation")
- == FishingShop.SHOP_SLOT_SEPARATION
- )
- assert(
- bait_grid.get_theme_constant("v_separation")
- == FishingShop.SHOP_SLOT_SEPARATION
- )
- var shop_bait_quantity := worms_button.get_node(
- "SupplyQuantityBadge/Quantity"
- ) as Label
- assert(shop_bait_quantity.text == expected_bait_supply)
- var shop_bait_badge := shop_bait_quantity.get_parent() as Panel
- assert(shop_bait_badge.position.y == FishingShop.BAIT_SUPPLY_BADGE_Y)
- assert(
- is_equal_approx(
- shop_bait_badge.rotation_degrees,
- UtilityPageStyle.SUPPLY_BADGE_ROTATION_DEGREES,
- )
- )
- assert(
- shop_bait_badge.position.y + shop_bait_badge.size.y
- <= FishingShop.SUPPLY_PRICE_Y
- )
- var worms_unit_price: int = FishingShopStock.get_price(worms.item_id)
- assert(
- worms_button.tooltip_text.contains(
- "%d fish coin%s per bait piece" % [
- worms_unit_price,
- "" if worms_unit_price == 1 else "s",
- ]
- )
- )
- assert(not worms_button.tooltip_text.contains("owned"))
- var worms_restock_quantity: int = FishingShopStock.get_purchase_quantity(
- worms.item_id,
- player.bag.get_quantity(worms.item_id),
- worms.max_stack,
- )
- var worms_restock_cost: int = FishingShopStock.get_purchase_cost(
- worms.item_id,
- worms_restock_quantity,
- player.bag.is_bait_unlocked(worms.item_id),
- )
- var worms_price := worms_button.get_parent().get_node(
- "PriceBubble/CurrencyAmount/Price"
- ) as Label
- assert(worms_price.text == str(maxi(worms_restock_cost, 0)))
- var bait_receipt: String = shop.call(
- "_purchase_success_feedback",
- worms.item_id,
- NetworkShopProtocol.ProductCategory.SUPPLY,
- maxi(worms_restock_quantity, 1),
- maxi(worms_restock_cost, worms_unit_price),
- )
- assert(bait_receipt.begins_with("Bait Restocked\n"))
- assert(bait_receipt.contains(CurrencyPresentation.ICON_PATH))
- assert(bait_receipt.contains(" spent on "))
- assert(bait_receipt.ends_with(" worms"))
- assert(snails_button.get_node_or_null("SupplyQuantityBadge") == null)
- assert(worms_button.get_node_or_null("UnlockStateBadge") == null)
- var snails_lock_icon := snails_button.get_node(
- "UnlockStateBadge/UnlockStateIcon"
- ) as TextureRect
- assert(not bool(snails_lock_icon.get_meta(&"unlocked")))
- assert(snails_lock_icon.texture.resource_path.ends_with(
- "/lock_light.png"
- ))
- assert(is_equal_approx(snails_lock_icon.modulate.a, 0.88))
- await _activate_pointer_control(equipment_tab, ui_viewport)
- await process_frame
- var equipment_sections: Array[String] = []
- for child: Node in shop.get_node("%SuppliesList").get_children():
- if child is Label:
- equipment_sections.append((child as Label).text)
- assert(equipment_sections == ["rods", "equipment"])
- var rod_carousel := supplies_list.get_node("RodCarousel") as HBoxContainer
- assert(rod_carousel != null)
- var rod_cards := rod_carousel.get_node("RodScroll/RodCards") as HBoxContainer
- assert(rod_cards != null and rod_cards.get_child_count() == 1)
- assert(
- rod_cards.get_theme_constant("separation")
- == FishingShop.SHOP_SLOT_SEPARATION
- )
- var basic_rod_button := rod_cards.find_child(
- "Rod_basic_fishing_rod", true, false
- ) as Button
- assert(basic_rod_button != null)
- assert(basic_rod_button.custom_minimum_size == Vector2(144.0, 144.0))
- assert(basic_rod_button.icon != null and basic_rod_button.disabled)
- assert(basic_rod_button.get_node_or_null("UnlockStateBadge") == null)
- assert(rod_cards.find_child("Rod_aurora_rod", true, false) == null)
- await _activate_pointer_control(art_supplies_tab, ui_viewport)
- await process_frame
- var stock_sections: Array[String] = []
- for child: Node in shop.get_node("%SuppliesList").get_children():
- if child is Label:
- stock_sections.append((child as Label).text)
- assert(stock_sections == [
- "art kit", "markers", "brushes", "grids", "stamps",
- ])
- var marker_icons := shop.find_children(
- "MarkerIcon", "TextureRect", true, false
- )
- assert(marker_icons.size() == ArtShopStock.MARKER_PRODUCTS.size())
- for index: int in marker_icons.size():
- var marker_icon := marker_icons[index] as TextureRect
- var marker_material := marker_icon.material as ShaderMaterial
- var product_id: StringName = ArtShopStock.MARKER_PRODUCTS[index]
- var color_id: StringName = PlayerArtUnlocks.color_id_for_product(
- product_id
- )
- assert(marker_icon.texture.resource_path.ends_with("art_kit_marker.png"))
- assert(marker_material != null)
- assert(
- marker_material.get_shader_parameter("marker_color")
- == SurfaceDrawingPalette.get_color(color_id)
- )
- var expected_art_upgrade_icons: Dictionary[StringName, String] = {
- &"brush_2x": "art_kit_marker_tip_thin.png",
- &"brush_3x": "art_kit_marker_tip_mid.png",
- &"brush_4x": "art_kit_marker_tip_thick.png",
- &"grid_32x": "art_kit_grid_medium_light.png",
- &"grid_64x": "art_kit_grid_large_light.png",
- &"grid_128x": "art_kit_grid_xl_light.png",
- }
- var found_art_upgrade_icons: int = 0
- for upgrade_node: Node in shop.find_children("*", "Button", true, false):
- var upgrade_button := upgrade_node as Button
- if not upgrade_button.has_meta(&"art_product_id"):
- continue
- var product_id := StringName(
- str(upgrade_button.get_meta(&"art_product_id"))
- )
- if not expected_art_upgrade_icons.has(product_id):
- continue
- found_art_upgrade_icons += 1
- assert(upgrade_button.text.is_empty())
- assert(upgrade_button.icon != null)
- var product_unlocked: bool = (
- player.art_unlocks.owns_product(product_id)
- )
- var state_badge := upgrade_button.get_node_or_null(
- "UnlockStateBadge"
- ) as Panel
- var state_icon := (
- state_badge.get_node("UnlockStateIcon") as TextureRect
- if state_badge != null
- else null
- ) as TextureRect
- assert((state_icon == null) == product_unlocked)
- if state_icon != null:
- assert(state_icon.texture.resource_path.ends_with("/lock_light.png"))
- assert(
- upgrade_button.icon.resource_path.ends_with(
- expected_art_upgrade_icons[product_id]
- )
- )
- assert(found_art_upgrade_icons == expected_art_upgrade_icons.size())
- var stamp_button: Button
- for upgrade_node: Node in shop.find_children("*", "Button", true, false):
- var upgrade_button := upgrade_node as Button
- if StringName(str(upgrade_button.get_meta(&"art_product_id", ""))) == (
- PlayerArtUnlocks.STAMP_PRODUCT_ID
- ):
- stamp_button = upgrade_button
- break
- assert(stamp_button != null)
- assert(stamp_button.text == "stamp")
- assert(stamp_button.icon == null)
- var price_bubbles := shop.find_children(
- "PriceBubble", "PanelContainer", true, false
- )
- assert(not price_bubbles.is_empty())
- for price_bubble: Node in price_bubbles:
- var currency_icon := price_bubble.find_child(
- "CurrencyIcon", true, false
- ) as TextureRect
- assert(currency_icon != null and currency_icon.texture != null)
- assert(
- currency_icon.texture.resource_path.ends_with(
- "/shop/32_currency.png"
- )
- )
- await _activate_pointer_control(sell_mode, ui_viewport)
- await process_frame
- assert(shop.visible and not player_menu.visible)
- assert(not shop.has_node("ShopPanel/Margin/Layout/Body/FishSales"))
- assert((shop.get_node("%ShopCoolerPage") as Control).visible)
- assert((shop.get_node("%ShopPanel") as Control).visible)
- assert(not (shop.get_node("ShopPanel/Margin/Layout/Body") as Control).visible)
- assert(not (shop.get_node("%Feedback") as Control).visible)
- var sell_inventory := shop.get("_sell_inventory") as ShopSellInventory
- assert(sell_inventory != null and sell_inventory.visible)
- var sell_tray_grid := sell_inventory.get("_tray_grid") as GridContainer
- assert(
- sell_tray_grid.columns == PlayerInventoryLayout.INVENTORY_COLUMNS
- )
- assert(
- sell_tray_grid.get_theme_constant("h_separation")
- == GeneralInventoryGrid.DEFAULT_SLOT_SEPARATION
- )
- assert(
- sell_inventory.get_parent() == shop.get_node("%ShopCoolerMount")
- )
- var staged: Dictionary = sell_inventory.get("_staged")
- sell_inventory.call(
- "_stage",
- PlayerInventoryLayout.EntryKind.CATCH,
- reserved_catch.catch_id,
- )
- assert(staged.is_empty())
- var sell_button := sell_inventory.get("_sell_button") as Button
- var all_button := sell_inventory.get("_all_button") as Button
- assert(sell_button != null and sell_button.text == "sell")
- assert(all_button != null and all_button.text == "all")
- assert(sell_button.get_parent() == all_button.get_parent())
- assert(
- sell_button.size_flags_horizontal == Control.SIZE_EXPAND_FILL
- and all_button.size_flags_horizontal == Control.SIZE_EXPAND_FILL
- )
- sell_inventory.call("_stage_all_eligible")
- assert(staged.has(PlayerInventoryLayout.catch_key(fish_catch.catch_id)))
- assert(not staged.has(
- PlayerInventoryLayout.catch_key(reserved_catch.catch_id)
- ))
- assert(all_button.disabled)
- assert((sell_inventory.get("_feedback") as Label).text.is_empty())
- var source_grid := sell_inventory.get("_inventory_grid") as GeneralInventoryGrid
- var staged_source_found := false
- for source_slot: GeneralInventorySlot in source_grid.get_slots():
- if source_slot.entry_identity == fish_catch.catch_id:
- staged_source_found = bool(source_slot.get("_staged"))
- break
- assert(staged_source_found)
- var expected_all_payout := int(sell_inventory.call("_calculate_total"))
- assert(expected_all_payout >= fish_catch.sale_value)
- var feedback := sell_inventory.get("_feedback") as Label
- var total_label := sell_inventory.get("_total_label") as Label
- var sale_buttons := sell_button.get_parent() as HBoxContainer
- assert(feedback.get_index() < total_label.get_parent().get_index())
- assert(total_label.get_parent().get_index() < sale_buttons.get_index())
- _sale_result.clear()
- sell_inventory.call("_submit_sale")
- await process_frame
- assert(not _sale_result.is_empty() and bool(_sale_result[1]))
- assert(not player.inventory.contains_catch_id(fish_catch.catch_id))
- assert(player.wallet.get_balance() == balance_before + expected_all_payout)
- assert(not sale_service.is_local_sale_pending())
- assert(reservations.release(reservation_id))
- await _activate_pointer_control(shop_tabs[0] as Button, ui_viewport)
- await process_frame
- assert(shop.visible and (shop.get_node("%ShopPanel") as Control).visible)
- assert(not (shop.get_node("%ShopCoolerPage") as Control).visible)
- assert(not sell_inventory.is_visible_in_tree())
- shop.close_shop()
- await shop.menu_visibility_changed
- assert(not shop.visible)
-
-
-func _assert_sale(
- player: Player,
- sale_service: NetworkSaleService,
- catch_ids: Array[StringName],
- expected_success: bool,
- expect_request_id: bool = true,
- buyer_id: StringName = NetworkSaleService.PELICAN_BUYER_ID,
-) -> void:
- var balance_before: int = player.wallet.get_balance()
- _sale_result.clear()
- var request_id: String = sale_service.request_local_sale(
- catch_ids, buyer_id
- )
- assert((not request_id.is_empty()) == expect_request_id)
- assert(not _sale_result.is_empty())
- assert(bool(_sale_result[1]) == expected_success)
- assert(not sale_service.is_local_sale_pending())
- if expected_success:
- var payout: int = int(_sale_result[4])
- assert(payout >= 0)
- assert(player.wallet.get_balance() == balance_before + payout)
- for catch_id: StringName in catch_ids:
- assert(not player.inventory.contains_catch_id(catch_id))
- else:
- assert(player.wallet.get_balance() == balance_before)
- for catch_id: StringName in catch_ids:
- assert(player.inventory.contains_catch_id(catch_id))
-
-
-func _make_catch(fish: FishData) -> FishCatch:
- var fish_catch := FishCatchType.new()
- fish_catch.fish = fish
- fish_catch.fish_id = fish.id
- fish_catch.weight_lb = fish.get_minimum_weight()
- fish_catch.display_scale = fish.get_display_scale_for_weight(
- fish_catch.weight_lb
- )
- fish_catch.sale_value = fish.get_sale_value_for_weight(
- fish_catch.weight_lb
- )
- fish_catch.ensure_identity()
- return fish_catch
-
-
-func _on_sale_finished(
- request_id: String,
- accepted: bool,
- message: String,
- catch_ids: Array[StringName],
- payout: int,
-) -> void:
- _sale_result = [request_id, accepted, message, catch_ids, payout]
-
-
-func _on_shop_finished(
- request_id: String,
- accepted: bool,
- message: String,
- product_id: StringName,
- category: int,
- quantity: int,
- total_cost: int,
-) -> void:
- _shop_result = [
- request_id,
- accepted,
- message,
- product_id,
- category,
- quantity,
- total_cost,
- ]
diff --git a/tests/economy_regression_validation.gd.uid b/tests/economy_regression_validation.gd.uid
deleted file mode 100644
index f10b0ad..0000000
--- a/tests/economy_regression_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b41oh3rfoejhw
diff --git a/tests/equipment_catalog_validation.gd b/tests/equipment_catalog_validation.gd
deleted file mode 100644
index bc549aa..0000000
--- a/tests/equipment_catalog_validation.gd
+++ /dev/null
@@ -1,109 +0,0 @@
-extends SceneTree
-
-const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
-const FishingShopType = preload("res://ui/fishing_shop.gd")
-const ItemCatalogType = preload("res://items/item_catalog.gd")
-const ItemDataType = preload("res://items/item_data.gd")
-const PlayerBagType = preload("res://inventory/player_bag.gd")
-const PlayerWalletType = preload("res://economy/player_wallet.gd")
-
-const ItemCatalogResource: ItemCatalogType = preload(
- "res://items/catalog/item_catalog.tres"
-)
-
-
-func _initialize() -> void:
- var shop := FishingShopType.new()
- assert(shop.call("_unlock_status", false) == "locked")
- assert(shop.call("_unlock_status", true) == "unlocked")
- var locked_button := Button.new()
- shop.call(
- "_add_unlock_state_icon", locked_button, false, Vector2(72.0, 72.0)
- )
- var locked_badge := locked_button.get_node(
- "UnlockStateBadge"
- ) as Panel
- var locked_icon := locked_badge.get_node(
- "UnlockStateIcon"
- ) as TextureRect
- assert(locked_badge.position == Vector2(4.0, 4.0))
- assert(locked_badge.size == Vector2(30.0, 30.0))
- assert(locked_icon.texture.resource_path.ends_with("/lock_light.png"))
- assert(locked_icon.size == Vector2(18.0, 18.0))
- assert(locked_icon.position == Vector2(6.0, 6.0))
- assert(is_equal_approx(locked_icon.modulate.a, 0.88))
- assert(not bool(locked_icon.get_meta(&"unlocked")))
- locked_button.free()
- var unlocked_button := Button.new()
- shop.call(
- "_add_unlock_state_icon", unlocked_button, true, Vector2(72.0, 72.0)
- )
- assert(unlocked_button.get_node_or_null("UnlockStateBadge") == null)
- unlocked_button.free()
- shop.free()
-
- var magnet: ItemDataType = ItemCatalogResource.get_available_item_by_id(
- &"magnet"
- )
- assert(magnet != null)
- assert(magnet.category == ItemDataType.Category.TOOL)
- assert(magnet.icon == null)
- assert(not magnet.usable)
- assert(not magnet.equippable)
- assert(not magnet.hotbar_allowed)
- assert(FishingShopStockType.get_price(&"magnet") == 250)
- assert(FishingShopStockType.get_stock_item_ids().has(&"magnet"))
- assert(FishingShopStockType.is_permanent_unlock(&"magnet", magnet))
- var fishing_net: ItemDataType = (
- ItemCatalogResource.get_available_item_by_id(&"fishing_net")
- )
- assert(fishing_net != null)
- assert(fishing_net.category == ItemDataType.Category.TOOL)
- assert(fishing_net.icon == null)
- assert(not fishing_net.usable)
- assert(not fishing_net.equippable)
- assert(not fishing_net.hotbar_allowed)
- assert(FishingShopStockType.get_price(&"fishing_net") == 250)
- assert(FishingShopStockType.get_stock_item_ids().has(&"fishing_net"))
- assert(FishingShopStockType.is_permanent_unlock(
- &"fishing_net", fishing_net
- ))
- assert(not FishingShopStockType.is_gathering_tool(&"fishing_net"))
- var crab_net: ItemDataType = ItemCatalogResource.get_available_item_by_id(
- &"crab_net"
- )
- assert(crab_net != null)
- assert(crab_net.icon != null)
- assert(crab_net.icon.resource_path.ends_with("/equipment/net.png"))
- var shovel: ItemDataType = ItemCatalogResource.get_available_item_by_id(
- &"standard_shovel"
- )
- assert(shovel != null)
- assert(shovel.category == ItemDataType.Category.TOOL)
- assert(shovel.icon != null)
- assert(shovel.icon.resource_path.ends_with("/equipment/shovel.png"))
- assert(shovel.equippable)
- assert(shovel.hotbar_allowed)
- assert(FishingShopStockType.get_price(&"standard_shovel") == 75)
- assert(FishingShopStockType.get_stock_item_ids().has(&"standard_shovel"))
- assert(FishingShopStockType.is_permanent_unlock(&"standard_shovel", shovel))
- assert(FishingShopStockType.is_gathering_tool(&"crab_net"))
- assert(FishingShopStockType.is_gathering_tool(&"standard_shovel"))
- assert(not FishingShopStockType.is_gathering_tool(&"basic_fishing_rod"))
-
- var wallet := PlayerWalletType.new()
- wallet.current_balance = 250
- var bag := PlayerBagType.new()
- bag.setup(ItemCatalogResource)
- assert(FishingShopStockType.purchase_one(
- &"magnet", wallet, bag, ItemCatalogResource
- ))
- assert(bag.owns_item(&"magnet"))
- assert(wallet.get_balance() == 0)
- assert(not FishingShopStockType.purchase_one(
- &"magnet", wallet, bag, ItemCatalogResource
- ))
- bag.free()
- wallet.free()
- print("Equipment catalog validation: PASS")
- quit()
diff --git a/tests/equipment_catalog_validation.gd.uid b/tests/equipment_catalog_validation.gd.uid
deleted file mode 100644
index b26406d..0000000
--- a/tests/equipment_catalog_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://1f2k3yrth7qn
diff --git a/tests/exported_decal_hotfix_validation.gd b/tests/exported_decal_hotfix_validation.gd
deleted file mode 100644
index 6560dd6..0000000
--- a/tests/exported_decal_hotfix_validation.gd
+++ /dev/null
@@ -1,163 +0,0 @@
-extends SceneTree
-
-const FEATURE_ASSET_ROOTS: Dictionary = {
- "eyes": "res://art/exported/characters/faces/eyes",
- "mouth": "res://art/exported/characters/faces/mouth",
- "nose": "res://art/exported/characters/faces/noses",
-}
-const FACIAL_FEATURE_SIZE_LIMIT: int = 512
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- CharacterCustomizationCatalog.refresh_feature_assets()
- assert(CharacterCustomizationCatalog._feature_textures.is_empty())
- assert(
- CharacterCustomizationCatalog._canonical_feature_filename(
- "eyes_alligator_eyes.png"
- ) == "eyes_alligator_eyes.png"
- )
- assert(
- CharacterCustomizationCatalog._canonical_feature_filename(
- "eyes_alligator_eyes.png.import"
- ) == "eyes_alligator_eyes.png"
- )
- assert(
- CharacterCustomizationCatalog._canonical_feature_filename(
- "eyes_alligator_eyes.png.remap"
- ) == "eyes_alligator_eyes.png"
- )
- assert(
- CharacterCustomizationCatalog._canonical_feature_filename(
- "eyes_alligator_eyes.jpg"
- ).is_empty()
- )
-
- var load_candidates: Array[Dictionary] = []
- for category_id: String in FEATURE_ASSET_ROOTS:
- var options := CharacterCustomizationCatalog.options_for(category_id)
- var expected_ids: Dictionary = _feature_ids_on_disk(category_id)
- assert(options.size() == expected_ids.size() + 1)
- var seen_ids: Dictionary = {}
- for option: Dictionary in options:
- var option_id := str(option.get("id", ""))
- assert(not seen_ids.has(option_id))
- seen_ids[option_id] = true
- if option_id != "none":
- var texture_path := (
- CharacterCustomizationCatalog.feature_texture_path(
- category_id, option_id
- )
- )
- assert(not texture_path.is_empty())
- assert(ResourceLoader.exists(texture_path, "Texture2D"))
- _validate_feature_import(texture_path)
- load_candidates.append({
- "category_id": category_id,
- "option_id": option_id,
- })
- for expected_id: String in expected_ids:
- assert(seen_ids.has(expected_id))
- assert(CharacterCustomizationCatalog._feature_textures.is_empty())
- assert(
- load_candidates.size()
- > CharacterCustomizationCatalog.FEATURE_TEXTURE_CACHE_LIMIT
- )
- for candidate_index: int in range(
- CharacterCustomizationCatalog.FEATURE_TEXTURE_CACHE_LIMIT + 1
- ):
- var candidate: Dictionary = load_candidates[candidate_index]
- assert(CharacterCustomizationCatalog.texture_for(
- str(candidate["category_id"]),
- str(candidate["option_id"]),
- ) != null)
- assert(
- CharacterCustomizationCatalog._feature_textures.size()
- == CharacterCustomizationCatalog.FEATURE_TEXTURE_CACHE_LIMIT
- )
- for mouth_id: String in ["open_ah", "open_oh", "open_smile"]:
- assert(CharacterCustomizationCatalog.texture_for(
- "mouth", mouth_id
- ) != null)
- assert(
- CharacterCustomizationCatalog._feature_textures.size()
- <= CharacterCustomizationCatalog.FEATURE_TEXTURE_CACHE_LIMIT
- )
-
- _validate_linux_arm64_texture_format()
- await _validate_incremental_profile_previews()
-
- print("Exported decal hotfix validation: PASS")
- quit()
-
-
-func _validate_feature_import(texture_path: String) -> void:
- var import_config := ConfigFile.new()
- assert(import_config.load(texture_path + ".import") == OK)
- assert(int(import_config.get_value("params", "compress/mode", -1)) == 2)
- assert(not bool(import_config.get_value(
- "params", "mipmaps/generate", true
- )))
- assert(int(import_config.get_value(
- "params", "process/size_limit", 0
- )) == FACIAL_FEATURE_SIZE_LIMIT)
- assert(int(import_config.get_value(
- "params", "detect_3d/compress_to", -1
- )) == 0)
-
-
-func _validate_linux_arm64_texture_format() -> void:
- var export_config := ConfigFile.new()
- assert(export_config.load("res://export_presets.cfg") == OK)
- assert(not bool(export_config.get_value(
- "preset.3.options", "texture_format/s3tc_bptc", true
- )))
- assert(bool(export_config.get_value(
- "preset.3.options", "texture_format/etc2_astc", false
- )))
-
-
-func _validate_incremental_profile_previews() -> void:
- var profile_page := ProfilePage.new()
- root.add_child(profile_page)
- await process_frame
- for category_id: String in CharacterCustomizationCatalog.FEATURE_CATEGORIES:
- var cached_before: int = (
- (profile_page.get("_feature_preview_cache") as Dictionary).size()
- )
- profile_page.call("_select_category", category_id)
- var initial_requests: int = (
- (profile_page.get("_feature_preview_requests") as Array).size()
- )
- assert(initial_requests > 3)
- for _frame: int in 4:
- await process_frame
- var cached_after: int = (
- (profile_page.get("_feature_preview_cache") as Dictionary).size()
- )
- assert(cached_after > cached_before)
- assert(cached_after - cached_before < initial_requests)
- profile_page.queue_free()
- for _frame: int in 8:
- await process_frame
-
-
-func _feature_ids_on_disk(category_id: String) -> Dictionary:
- var result: Dictionary = {}
- var root_path: String = str(FEATURE_ASSET_ROOTS[category_id])
- var directory := DirAccess.open(root_path)
- assert(directory != null)
- for file_name: String in directory.get_files():
- if not file_name.to_lower().ends_with(".png"):
- continue
- var option_id: String = (
- CharacterCustomizationCatalog._feature_id_from_filename(
- category_id, file_name
- )
- )
- if not option_id.is_empty():
- result[option_id] = true
- return result
diff --git a/tests/exported_decal_hotfix_validation.gd.uid b/tests/exported_decal_hotfix_validation.gd.uid
deleted file mode 100644
index e75742a..0000000
--- a/tests/exported_decal_hotfix_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dolsbknm52hsy
diff --git a/tests/file_dialog_controller_navigation_validation.gd b/tests/file_dialog_controller_navigation_validation.gd
deleted file mode 100644
index 7ac8df9..0000000
--- a/tests/file_dialog_controller_navigation_validation.gd
+++ /dev/null
@@ -1,348 +0,0 @@
-extends SceneTree
-
-const FileDialogControllerNavigationType = preload(
- "res://ui/file_dialog_controller_navigation.gd"
-)
-const InterfaceFontControllerType = preload(
- "res://ui/interface_font_controller.gd"
-)
-const OnScreenKeyboardType = preload("res://ui/on_screen_keyboard.gd")
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- root.size = Vector2i(1280, 720)
- var dialog := FileDialog.new()
- dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
- dialog.access = FileDialog.ACCESS_FILESYSTEM
- dialog.use_native_dialog = false
- root.add_child(dialog)
- dialog.current_dir = "/tmp"
- dialog.popup_centered(Vector2i(900, 540))
- for _frame: int in 3:
- await process_frame
- FileDialogControllerNavigationType.configure(dialog)
- await process_frame
-
- var root_scope: Window = (
- FileDialogControllerNavigationType.active_scope(dialog)
- )
- assert(root_scope == dialog)
- var root_controls: Array[Control] = (
- FileDialogControllerNavigationType.interactive_controls(root_scope)
- )
- var directory_list: ItemList
- var parent_button: Button
- var create_folder_button: Button
- var path_edit: LineEdit
- var drive_button: MenuButton
- var refresh_button: Button
- var favorite_button: Button
- var hidden_button: Button
- var grid_button: Button
- var list_button: Button
- var filter_button: Button
- var sort_button: MenuButton
- var cancel_button: Button
- var select_button: Button
- for control: Control in root_controls:
- if (
- control is ItemList
- and control.accessibility_name == "Directories & Files:"
- ):
- directory_list = control as ItemList
- if control is LineEdit and control.accessibility_name == "Path:":
- path_edit = control as LineEdit
- var button := control as Button
- if button != null:
- parent_button = (
- button
- if button.tooltip_text == "Go to parent folder."
- else parent_button
- )
- create_folder_button = (
- button
- if button.tooltip_text == "Create a new folder."
- else create_folder_button
- )
- refresh_button = (
- button
- if button.tooltip_text == "Refresh files."
- else refresh_button
- )
- favorite_button = (
- button
- if button.tooltip_text == "(Un)favorite current folder."
- else favorite_button
- )
- hidden_button = (
- button
- if button.tooltip_text == (
- "Toggle the visibility of hidden files."
- )
- else hidden_button
- )
- grid_button = (
- button
- if button.tooltip_text == (
- "View items as a grid of thumbnails."
- )
- else grid_button
- )
- list_button = (
- button
- if button.tooltip_text == "View items as a list."
- else list_button
- )
- filter_button = (
- button
- if button.tooltip_text == (
- "Toggle the visibility of the filter for file names."
- )
- else filter_button
- )
- cancel_button = button if button.text == "Cancel" else cancel_button
- select_button = (
- button
- if button.text == "Select Current Folder"
- else select_button
- )
- var menu := control as MenuButton
- if menu != null:
- drive_button = menu if menu.accessibility_name == "Drive" else drive_button
- sort_button = (
- menu if menu.tooltip_text == "Sort files" else sort_button
- )
- assert(directory_list != null)
- assert(parent_button != null)
- assert(path_edit != null)
- assert(create_folder_button != null)
- assert(drive_button != null)
- assert(refresh_button != null)
- assert(favorite_button != null)
- assert(hidden_button != null)
- assert(grid_button != null)
- assert(list_button != null)
- assert(filter_button != null)
- assert(sort_button != null)
- assert(cancel_button != null)
- assert(select_button != null)
- assert(dialog.gui_get_focus_owner() == directory_list)
- _assert_neighbor(path_edit, &"focus_neighbor_left", parent_button)
- _assert_neighbor(path_edit, &"focus_neighbor_right", drive_button)
- _assert_neighbor(drive_button, &"focus_neighbor_left", path_edit)
- _assert_neighbor(refresh_button, &"focus_neighbor_left", drive_button)
- _assert_neighbor(favorite_button, &"focus_neighbor_left", refresh_button)
- _assert_neighbor(
- create_folder_button, &"focus_neighbor_left", favorite_button
- )
- _assert_neighbor(hidden_button, &"focus_neighbor_right", grid_button)
- _assert_neighbor(grid_button, &"focus_neighbor_right", list_button)
- _assert_neighbor(list_button, &"focus_neighbor_right", filter_button)
- _assert_neighbor(filter_button, &"focus_neighbor_right", sort_button)
- _assert_neighbor(sort_button, &"focus_neighbor_top", create_folder_button)
- _assert_neighbor(
- create_folder_button, &"focus_neighbor_bottom", sort_button
- )
- _assert_neighbor(directory_list, &"focus_neighbor_top", hidden_button)
- _assert_neighbor(directory_list, &"focus_neighbor_bottom", select_button)
- _assert_neighbor(cancel_button, &"focus_neighbor_right", select_button)
- _assert_neighbor(select_button, &"focus_neighbor_left", cancel_button)
- assert(favorite_button.accessibility_name == "favorite current folder")
- assert(create_folder_button.accessibility_name == "new folder")
- assert(filter_button.accessibility_name == "filter file names")
- _assert_directionally_reachable(directory_list, root_controls)
- directory_list.clear()
- directory_list.add_item("test folder")
- directory_list.select(0)
- directory_list.grab_focus()
- assert(
- FileDialogControllerNavigationType.move_from_item_list(
- directory_list, Vector2.UP
- )
- )
- assert(dialog.gui_get_focus_owner() == hidden_button)
- directory_list.grab_focus()
- assert(
- FileDialogControllerNavigationType.move_from_item_list(
- directory_list, Vector2.DOWN
- )
- )
- assert(dialog.gui_get_focus_owner() == select_button)
- sort_button.grab_focus()
- var accept_press := InputEventAction.new()
- accept_press.action = &"ui_accept"
- accept_press.pressed = true
- Input.parse_input_event(accept_press)
- var accept_release := InputEventAction.new()
- accept_release.action = &"ui_accept"
- accept_release.pressed = false
- Input.parse_input_event(accept_release)
- await process_frame
- assert(sort_button.get_popup().visible)
- sort_button.get_popup().hide()
- await process_frame
-
- create_folder_button.pressed.emit()
- for _frame: int in 3:
- await process_frame
- FileDialogControllerNavigationType.configure(dialog)
- await process_frame
- var folder_scope: Window = (
- FileDialogControllerNavigationType.active_scope(dialog)
- )
- assert(folder_scope != null and folder_scope != dialog)
- var folder_controls: Array[Control] = (
- FileDialogControllerNavigationType.interactive_controls(folder_scope)
- )
- var folder_name_edit: LineEdit
- var folder_cancel: Button
- var folder_ok: Button
- for control: Control in folder_controls:
- if control is LineEdit:
- folder_name_edit = control as LineEdit
- var button := control as Button
- if button != null and button.text == "Cancel":
- folder_cancel = button
- if button != null and button.text == "OK":
- folder_ok = button
- assert(folder_name_edit != null)
- assert(folder_cancel != null)
- assert(folder_ok != null)
- assert(folder_scope.gui_get_focus_owner() == folder_name_edit)
- _assert_directionally_reachable(folder_name_edit, folder_controls)
-
- var keyboard := OnScreenKeyboardType.new()
- root.add_child(keyboard)
- keyboard.set_enabled(true)
- folder_name_edit.grab_focus()
- assert(keyboard.request_for_control(folder_name_edit))
- assert(keyboard.is_open())
- assert(keyboard.get_parent() == folder_scope)
- keyboard.call("_close_keyboard", true)
-
- var main_source: String = FileAccess.get_file_as_string(
- "res://main/main.gd"
- )
- assert(main_source.contains("picker_scope != _data_folder_dialog"))
- assert(main_source.contains("picker_scope.hide()"))
- assert(main_source.contains("is_controller_text_entry_open()"))
-
- dialog.queue_free()
- keyboard.queue_free()
- await process_frame
- await _validate_compact_dialog()
- print("File dialog controller navigation validation: PASS")
- quit()
-
-
-func _validate_compact_dialog() -> void:
- root.size = Vector2i(640, 480)
- var font_controller := InterfaceFontControllerType.new()
- root.add_child(font_controller)
- var keyboard := OnScreenKeyboardType.new()
- root.add_child(keyboard)
- await process_frame
- keyboard.set_enabled(false)
- font_controller.set_controller_text_entry_request(
- Callable(keyboard, "request_for_control"),
- Callable(keyboard, "is_open"),
- )
- var dialog := FileDialog.new()
- dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
- dialog.access = FileDialog.ACCESS_FILESYSTEM
- dialog.use_native_dialog = false
- root.add_child(dialog)
- dialog.current_dir = "/tmp"
- font_controller.popup_file_dialog(dialog)
- for _frame: int in 4:
- await process_frame
- assert(dialog.size.x <= 616 and dialog.size.y <= 456)
- var scope: Window = FileDialogControllerNavigationType.active_scope(dialog)
- var controls: Array[Control] = (
- FileDialogControllerNavigationType.interactive_controls(scope)
- )
- var directory_list: ItemList
- var path_edit: LineEdit
- var select_button: Button
- for control: Control in controls:
- if (
- control is ItemList
- and control.accessibility_name == "Directories & Files:"
- ):
- directory_list = control as ItemList
- if control is LineEdit and control.accessibility_name == "Path:":
- path_edit = control as LineEdit
- if control is Button and (control as Button).text == (
- "Select Current Folder"
- ):
- select_button = control as Button
- assert(directory_list != null)
- assert(path_edit != null)
- assert(select_button != null)
- _assert_directionally_reachable(directory_list, controls)
- directory_list.clear()
- directory_list.add_item("test folder")
- directory_list.select(0)
- directory_list.grab_focus()
- var down_event := InputEventJoypadButton.new()
- down_event.button_index = JOY_BUTTON_DPAD_DOWN
- down_event.pressed = true
- var dialog_controller := font_controller.get(
- "_file_dialog_controller"
- ) as FileDialogController
- dialog_controller.call("_input", down_event)
- assert(scope.gui_get_focus_owner() == select_button)
- path_edit.grab_focus()
- var accept_event := InputEventJoypadButton.new()
- accept_event.button_index = JOY_BUTTON_A
- accept_event.pressed = true
- dialog_controller.call("_input", accept_event)
- assert(keyboard.is_open())
- keyboard.call("_close_keyboard", true)
- dialog.queue_free()
- font_controller.queue_free()
- keyboard.queue_free()
- await process_frame
-
-
-func _assert_directionally_reachable(
- start: Control,
- controls: Array[Control],
-) -> void:
- var expected: Dictionary[int, bool] = {}
- for control: Control in controls:
- expected[control.get_instance_id()] = true
- var visited: Dictionary[int, bool] = {start.get_instance_id(): true}
- var pending: Array[Control] = [start]
- while not pending.is_empty():
- var current: Control = pending.pop_front()
- for path: NodePath in [
- current.focus_neighbor_left,
- current.focus_neighbor_right,
- current.focus_neighbor_top,
- current.focus_neighbor_bottom,
- ]:
- var neighbor := current.get_node_or_null(path) as Control
- if (
- neighbor == null
- or not expected.has(neighbor.get_instance_id())
- or visited.has(neighbor.get_instance_id())
- ):
- continue
- visited[neighbor.get_instance_id()] = true
- pending.append(neighbor)
- assert(visited.size() == expected.size())
-
-
-func _assert_neighbor(
- origin: Control,
- property_name: StringName,
- expected: Control,
-) -> void:
- var neighbor_path: NodePath = origin.get(property_name)
- var neighbor := origin.get_node_or_null(neighbor_path) as Control
- assert(neighbor == expected)
diff --git a/tests/file_dialog_controller_navigation_validation.gd.uid b/tests/file_dialog_controller_navigation_validation.gd.uid
deleted file mode 100644
index b2156bf..0000000
--- a/tests/file_dialog_controller_navigation_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bgnku8bs7oxx7
diff --git a/tests/fish_catalog_content_validation.gd b/tests/fish_catalog_content_validation.gd
deleted file mode 100644
index 3b31005..0000000
--- a/tests/fish_catalog_content_validation.gd
+++ /dev/null
@@ -1,631 +0,0 @@
-extends SceneTree
-
-const FishCatchType = preload("res://fish/fish_catch.gd")
-const CollectionLogType = preload("res://collection/collection_log.gd")
-const FishDataType = preload("res://fish/fish_data.gd")
-const FishInventoryType = preload("res://inventory/fish_inventory.gd")
-const FishPoolType = preload("res://fish/fish_pool.gd")
-const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
-const FishSelectorType = preload("res://fish/fish_selector.gd")
-const FishingContextType = preload("res://fishing/fishing_context.gd")
-const NetworkSaleServiceType = preload(
- "res://network/network_sale_service.gd"
-)
-const NetworkFishingServiceType = preload(
- "res://network/network_fishing_service.gd"
-)
-const FishingSpotType = preload("res://fishing/fishing_spot.gd")
-const CalendarSeasonType = preload("res://world/calendar_season.gd")
-
-const Catalog: FishPoolType = preload("res://fish/pools/fish_catalog.tres")
-const PondPool: FishPoolType = preload(
- "res://fish/pools/starter_pond_pool.tres"
-)
-const OceanPool: FishPoolType = preload(
- "res://fish/pools/starter_ocean_pool.tres"
-)
-const GeneratedPondPool: FishPoolType = preload(
- "res://fish/pools/generated_pond_pool.tres"
-)
-const GeneratedLakePool: FishPoolType = preload(
- "res://fish/pools/generated_lake_pool.tres"
-)
-const GeneratedRiverPool: FishPoolType = preload(
- "res://fish/pools/generated_river_pool.tres"
-)
-const PelicanBuyer = preload("res://economy/buyers/pelicans.tres")
-const StarterRegionScene = preload(
- "res://world/regions/starter_island_region.tscn"
-)
-const SharedFishPlaceholder: Texture2D = preload(
- "res://art/exported/creatures/fish_placeholder.png"
-)
-const FINAL_CREATURE_TEXTURE_PATHS: Dictionary[StringName, String] = {
- &"bowfin": "res://art/exported/creatures/101.png",
- &"sturgeon_lake": "res://art/exported/creatures/102.png",
- &"gar_longnose": "res://art/exported/creatures/103.png",
- &"paddlefish": "res://art/exported/creatures/104.png",
- &"sturgeon_shovelnose": "res://art/exported/creatures/105.png",
- &"gar_spotted": "res://art/exported/creatures/106.png",
- &"lungfish_west_african": "res://art/exported/creatures/107.png",
- &"mudskipper_atlantic": "res://art/exported/creatures/201.png",
- &"flounder_winter": "res://art/exported/creatures/403.png",
- &"mackerel_atlantic": "res://art/exported/creatures/701.png",
- &"mackerel_chub": "res://art/exported/creatures/704.png",
- &"sand_lance_american": "res://art/exported/creatures/801.png",
- &"menhaden_atlantic": "res://art/exported/creatures/802.png",
- &"anchovy_european": "res://art/exported/creatures/805.png",
- &"sardine_european": "res://art/exported/creatures/806.png",
- &"anchovy_northern": "res://art/exported/creatures/808.png",
- &"crab_brown": "res://art/exported/creatures/3906.png",
- &"salmon_atlantic": "res://art/exported/creatures/4401.png",
- &"mahi_mahi": "res://art/exported/creatures/4701.png",
- &"clownfish_ocellaris": "res://art/exported/creatures/5702.png",
- &"chromis_blue_green": "res://art/exported/creatures/5901.png",
- &"hogfish": "res://art/exported/creatures/6103.png",
- &"mullet_striped": "res://art/exported/creatures/6804.png",
- &"sheepshead": "res://art/exported/creatures/6905.png",
- &"milkfish": "res://art/exported/creatures/7101.png",
- &"ladyfish": "res://art/exported/creatures/7403.png",
- &"tilapia_nile": "res://art/exported/creatures/7506.png",
- &"betta_siamese": "res://art/exported/creatures/7508.png",
-}
-
-const ACTIVE_CATALOG_COUNT: int = 313
-const INACTIVE_CATALOG_COUNT: int = 3
-const FISHING_SPECIES_COUNT: int = 310
-const GENERATED_POND_COUNT: int = 33
-const GENERATED_LAKE_COUNT: int = 20
-const GENERATED_RIVER_COUNT: int = 53
-const STARTER_POND_COUNT: int = 106
-const OCEAN_COUNT: int = 204
-
-const ORIGINAL_IDS: Array[StringName] = [
- &"bluegill", &"bass", &"carp", &"sunfish",
-]
-const CATFISH_IDS: Array[StringName] = [
- &"catfish_blue",
- &"catfish_channel",
- &"catfish_flathead",
- &"catfish_white",
-]
-const FRESH_WATER_IDS: Array[StringName] = [
- &"bluegill", &"carp", &"catfish_blue", &"catfish_channel",
- &"catfish_flathead", &"catfish_white",
- &"goby_round",
-]
-const SALT_WATER_IDS: Array[StringName] = [
- &"bass",
- &"sunfish",
- &"tuna_albacore",
- &"tuna_bigeye",
- &"tuna_bluefin",
- &"tuna_skipjack",
- &"tuna_yellowfin",
- &"salmon_atlantic",
- &"salmon_chum",
- &"salmon_coho",
- &"salmon_pink",
- &"salmon_sockeye",
- &"anchovy_european", &"anchovy_northern",
- &"grouper_gulf", &"grouper_red",
- &"mackerel_atlantic", &"mackerel_cero", &"mackerel_chub",
- &"mackerel_king", &"mackerel_spanish",
- &"marlin_black", &"marlin_blue", &"marlin_white",
- &"pomfret_black", &"pomfret_chinese", &"pomfret_golden",
- &"pomfret_white", &"sailfish",
- &"snapper_lane", &"snapper_mangrove", &"snapper_mutton",
- &"snapper_red", &"swordfish",
-]
-const NEW_FRESH_WATER_IDS: Array[StringName] = [
- &"chub_european", &"chub_flame", &"chub_lake", &"goldfish",
- &"goldfish_bubbleeye", &"sauger", &"saugeye", &"trout_cutthroat",
- &"trout_golden", &"trout_rainbow", &"trout_steelhead", &"walleye",
- &"bowfin", &"sturgeon_lake", &"gar_longnose", &"paddlefish",
- &"sturgeon_shovelnose", &"gar_spotted", &"lungfish_west_african",
-]
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- _validate_catalog_and_pools()
- _validate_generated_habitat_pools()
- _validate_weight_based_display_scale()
- _validate_starter_water_bodies()
- _validate_authoritative_water_filter()
- _validate_catches_and_authoritative_sale()
- print("Fish catalog content validation: PASS")
- quit()
-
-
-func _validate_generated_habitat_pools() -> void:
- var expected_counts: Dictionary = {
- GeneratedPondPool: GENERATED_POND_COUNT,
- GeneratedLakePool: GENERATED_LAKE_COUNT,
- GeneratedRiverPool: GENERATED_RIVER_COUNT,
- }
- var freshwater_ids: Dictionary[StringName, bool] = {}
- for pool: FishPoolType in expected_counts:
- assert(pool.candidates.size() == int(expected_counts[pool]))
- for fish: FishDataType in pool.candidates:
- assert(fish != null and fish.is_fishable())
- assert(not freshwater_ids.has(fish.id))
- freshwater_ids[fish.id] = true
- assert(fish.is_allowed_in_water(WaterType.Type.FRESH_WATER))
- assert(freshwater_ids.size() == STARTER_POND_COUNT)
- assert(PondPool.candidates.size() == STARTER_POND_COUNT)
- for fish: FishDataType in PondPool.candidates:
- assert(freshwater_ids.has(fish.id))
- assert(OceanPool.candidates.size() == OCEAN_COUNT)
- var ocean_ids: Dictionary[StringName, bool] = {}
- for fish: FishDataType in OceanPool.candidates:
- assert(fish != null and fish.is_fishable())
- assert(not ocean_ids.has(fish.id))
- ocean_ids[fish.id] = true
- assert(fish.is_allowed_in_water(WaterType.Type.SALT_WATER))
- for fish: FishDataType in Catalog.candidates:
- if not fish.is_fishable():
- continue
- assert(freshwater_ids.has(fish.id) != ocean_ids.has(fish.id))
- assert(freshwater_ids.size() + ocean_ids.size() == FISHING_SPECIES_COUNT)
- assert(GeneratedPondPool.get_fish_by_id(&"gar_longnose") == null)
- assert(GeneratedRiverPool.get_fish_by_id(&"gar_longnose") != null)
- assert(GeneratedRiverPool.get_fish_by_id(&"goby_round") == null)
- assert(GeneratedLakePool.get_fish_by_id(&"goby_round") != null)
-
-
-func _validate_weight_based_display_scale() -> void:
- var previous_scale: float = 0.0
- for fish: FishDataType in Catalog.candidates:
- if not fish.active:
- continue
- assert(fish.is_selectable())
- assert(fish.weight_min_lb > 0.0)
- assert(fish.weight_max_lb <= 500000.0)
- var minimum_scale: float = fish.get_display_scale_for_weight(
- fish.weight_min_lb
- )
- var maximum_scale: float = fish.get_display_scale_for_weight(
- fish.weight_max_lb
- )
- assert(minimum_scale >= FishDataType.DISPLAY_MIN_SCALE)
- assert(maximum_scale <= FishDataType.DISPLAY_MAX_SCALE)
- assert(maximum_scale >= minimum_scale)
- var midpoint_weight: float = lerpf(
- fish.weight_min_lb,
- fish.weight_max_lb,
- 0.5,
- )
- assert(
- fish.get_display_scale_for_weight(midpoint_weight)
- >= minimum_scale
- )
- # A one-pound catch is a shared visual reference, not a per-species
- # texture-size decision.
- var reference_scale: float = fish.get_display_scale_for_weight(1.0)
- assert(is_equal_approx(reference_scale, FishDataType.DISPLAY_REFERENCE_SCALE))
- previous_scale = maxf(previous_scale, maximum_scale)
- assert(previous_scale <= FishDataType.DISPLAY_MAX_SCALE)
-
-
-func _validate_catalog_and_pools() -> void:
- assert(Catalog.candidates.size() == 316)
- assert(PondPool.candidates.size() == STARTER_POND_COUNT)
- assert(OceanPool.candidates.size() == OCEAN_COUNT)
- var active_count: int = 0
- var inactive_count: int = 0
- var fishing_count: int = 0
- var final_art_count: int = 0
- var placeholder_fishing_count: int = 0
- var catalog_numbers: Dictionary[int, bool] = {}
- for fish: FishDataType in Catalog.candidates:
- assert(fish != null and not fish.id.is_empty())
- assert(fish.catalog_number > 0)
- assert(not fish.collection_group.is_empty())
- assert(not catalog_numbers.has(fish.catalog_number))
- catalog_numbers[fish.catalog_number] = true
- assert(not fish.logbook_fact.strip_edges().is_empty())
- assert(CalendarSeasonType.is_valid_mask(fish.available_seasons))
- assert(not fish.get_season_text().is_empty())
- if fish.active:
- active_count += 1
- assert(fish.is_selectable())
- assert(fish.display_texture != null)
- else:
- inactive_count += 1
- assert(not fish.is_selectable())
- assert(fish.display_texture == null)
- if FINAL_CREATURE_TEXTURE_PATHS.has(fish.id):
- var expected_texture: Texture2D = load(
- FINAL_CREATURE_TEXTURE_PATHS[fish.id]
- ) as Texture2D
- assert(expected_texture != null)
- assert(fish.display_texture == expected_texture)
- final_art_count += 1
- if fish.collection_method == FishDataType.CollectionMethod.FISHING:
- fishing_count += 1
- assert(fish.active)
- if not FINAL_CREATURE_TEXTURE_PATHS.has(fish.id):
- assert(fish.display_texture == SharedFishPlaceholder)
- placeholder_fishing_count += 1
- assert(active_count == ACTIVE_CATALOG_COUNT)
- assert(inactive_count == INACTIVE_CATALOG_COUNT)
- assert(fishing_count == FISHING_SPECIES_COUNT)
- assert(final_art_count == FINAL_CREATURE_TEXTURE_PATHS.size())
- assert(placeholder_fishing_count == 283)
- var chum: FishDataType = Catalog.get_fish_by_id(&"salmon_chum")
- assert(chum != null)
- assert(chum.get_season_text() == "fall")
- assert(
- chum.is_available_in_season(CalendarSeasonType.Season.FALL)
- )
- assert(
- not chum.is_available_in_season(CalendarSeasonType.Season.SUMMER)
- )
- var chum_pool := FishPoolType.new()
- chum_pool.candidates = [chum]
- var seasonal_collection := CollectionLogType.new()
- var seasonal_selector := FishSelectorType.new()
- seasonal_selector.use_deterministic_test_seed = true
- var summer_context := FishingContextType.new()
- summer_context.water_type = WaterType.Type.SALT_WATER
- summer_context.season = CalendarSeasonType.Season.SUMMER
- seasonal_selector.begin_roll()
- assert(
- seasonal_selector.select_fish(
- chum_pool,
- summer_context,
- seasonal_collection,
- ) == null
- )
- var fall_context := FishingContextType.new()
- fall_context.water_type = WaterType.Type.SALT_WATER
- fall_context.season = CalendarSeasonType.Season.FALL
- seasonal_selector.begin_roll()
- assert(
- seasonal_selector.select_fish(
- chum_pool,
- fall_context,
- seasonal_collection,
- ) == chum
- )
- seasonal_collection.free()
- var inactive_fish: FishDataType = Catalog.get_fish_by_id(&"crab_blue")
- assert(inactive_fish != null and not inactive_fish.active)
- var inactive_pool := FishPoolType.new()
- inactive_pool.candidates = [inactive_fish]
- var inactive_context := FishingContextType.new()
- inactive_context.water_type = WaterType.Type.SALT_WATER
- var inactive_collection := CollectionLogType.new()
- var inactive_selector := FishSelectorType.new()
- inactive_selector.use_deterministic_test_seed = true
- inactive_selector.begin_roll()
- assert(
- inactive_selector.select_fish(
- inactive_pool,
- inactive_context,
- inactive_collection,
- ) == null
- )
- assert(inactive_selector.create_catch(inactive_fish) == null)
- inactive_collection.free()
- for fish_id: StringName in ORIGINAL_IDS:
- var original_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
- assert(original_fish != null)
- assert(original_fish.is_selectable())
- for fish_id: StringName in FRESH_WATER_IDS:
- var fresh_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
- assert(PondPool.get_fish_by_id(fish_id) == fresh_fish)
- assert(OceanPool.get_fish_by_id(fish_id) == null)
- assert(fresh_fish.is_allowed_in_water(WaterType.Type.FRESH_WATER))
- assert(not fresh_fish.is_allowed_in_water(WaterType.Type.SALT_WATER))
- assert(
- LogbookCatalog.category_for(fresh_fish)
- == LogbookCatalog.Category.FRESH_WATER
- )
- for fish_id: StringName in SALT_WATER_IDS:
- var salt_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
- assert(OceanPool.get_fish_by_id(fish_id) == salt_fish)
- assert(PondPool.get_fish_by_id(fish_id) == null)
- assert(salt_fish.is_allowed_in_water(WaterType.Type.SALT_WATER))
- assert(not salt_fish.is_allowed_in_water(WaterType.Type.FRESH_WATER))
- assert(
- LogbookCatalog.category_for(salt_fish)
- == LogbookCatalog.Category.SALT_WATER
- )
- for fish_id: StringName in CATFISH_IDS:
- var fish: FishDataType = Catalog.get_fish_by_id(fish_id)
- assert(fish != null)
- assert(fish.is_selectable())
- assert(PondPool.get_fish_by_id(fish_id) == fish)
- assert(OceanPool.get_fish_by_id(fish_id) == null)
- var expected_tags: Array[StringName] = []
- if fish_id in [&"catfish_channel", &"catfish_white"]:
- expected_tags.assign([&"starter_pond", &"pond"])
- else:
- expected_tags.append(&"river")
- assert(fish.availability.allowed_location_tags == expected_tags)
- assert(
- LogbookCatalog.category_for(fish)
- == LogbookCatalog.Category.FRESH_WATER
- )
- for fish_id: StringName in NEW_FRESH_WATER_IDS:
- var fish: FishDataType = Catalog.get_fish_by_id(fish_id)
- assert(fish != null and fish.is_selectable())
- assert(PondPool.get_fish_by_id(fish_id) == fish)
- assert(OceanPool.get_fish_by_id(fish_id) == null)
- assert(
- LogbookCatalog.category_for(fish)
- == LogbookCatalog.Category.FRESH_WATER
- )
-
- var expected_values: Dictionary[StringName, Array] = {
- &"catfish_blue": [1, 1.25, 3.0, 12.0, 6, 9],
- &"catfish_channel": [0, 2.0, 1.5, 7.0, 4, 7],
- &"catfish_flathead": [1, 1.0, 3.0, 14.0, 7, 10],
- &"catfish_white": [0, 2.0, 1.0, 5.0, 4, 6],
- }
- for fish_id: StringName in CATFISH_IDS:
- var fish: FishDataType = Catalog.get_fish_by_id(fish_id)
- var values: Array = expected_values[fish_id]
- assert(int(fish.rarity) == int(values[0]))
- assert(is_equal_approx(fish.base_catch_weight, float(values[1])))
- assert(is_equal_approx(fish.weight_min_lb, float(values[2])))
- assert(is_equal_approx(fish.weight_max_lb, float(values[3])))
- assert(fish.sell_value_min == int(values[4]))
- assert(fish.sell_value_max == int(values[5]))
-
- var expected_tuna_values: Dictionary[StringName, Array] = {
- &"tuna_albacore": [2, 1.5, 10.0, 45.0, 12, 24],
- &"tuna_bigeye": [3, 0.55, 20.0, 120.0, 22, 48],
- &"tuna_bluefin": [4, 0.18, 30.0, 250.0, 35, 90],
- &"tuna_skipjack": [1, 3.0, 5.0, 25.0, 8, 16],
- &"tuna_yellowfin": [2, 1.0, 15.0, 80.0, 16, 36],
- }
- for fish_id: StringName in expected_tuna_values:
- var fish: FishDataType = Catalog.get_fish_by_id(fish_id)
- var values: Array = expected_tuna_values[fish_id]
- assert(fish != null and fish.is_selectable())
- assert(int(fish.rarity) == int(values[0]))
- assert(is_equal_approx(fish.base_catch_weight, float(values[1])))
- assert(is_equal_approx(fish.weight_min_lb, float(values[2])))
- assert(is_equal_approx(fish.weight_max_lb, float(values[3])))
- assert(fish.sell_value_min == int(values[4]))
- assert(fish.sell_value_max == int(values[5]))
-
- var expected_new_values: Dictionary[StringName, Array] = {
- &"goby_round": [0, 8.0, 0.1, 0.6, 2, 3],
- &"salmon_atlantic": [3, 0.65, 8.0, 40.0, 20, 42],
- &"salmon_chum": [1, 2.5, 6.0, 25.0, 10, 22],
- &"salmon_coho": [2, 1.4, 5.0, 30.0, 14, 28],
- &"salmon_pink": [1, 3.0, 3.0, 12.0, 8, 15],
- &"salmon_sockeye": [2, 1.6, 4.0, 15.0, 12, 24],
- }
- for fish_id: StringName in expected_new_values:
- var fish: FishDataType = Catalog.get_fish_by_id(fish_id)
- var values: Array = expected_new_values[fish_id]
- assert(fish != null and fish.is_selectable())
- assert(int(fish.rarity) == int(values[0]))
- assert(is_equal_approx(fish.base_catch_weight, float(values[1])))
- assert(is_equal_approx(fish.weight_min_lb, float(values[2])))
- assert(is_equal_approx(fish.weight_max_lb, float(values[3])))
- assert(fish.sell_value_min == int(values[4]))
- assert(fish.sell_value_max == int(values[5]))
-
- var pond_context := FishingContextType.new()
- pond_context.location_tags = [&"starter_pond"]
- pond_context.water_type = WaterType.Type.FRESH_WATER
- var ocean_context := FishingContextType.new()
- ocean_context.location_tags = [&"coast", &"ocean"]
- ocean_context.water_type = WaterType.Type.SALT_WATER
- for fish_id: StringName in CATFISH_IDS:
- var fish: FishDataType = Catalog.get_fish_by_id(fish_id)
- var day_habitat_context := FishingContextType.new()
- if fish_id in [&"catfish_channel", &"catfish_white"]:
- day_habitat_context.location_tags.append(&"pond")
- else:
- day_habitat_context.location_tags.append(&"river")
- day_habitat_context.water_type = WaterType.Type.FRESH_WATER
- var night_habitat_context := FishingContextType.new()
- night_habitat_context.location_tags = (
- day_habitat_context.location_tags.duplicate()
- )
- night_habitat_context.water_type = WaterType.Type.FRESH_WATER
- night_habitat_context.is_night = true
- assert(not fish.availability.is_available(day_habitat_context))
- assert(fish.availability.is_available(night_habitat_context))
- assert(not fish.availability.is_available(ocean_context))
- var single_species_pool := FishPoolType.new()
- single_species_pool.candidates = [fish]
- var collection := CollectionLogType.new()
- var selector := FishSelectorType.new()
- selector.use_deterministic_test_seed = true
- selector.begin_roll()
- assert(
- selector.select_fish(
- single_species_pool, night_habitat_context, collection
- ) == fish
- )
- collection.free()
- for fish_id: StringName in SALT_WATER_IDS:
- var fish: FishDataType = Catalog.get_fish_by_id(fish_id)
- var single_species_pool := FishPoolType.new()
- single_species_pool.candidates = [fish]
- var collection := CollectionLogType.new()
- var selector := FishSelectorType.new()
- selector.use_deterministic_test_seed = true
- selector.begin_roll()
- assert(
- selector.select_fish(single_species_pool, pond_context, collection)
- == null
- )
- assert(
- selector.select_fish(single_species_pool, ocean_context, collection)
- == fish
- )
- collection.free()
-
-
-func _validate_authoritative_water_filter() -> void:
- var fishing_spot := FishingSpotType.new()
- var service := NetworkFishingServiceType.new()
- service.set("_fish_catalog", Catalog)
- service.set("_fishing_spot", fishing_spot)
- var pond_region := FishableWaterRegion.new()
- pond_region.water_type = WaterType.Type.FRESH_WATER
- pond_region.fish_pool = PondPool
- pond_region.location_tags = [&"starter_pond"]
- var ocean_region := FishableWaterRegion.new()
- ocean_region.water_type = WaterType.Type.SALT_WATER
- ocean_region.fish_pool = OceanPool
- ocean_region.location_tags = [&"coast", &"ocean"]
- var stale_salt_pool_region := FishableWaterRegion.new()
- stale_salt_pool_region.water_type = WaterType.Type.FRESH_WATER
- var salt_only_pool := FishPoolType.new()
- salt_only_pool.candidates = [Catalog.get_fish_by_id(&"bass")]
- stale_salt_pool_region.fish_pool = salt_only_pool
- stale_salt_pool_region.location_tags = [&"starter_pond"]
- var stale_fresh_pool_region := FishableWaterRegion.new()
- stale_fresh_pool_region.water_type = WaterType.Type.SALT_WATER
- var fresh_only_pool := FishPoolType.new()
- fresh_only_pool.candidates = [Catalog.get_fish_by_id(&"bluegill")]
- stale_fresh_pool_region.fish_pool = fresh_only_pool
- stale_fresh_pool_region.location_tags = [&"coast", &"ocean"]
- var evidence := {"discovered_fish_ids": []}
- assert(
- service.call(
- "_select_authoritative_fish", pond_region, evidence, null, null
- )
- != null
- )
- assert(
- service.call(
- "_select_authoritative_fish", ocean_region, evidence, null, null
- )
- != null
- )
- assert(
- service.call(
- "_select_authoritative_fish",
- stale_salt_pool_region,
- evidence,
- null,
- null,
- ) == null
- )
- assert(
- service.call(
- "_select_authoritative_fish",
- stale_fresh_pool_region,
- evidence,
- null,
- null,
- ) == null
- )
- service.free()
- fishing_spot.free()
- pond_region.free()
- ocean_region.free()
- stale_salt_pool_region.free()
- stale_fresh_pool_region.free()
-
-
-func _validate_starter_water_bodies() -> void:
- var region := StarterRegionScene.instantiate()
- var pond := region.get_node("WaterBodies/Pond/FishingRegion") as FishableWaterRegion
- var ocean := region.get_node(
- "WaterBodies/Ocean/FishingRegions/OceanFishingRegion"
- ) as FishableWaterRegion
- assert(pond.water_type == WaterType.Type.FRESH_WATER)
- assert(ocean.water_type == WaterType.Type.SALT_WATER)
- assert(pond.fish_pool == PondPool)
- assert(ocean.fish_pool == OceanPool)
- assert(region.get_node_or_null("ShorelineRibbons/Pond") == null)
- var shoreline_geometry := (
- region.get_node_or_null("ShorelineRibbons/Ocean") as MeshInstance3D
- )
- assert(shoreline_geometry != null)
- assert(not shoreline_geometry.visible)
- assert(shoreline_geometry.material_override == null)
- var pond_material := (
- region.get_node("WaterBodies/Pond/VisualWater") as MeshInstance3D
- ).material_override as ShaderMaterial
- var ocean_material := (
- region.get_node("WaterBodies/Ocean/VisualWater") as MeshInstance3D
- ).material_override as ShaderMaterial
- assert(not bool(pond_material.get_shader_parameter("tide_effect_enabled")))
- assert(bool(ocean_material.get_shader_parameter("tide_effect_enabled")))
- region.free()
-
-
-func _validate_catches_and_authoritative_sale() -> void:
- var inventory := FishInventoryType.new()
- root.add_child(inventory)
- var selector := FishSelectorType.new()
- selector.use_deterministic_test_seed = true
- selector.begin_roll()
- var sale_service := NetworkSaleServiceType.new()
- var session := NetworkSession.new()
- root.add_child(session)
- root.add_child(sale_service)
- sale_service.set("_session", session)
- sale_service.set("_fish_catalog", Catalog)
- var sale_buyers: Dictionary[StringName, FishBuyerProfileType] = {
- PelicanBuyer.id: PelicanBuyer,
- }
- sale_service.set("_buyers", sale_buyers)
-
- var catch_sequence: int = 0
- for fish: FishDataType in Catalog.candidates:
- if not fish.active:
- continue
- catch_sequence += 1
- var fish_catch: FishCatch = selector.create_catch(fish)
- assert(fish_catch != null)
- fish_catch.catch_sequence = catch_sequence
- assert(fish_catch.fish.display_texture == fish.display_texture)
-
- var loaded: FishCatch = FishCatchType.from_save_dict(
- fish_catch.to_save_dict(), Catalog.get_fish_by_id(fish.id)
- )
- assert(loaded != null)
- assert(loaded.fish_id == fish.id)
- assert(loaded.fish == fish)
- assert(
- is_equal_approx(
- loaded.display_scale,
- fish.get_display_scale_for_weight(loaded.weight_lb),
- )
- )
-
- var replicated: FishCatch = FishCatchType.from_network_dict(
- fish_catch.to_network_dict(), Catalog.get_fish_by_id(fish.id)
- )
- assert(replicated != null)
- assert(replicated.fish_id == fish.id)
- assert(replicated.fish.display_texture == fish.display_texture)
- assert(
- is_equal_approx(
- replicated.display_scale,
- fish.get_display_scale_for_weight(replicated.weight_lb),
- )
- )
-
- inventory.add_catch(loaded)
- assert(inventory.contains_catch_id(loaded.catch_id))
- var sale_result: Dictionary = sale_service.call(
- "_build_authoritative_result",
- 1,
- "catalog_sale_%d" % catch_sequence,
- [loaded.to_network_dict()],
- [],
- PelicanBuyer,
- )
- assert(bool(sale_result.get("accepted", false)))
- assert(int(sale_result.get("base_value", -1)) == loaded.sale_value)
- assert((sale_result.get("catch_ids", []) as Array).size() == 1)
-
- inventory.queue_free()
- sale_service.queue_free()
- session.queue_free()
diff --git a/tests/fish_catalog_content_validation.gd.uid b/tests/fish_catalog_content_validation.gd.uid
deleted file mode 100644
index 15244b8..0000000
--- a/tests/fish_catalog_content_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dmocxxio0u53e
diff --git a/tests/fish_hotbar_showcase_validation.gd b/tests/fish_hotbar_showcase_validation.gd
deleted file mode 100644
index af3644b..0000000
--- a/tests/fish_hotbar_showcase_validation.gd
+++ /dev/null
@@ -1,239 +0,0 @@
-extends SceneTree
-
-const MainScene = preload("res://main/main.tscn")
-const FishCatchType = preload("res://fish/fish_catch.gd")
-const FishDataType = preload("res://fish/fish_data.gd")
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- root.size = Vector2i(1280, 720)
- var main := MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- var session := main.get_node("%NetworkSession") as NetworkSession
- assert(session.start_private_host(17977))
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(save_manager.initialize_new_game())
- main.call("_enter_gameplay")
- for _frame: int in 8:
- await process_frame
-
- var player := main.get("_player") as Player
- var world_time := main.get_node("%WorldTimeService") as WorldTimeService
- var world_weather := (
- main.get_node("%WorldWeatherService") as WorldWeatherService
- )
- world_time.synchronize_time(19.75)
- assert(player.experience.award_experience(125))
- var fish_catalog := main.get("fish_catalog") as FishPool
- var service := main.get_node(
- "%NetworkFishShowcaseService"
- ) as NetworkFishShowcaseService
- assert(player != null and fish_catalog != null and service != null)
- var fish: FishDataType = fish_catalog.get_fish_by_id(&"bluegill")
- assert(fish != null)
- var fish_catch := FishCatchType.new()
- fish_catch.fish = fish
- fish_catch.fish_id = fish.id
- fish_catch.weight_lb = fish.get_minimum_weight()
- fish_catch.display_scale = fish.get_display_scale_for_weight(
- fish_catch.weight_lb
- )
- fish_catch.quality = FishQuality.Tier.EXCEPTIONAL
- fish_catch.sale_value = FishQuality.apply_sale_value(
- fish.get_sale_value_for_weight(fish_catch.weight_lb),
- fish_catch.quality,
- )
- fish_catch.ensure_identity()
- player.inventory.add_catch(fish_catch)
- player.collection_log.record_catch(
- fish_catch.fish_id,
- fish_catch.quality,
- )
- var game_ui := main.get_node("%GameUI") as GameUI
- var hotbar_ui := game_ui.get_node("%Hotbar") as HotbarUI
- assert(hotbar_ui != null)
- hotbar_ui.set_drag_enabled(true)
- var slots: Array = hotbar_ui.get("_slots")
- var fish_payload: Dictionary = {
- "kind": "cooler_fish",
- "catch_id": String(fish_catch.catch_id),
- }
- assert(slots.size() == PlayerHotbar.SLOT_COUNT)
- assert(bool(slots[1].call("_can_drop_data", Vector2.ZERO, fish_payload)))
- slots[1].call("_drop_data", Vector2.ZERO, fish_payload)
- assert(player.hotbar.get_fish_catch_id(1) == fish_catch.catch_id)
- assert(player.hotbar.select_slot(1))
- assert(
- player.hotbar.get_selected_assignment_kind()
- == PlayerHotbar.AssignmentKind.FISH
- )
- assert(player.hotbar.get_selected_fish_catch_id() == fish_catch.catch_id)
- assert(player.hotbar.get_selected_item_id().is_empty())
-
- assert(service.is_local_showcase_visible())
- assert(service.get_local_showcase_catch_id() == fish_catch.catch_id)
- await _wait_for_held_fish_visibility(player, true)
- assert(service.toggle_selected_fish())
- await process_frame
- assert(not service.is_local_showcase_visible())
- await _wait_for_held_fish_visibility(player, false)
- var saved_weather: WorldWeatherService.Weather = (
- world_weather.get_persistent_weather()
- )
- var saved_weather_seconds: float = (
- world_weather.get_persistent_seconds_remaining()
- )
- var saved_time_hours: float = world_time.get_persistent_time_hours()
-
- assert(save_manager.save_now())
- var save_path: String = str(save_manager.get("_save_path"))
- var decoded: Dictionary = ProgressionSaveCodec.read_local_save(save_path)
- assert(bool(decoded.get("ok", false)))
- var parsed: Dictionary = decoded["data"]
- var hotbar_data: Dictionary = parsed["hotbar"]
- assert(typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY)
- assert(str((hotbar_data["fish_slots"] as Array)[1]) == fish_catch.catch_id)
- assert(int(parsed["save_version"]) == 11)
- assert(not PlayerHomeState.sanitize_save_data(
- parsed.get("home", {})
- ).is_empty())
- assert(
- int(parsed["experience"]["total_experience"])
- == 125
- )
- assert(absf(
- float(parsed["world"]["time_hours"]) - saved_time_hours
- ) < 0.01)
- assert(
- int(parsed["world"]["weather"])
- == int(saved_weather)
- )
- assert(absf(
- float(
- parsed["world"][
- "weather_seconds_remaining"
- ]
- ) - saved_weather_seconds
- ) < 0.1)
- var saved_catches: Array = (parsed as Dictionary)["inventory"]["catches"]
- assert(int((saved_catches[0] as Dictionary)["quality"]) == fish_catch.quality)
- var saved_masks: Dictionary = (
- (parsed as Dictionary)["collection"]["discovered_quality_masks"]
- )
- assert(
- int(saved_masks[String(fish.id)])
- == FishQuality.bit_for(FishQuality.Tier.EXCEPTIONAL)
- )
- var saved_catch_counts: Dictionary = (
- (parsed as Dictionary)["collection"]["catch_counts"]
- )
- assert(int(saved_catch_counts[String(fish.id)]) == 1)
- var legacy_without_counts: Dictionary = parsed.duplicate(true)
- (legacy_without_counts["collection"] as Dictionary).erase("catch_counts")
- var legacy_snapshot: RefCounted = save_manager.call(
- "_build_load_snapshot",
- legacy_without_counts,
- )
- assert(legacy_snapshot != null)
- var recovered_counts: Dictionary = legacy_snapshot.get("catch_counts")
- assert(int(recovered_counts.get(fish.id, 0)) == 1)
-
- assert(player.hotbar.clear_slot(1))
- assert(player.experience.restore_total_experience(0))
- world_time.synchronize_time(8.0)
- world_weather.clear_daily_plan()
- world_weather.apply_authoritative_snapshot(
- WorldWeatherService.Weather.FOGGY,
- 10.0,
- )
- assert(save_manager.load_player_data())
- assert(player.experience.get_total_experience() == 125)
- # Loading a save must not replace the active RTC-authoritative clock.
- assert(absf(world_time.get_time_hours() - 8.0) < 0.01)
- assert(world_weather.get_persistent_weather() == saved_weather)
- assert(absf(
- world_weather.get_persistent_seconds_remaining()
- - saved_weather_seconds
- ) < 2.0)
- assert(player.experience.get_level() == 2)
- assert(player.hotbar.get_fish_catch_id(1) == fish_catch.catch_id)
- assert(player.hotbar.get_selected_slot() == 1)
- assert(
- player.collection_log.has_discovered_quality(
- fish.id,
- FishQuality.Tier.EXCEPTIONAL,
- )
- )
- assert(player.collection_log.get_catch_count(fish.id) == 1)
- assert(service.is_local_showcase_visible())
- await _wait_for_held_fish_visibility(player, true)
- assert(player.inventory.remove_catch_by_id(fish_catch.catch_id) != null)
- await process_frame
- assert(player.hotbar.get_fish_catch_id(1).is_empty())
- assert(not service.is_local_showcase_visible())
- await _wait_for_held_fish_visibility(player, false)
-
- var valid_state: Dictionary = {
- "session_id": "session",
- "owner_peer_id": 2,
- "visible": true,
- "fish_id": String(fish.id),
- "weight_lb": fish.get_minimum_weight(),
- "display_scale": fish.get_display_scale_for_weight(
- fish.get_minimum_weight()
- ),
- "quality": FishQuality.Tier.BORING,
- "revision": 1,
- }
- assert(NetworkFishShowcaseProtocol.validate_state(valid_state))
- var invalid_state: Dictionary = valid_state.duplicate(true)
- invalid_state["display_scale"] = 1000.0
- assert(not NetworkFishShowcaseProtocol.validate_state(invalid_state))
- assert(NetworkProtocol.PROTOCOL_VERSION == 12)
- assert(NetworkProtocol.ENET_CHANNEL_COUNT == 18)
- assert(
- NetworkProtocol.FISH_QUALITY_CAPABILITY
- == "fish_quality_v1"
- )
- assert(
- NetworkFishShowcaseProtocol.CAPABILITY
- == &"fish_showcase_v1"
- )
-
- print("Fish hotbar showcase validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _wait_for_held_fish_visibility(
- player: Player,
- expected_visible: bool,
-) -> void:
- var held_fish_display := player.get("_held_fish_display") as Node3D
- assert(held_fish_display != null)
- var deadline_msec: int = Time.get_ticks_msec() + 2000
- while (
- held_fish_display.visible != expected_visible
- and Time.get_ticks_msec() < deadline_msec
- ):
- await process_frame
- assert(
- held_fish_display.visible == expected_visible,
- "Held fish display did not become %s within the timeout."
- % expected_visible,
- )
diff --git a/tests/fish_hotbar_showcase_validation.gd.uid b/tests/fish_hotbar_showcase_validation.gd.uid
deleted file mode 100644
index 3fe842e..0000000
--- a/tests/fish_hotbar_showcase_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cpnfrron1g65q
diff --git a/tests/fish_quality_validation.gd b/tests/fish_quality_validation.gd
deleted file mode 100644
index 99a2169..0000000
--- a/tests/fish_quality_validation.gd
+++ /dev/null
@@ -1,515 +0,0 @@
-extends SceneTree
-
-const FishCatchType = preload("res://fish/fish_catch.gd")
-const FishQualityType = preload("res://fish/fish_quality.gd")
-const FishSelectorType = preload("res://fish/fish_selector.gd")
-const CollectionLogType = preload("res://collection/collection_log.gd")
-const NetworkSaleServiceType = preload(
- "res://network/network_sale_service.gd"
-)
-const Catalog: FishPool = preload("res://fish/pools/fish_catalog.tres")
-const PelicanBuyer: FishBuyerProfile = preload(
- "res://economy/buyers/pelicans.tres"
-)
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- _validate_tiers_and_distribution()
- _validate_barrier_challenge_curve()
- _validate_fight_pacing_and_reel_upgrades()
- _validate_catch_round_trip_and_sale()
- _validate_mail_round_trip()
- _validate_collection_mastery()
- _validate_version_four_migration()
- _validate_fishing_protocol_v2()
- assert(NetworkProtocol.PROTOCOL_VERSION == 12)
- assert(NetworkProtocol.ENET_CHANNEL_COUNT == 18)
- assert(NetworkProtocol.MOVEMENT_ANIMATION_CHANNEL == 11)
- assert(
- NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY
- == "movement_reconciliation_v2"
- )
- assert(
- NetworkProtocol.FISHING_REPLICATION_CAPABILITY
- == "fishing_replication_v2"
- )
- assert(NetworkProtocol.FISH_QUALITY_CAPABILITY == "fish_quality_v1")
- print("Fish quality validation: PASS")
- quit()
-
-
-func _validate_fishing_protocol_v2() -> void:
- var request: Dictionary = {
- "request_id": "cast-1",
- "session_id": "session-1",
- "origin": [0.0, 1.0, 0.0],
- "target": [0.0, 0.0, -4.0],
- "charge": 0.5,
- "rod_id": "basic_fishing_rod",
- "reel_speed": 0.2,
- "barrier_damage": 1,
- "bite_multiplier": 1.0,
- "rarity_multipliers": [],
- "discovered_fish_ids": [],
- "capacity_available": true,
- "movement_sequence": 17,
- }
- assert(NetworkFishingProtocol.validate_cast_request(request).is_empty())
- var missing_sequence: Dictionary = request.duplicate(true)
- missing_sequence.erase("movement_sequence")
- assert(
- NetworkFishingProtocol.validate_cast_request(missing_sequence)
- == "Malformed fishing request."
- )
- var invalid_sequence: Dictionary = request.duplicate(true)
- invalid_sequence["movement_sequence"] = -1
- assert(
- NetworkFishingProtocol.validate_cast_request(invalid_sequence)
- == "Fishing request values are outside allowed limits."
- )
- assert(NetworkFishingProtocol.OBSERVER_SNAPSHOT_CHANNEL == 10)
- assert(NetworkWorldSpawnProtocol.SNAPSHOT_CHANNEL == 15)
- assert(
- NetworkWorldSpawnProtocol.SNAPSHOT_CHANNEL
- != NetworkFishingProtocol.SNAPSHOT_CHANNEL
- )
-
-
-func _validate_tiers_and_distribution() -> void:
- assert(FishQualityType.TIER_COUNT == 5)
- assert(FishQualityType.display_name(0) == "boring")
- assert(FishQualityType.display_name(4) == "shiny")
- assert(UIPalette.get_quality_color(0) == UIPalette.QUALITY_BORING)
- assert(UIPalette.get_quality_color(4) == UIPalette.QUALITY_SHINY)
- assert(
- UIPalette.get_quality_color(0)
- != UIPalette.get_quality_color(1)
- )
- assert(FishQualityType.apply_sale_value(3, 0) == 3)
- assert(FishQualityType.apply_sale_value(3, 1) == 4)
- assert(FishQualityType.apply_sale_value(3, 2) == 5)
- assert(FishQualityType.apply_sale_value(3, 3) == 6)
- assert(FishQualityType.apply_sale_value(3, 4) == 7)
- var previous_offer: int = -1
- for quality: int in FishQualityType.TIER_COUNT:
- var offer: int = PelicanBuyer.get_quality_offer(3, quality)
- assert(offer > previous_offer)
- previous_offer = offer
- assert(
- FishQualityType.qualified_name_with_article("bluegill", 0)
- == "a boring bluegill"
- )
- assert(
- FishQualityType.qualified_name_with_article("bluegill", 1)
- == "an average bluegill"
- )
- var rng := RandomNumberGenerator.new()
- rng.seed = 727272
- var counts: Array[int] = [0, 0, 0, 0, 0]
- const SAMPLE_COUNT: int = 50000
- for _sample: int in SAMPLE_COUNT:
- counts[FishQualityType.roll(rng)] += 1
- for quality: int in FishQualityType.TIER_COUNT:
- var observed: float = float(counts[quality]) / float(SAMPLE_COUNT)
- var expected: float = (
- FishQualityType.BASE_ROLL_WEIGHTS[quality] / 100.0
- )
- assert(absf(observed - expected) < 0.015)
-
-
-func _validate_barrier_challenge_curve() -> void:
- assert(
- FishQualityType.BARRIER_HEALTH_MULTIPLIERS.size()
- == FishQualityType.TIER_COUNT
- )
- var expected_minimums: Array[int] = [1, 10, 50, 100, 200]
- var expected_maximums: Array[int] = [9, 50, 100, 200, 400]
- var previous_health: int = 0
- for quality: int in FishQualityType.TIER_COUNT:
- var expected_range := Vector2i(
- expected_minimums[quality], expected_maximums[quality]
- )
- assert(
- FishQualityType.barrier_health_range(quality)
- == expected_range
- )
- assert(
- FishQualityType.roll_barrier_health(null, quality)
- == expected_range.x
- )
- var rng := RandomNumberGenerator.new()
- rng.seed = 73013 + quality
- var observed_counts: Dictionary[int, int] = {}
- var observed_minimum: int = expected_range.y
- var observed_maximum: int = expected_range.x
- var observed_total: int = 0
- var band_size: int = expected_range.y - expected_range.x + 1
- var sample_count: int = maxi(10000, band_size * 250)
- for _sample: int in sample_count:
- var rolled_health: int = FishQualityType.roll_barrier_health(
- rng, quality
- )
- assert(
- rolled_health >= expected_range.x
- and rolled_health <= expected_range.y
- )
- observed_counts[rolled_health] = (
- int(observed_counts.get(rolled_health, 0)) + 1
- )
- observed_minimum = mini(observed_minimum, rolled_health)
- observed_maximum = maxi(observed_maximum, rolled_health)
- observed_total += rolled_health
- assert(observed_minimum == expected_range.x)
- assert(observed_maximum == expected_range.y)
- for authored_health: int in range(
- expected_range.x, expected_range.y + 1
- ):
- assert(observed_counts.has(authored_health))
- var observed_average: float = (
- float(observed_total) / float(sample_count)
- )
- var authored_midpoint: float = (
- float(expected_range.x + expected_range.y) / 2.0
- )
- assert(
- absf(observed_average - authored_midpoint)
- < maxf(0.15, float(band_size) * 0.01)
- )
- var health: int = FishQualityType.apply_barrier_health(8, quality)
- assert(health > previous_health)
- previous_health = health
- assert(FishQualityType.apply_barrier_health(8, -1) == 8)
- assert(FishQualityType.apply_barrier_health(0, FishQualityType.Tier.SHINY) == 4)
- assert(
- FishQualityType.barrier_health_range(-1)
- == Vector2i(1, 9)
- )
-
- var profile := CatchDifficultyProfile.new()
- profile.barrier_count_min = 1
- profile.barrier_count_max = 1
- profile.first_barrier_margin = 0.2
- profile.final_barrier_margin = 0.2
- profile.minimum_barrier_spacing = 0.1
- var controller := CatchController.new()
- root.add_child(controller)
- for quality: int in FishQualityType.TIER_COUNT:
- controller.start_authoritative_encounter(
- profile,
- 0.1,
- 1,
- 818181,
- quality,
- )
- var barriers_value: Variant = controller.get("_barriers")
- assert(barriers_value is Array)
- var barriers: Array = barriers_value as Array
- assert(barriers.size() == 1)
- var barrier := barriers[0] as RefCounted
- assert(barrier != null)
- var health: int = int(barrier.get("maximum_health"))
- assert(health >= expected_minimums[quality])
- assert(health <= expected_maximums[quality])
- controller.queue_free()
-
- var shiny_health: int = FishQualityType.barrier_health_range(
- FishQualityType.Tier.SHINY
- ).y
- var base_power_clicks: int = ceili(float(shiny_health) / 1.0)
- var maximum_barrier_damage: int = (
- PlayerFishingUpgrades.get_barrier_damage_for_level(
- PlayerFishingUpgrades.MAX_BARRIER_POWER_LEVEL
- )
- )
- var max_power_clicks: int = ceili(
- float(shiny_health)
- / float(maximum_barrier_damage)
- )
- assert(base_power_clicks == 400)
- assert(maximum_barrier_damage == 128)
- assert(
- NetworkFishingProtocol.MAX_BARRIER_DAMAGE
- == maximum_barrier_damage
- )
- assert(max_power_clicks == 4)
- assert(max_power_clicks < base_power_clicks)
-
-
-func _validate_fight_pacing_and_reel_upgrades() -> void:
- assert(is_equal_approx(CatchController.CHASE_SPEED, 0.07))
- assert(is_equal_approx(CatchController.CHASE_START_DELAY, 1.5))
- assert(is_equal_approx(CatchController.CHASE_START_OFFSET, 0.04))
- assert(is_equal_approx(Player.BASE_REEL_SPEED, 0.16))
-
- var upgrades := PlayerFishingUpgrades.new()
- assert(is_equal_approx(upgrades.get_reel_speed_multiplier(), 1.0))
- assert(
- PlayerFishingUpgrades.BARRIER_POWER_COSTS.size()
- == PlayerFishingUpgrades.MAX_BARRIER_POWER_LEVEL
- )
- for barrier_level: int in range(
- PlayerFishingUpgrades.MAX_BARRIER_POWER_LEVEL + 1
- ):
- assert(upgrades.restore_levels(0, barrier_level))
- assert(upgrades.get_barrier_damage() == 1 << barrier_level)
- if barrier_level < PlayerFishingUpgrades.MAX_BARRIER_POWER_LEVEL:
- assert(
- upgrades.get_next_barrier_power_cost()
- == PlayerFishingUpgrades.BARRIER_POWER_COSTS[barrier_level]
- )
- else:
- assert(upgrades.get_next_barrier_power_cost() == -1)
- assert(
- upgrades.restore_levels(
- PlayerFishingUpgrades.MAX_REEL_SPEED_LEVEL,
- 0,
- )
- )
- var upgraded_multiplier: float = upgrades.get_reel_speed_multiplier()
- assert(is_equal_approx(upgraded_multiplier, 1.5))
-
- var profile := CatchDifficultyProfile.new()
- profile.barrier_count_min = 0
- profile.barrier_count_max = 0
- var controller := CatchController.new()
- root.add_child(controller)
- controller.start_authoritative_encounter(
- profile,
- Player.BASE_REEL_SPEED,
- 1,
- 919191,
- )
- controller.set_reel_input(true)
- controller.call("_update_free_reeling", 1.0)
- var base_progress: float = controller.progress
- assert(is_equal_approx(base_progress, Player.BASE_REEL_SPEED))
-
- controller.reset()
- controller.start_authoritative_encounter(
- profile,
- Player.BASE_REEL_SPEED * upgraded_multiplier,
- 1,
- 919191,
- )
- controller.set_reel_input(true)
- controller.call("_update_free_reeling", 1.0)
- assert(
- is_equal_approx(
- controller.progress,
- Player.BASE_REEL_SPEED * upgraded_multiplier,
- )
- )
- assert(controller.progress > base_progress)
- controller.queue_free()
- upgrades.queue_free()
-
-
-func _validate_catch_round_trip_and_sale() -> void:
- var fish: FishData = Catalog.get_fish_by_id(&"bluegill")
- assert(fish != null)
- var selector := FishSelectorType.new()
- selector.use_deterministic_test_seed = true
- selector.deterministic_test_seed = 9911
- selector.quality_weight_multipliers = [0.0, 0.0, 0.0, 0.0, 1.0]
- selector.begin_roll()
- var fish_catch: FishCatch = selector.create_catch(fish, [&"worm"])
- assert(fish_catch != null)
- assert(fish_catch.quality == FishQualityType.Tier.SHINY)
- assert(
- fish_catch.sale_value
- == FishQualityType.apply_sale_value(
- fish.get_sale_value_for_weight(fish_catch.weight_lb),
- FishQualityType.Tier.SHINY,
- )
- )
- var save_data: Dictionary = fish_catch.to_save_dict()
- fish_catch.catch_sequence = 1
- save_data = fish_catch.to_save_dict()
- var restored: FishCatch = FishCatchType.from_save_dict(save_data, fish)
- assert(restored != null and restored.quality == fish_catch.quality)
- var network_data: Dictionary = fish_catch.to_network_dict()
- var replicated: FishCatch = FishCatchType.from_network_dict(
- network_data,
- fish,
- )
- assert(replicated != null and replicated.quality == fish_catch.quality)
- var missing_quality: Dictionary = network_data.duplicate(true)
- missing_quality.erase("quality")
- assert(FishCatchType.from_network_dict(missing_quality, fish) == null)
-
- var session := NetworkSession.new()
- var sale_service := NetworkSaleServiceType.new()
- root.add_child(session)
- root.add_child(sale_service)
- sale_service.set("_session", session)
- sale_service.set("_fish_catalog", Catalog)
- var accepted: Dictionary = sale_service.call(
- "_build_authoritative_result",
- 1,
- "quality_sale",
- [network_data],
- [],
- PelicanBuyer,
- )
- assert(bool(accepted.get("accepted", false)))
- assert(
- int(accepted["payout"])
- == PelicanBuyer.get_quality_offer(
- fish.get_sale_value_for_weight(fish_catch.weight_lb),
- fish_catch.quality,
- )
- )
- var inventory := FishInventory.new()
- var wallet := PlayerWallet.new()
- var local_sale := FishSaleService.new()
- root.add_child(inventory)
- root.add_child(wallet)
- root.add_child(local_sale)
- local_sale.setup(inventory, wallet)
- inventory.add_catch(fish_catch)
- var local_preview: FishSaleResult = local_sale.preview_batch(
- [fish_catch.catch_id],
- PelicanBuyer,
- )
- assert(local_preview.is_success())
- assert(local_preview.payout == int(accepted["payout"]))
- var forged: Dictionary = network_data.duplicate(true)
- forged["quality"] = FishQualityType.Tier.BORING
- var rejected: Dictionary = sale_service.call(
- "_build_authoritative_result",
- 1,
- "quality_sale_forged",
- [forged],
- [],
- PelicanBuyer,
- )
- assert(not bool(rejected.get("accepted", false)))
- local_sale.queue_free()
- wallet.queue_free()
- inventory.queue_free()
- sale_service.queue_free()
- session.queue_free()
-
-
-func _validate_mail_round_trip() -> void:
- var fish: FishData = Catalog.get_fish_by_id(&"bluegill")
- var fish_catch := FishCatchType.new()
- fish_catch.fish = fish
- fish_catch.fish_id = fish.id
- fish_catch.weight_lb = fish.get_minimum_weight()
- fish_catch.display_scale = fish.get_display_scale_for_weight(
- fish_catch.weight_lb
- )
- fish_catch.quality = FishQualityType.Tier.IMPRESSIVE
- fish_catch.sale_value = FishQualityType.apply_sale_value(
- fish.get_sale_value_for_weight(fish_catch.weight_lb),
- fish_catch.quality,
- )
- fish_catch.ensure_identity()
- var attachment: Dictionary = {
- "type": PlayerAssetReservationService.AttachmentType.FISH,
- "catch_id": String(fish_catch.catch_id),
- "catch": fish_catch.to_network_dict(),
- }
- var service := NetworkMailService.new()
- root.add_child(service)
- service.set("_fish_catalog", Catalog)
- var restored: FishCatch = service.call("_decode_catch", attachment)
- assert(restored != null)
- assert(restored.quality == FishQualityType.Tier.IMPRESSIVE)
- var signature: Array = NetworkMailProtocol.attachment_signature_fields(
- attachment
- )
- assert(int(signature[5]) == FishQualityType.Tier.IMPRESSIVE)
- var altered: Dictionary = attachment.duplicate(true)
- (altered["catch"] as Dictionary)["quality"] = (
- FishQualityType.Tier.BORING
- )
- assert(
- NetworkMailProtocol.attachment_signature_fields(altered)
- != signature
- )
- service.queue_free()
-
-
-func _validate_collection_mastery() -> void:
- var collection := CollectionLogType.new()
- root.add_child(collection)
- for quality: int in FishQualityType.TIER_COUNT:
- collection.mark_quality_discovered(&"bluegill", quality)
- assert(collection.has_discovered_quality(&"bluegill", quality))
- assert(collection.has_discovered(&"bluegill"))
- assert(collection.has_mastered(&"bluegill"))
- assert(
- collection.get_quality_mask(&"bluegill")
- == FishQualityType.ALL_TIERS_MASK
- )
- assert(collection.get_catch_count(&"bluegill") == 0)
- collection.record_catch(&"bluegill", FishQualityType.Tier.BORING)
- collection.record_catch(&"bluegill", FishQualityType.Tier.SHINY)
- assert(collection.get_catch_count(&"bluegill") == 2)
- assert(int(collection.get_catch_counts()[&"bluegill"]) == 2)
- collection.queue_free()
-
-
-func _validate_version_four_migration() -> void:
- var manager := PlayerSaveManager.new()
- root.add_child(manager)
- var version_four: Dictionary = {
- "save_version": 4,
- "wallet": {"balance": 12},
- "collection": {"discovered_fish_ids": ["bluegill"]},
- "inventory": {
- "next_catch_sequence": 2,
- "catches": [{
- "catch_id": "carp:legacy",
- "catch_sequence": 1,
- "fish_id": "carp",
- "weight_lb": 2.0,
- "display_scale": 1.0,
- "sale_value": 4,
- "is_favorited": false,
- }],
- },
- "bag": {"items": []},
- "hotbar": {"selected_slot": 0, "slots": []},
- "upgrades": {"reel_speed_level": 0, "barrier_power_level": 0},
- "cooler": {"capacity_level": 0},
- }
- var migrated: Dictionary = manager.call(
- "_migrate_save",
- version_four,
- 4,
- )
- assert(int(migrated.get("save_version", -1)) == 11)
- assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0)
- assert(
- is_equal_approx(
- float((migrated["world"] as Dictionary)["time_hours"]),
- 8.0,
- )
- )
- assert(
- int((migrated["world"] as Dictionary)["seed"])
- == PlayerSaveManager.DEFAULT_WORLD_SEED
- )
- var catches: Array = migrated["inventory"]["catches"]
- assert(int((catches[0] as Dictionary)["quality"]) == 0)
- var collection: Dictionary = migrated["collection"]
- var ids: Array = collection["discovered_fish_ids"]
- assert("bluegill" in ids and "carp" in ids)
- var masks: Dictionary = collection["discovered_quality_masks"]
- var boring_bit: int = FishQualityType.bit_for(FishQualityType.Tier.BORING)
- assert(int(masks["bluegill"]) == boring_bit)
- assert(int(masks["carp"]) == boring_bit)
- assert(
- PlayerJobService.validate_save_data(migrated.get("jobs", {}))
- )
- assert(not PlayerHomeState.sanitize_save_data(
- migrated.get("home", {})
- ).is_empty())
- manager.queue_free()
diff --git a/tests/fish_quality_validation.gd.uid b/tests/fish_quality_validation.gd.uid
deleted file mode 100644
index e129478..0000000
--- a/tests/fish_quality_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bffg3t4du01cu
diff --git a/tests/fish_showcase_multiplayer_validation.gd b/tests/fish_showcase_multiplayer_validation.gd
deleted file mode 100644
index 5161377..0000000
--- a/tests/fish_showcase_multiplayer_validation.gd
+++ /dev/null
@@ -1,191 +0,0 @@
-extends SceneTree
-
-const MainScene = preload("res://main/main.tscn")
-const FishCatchType = preload("res://fish/fish_catch.gd")
-const TEST_PORT: int = 17978
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var arguments: PackedStringArray = OS.get_cmdline_user_args()
- if arguments.has("host"):
- await _run_host()
- return
- if arguments.has("client"):
- await _run_client()
- return
- push_error("Fish showcase multiplayer validation needs host or client mode.")
- quit(1)
-
-
-func _run_host() -> void:
- var main: Node = await _create_initialized_main()
- var session := main.get_node("%NetworkSession") as NetworkSession
- assert(bool(main.call(
- "_apply_world", WorldLayout.STARTER_ISLAND, 1, true
- )))
- assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
- assert(session.start_private_host(TEST_PORT))
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(save_manager.initialize_new_game())
- main.call("_enter_gameplay")
- var player := main.get("_player") as Player
- var service := main.get_node(
- "%NetworkFishShowcaseService"
- ) as NetworkFishShowcaseService
- var fish_catch: FishCatch = _add_bluegill(main, player)
- assert(player.hotbar.assign_fish(1, fish_catch.catch_id))
- assert(player.hotbar.select_slot(1))
- assert(service.is_local_showcase_visible())
- await _wait_for_held_fish_visibility(player, true)
- assert(session.set_host_open(true))
-
- var remote_peer_id: int = 0
- var join_deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < join_deadline and remote_peer_id == 0:
- await process_frame
- for peer_id: int in session.get_authenticated_peer_ids():
- if peer_id != session.get_local_peer_id():
- remote_peer_id = peer_id
- break
- assert(remote_peer_id > 1)
- var spawn_service := main.get_node(
- "%PlayerSpawnService"
- ) as PlayerSpawnService
- var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id)
- assert(remote_avatar != null)
- var remote_display := remote_avatar.get("_held_fish_display") as Node3D
- var visible_deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < visible_deadline and not remote_display.visible:
- await process_frame
- assert(remote_display.visible)
- var hidden_deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < hidden_deadline and remote_display.visible:
- await process_frame
- assert(not remote_display.visible)
- service.toggle_selected_fish()
- print("Fish showcase multiplayer host validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _run_client() -> void:
- var main: Node = await _create_initialized_main()
- main.call(
- "_on_title_join_game_requested",
- "127.0.0.1:%d" % TEST_PORT,
- )
- var session := main.get_node("%NetworkSession") as NetworkSession
- var join_deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < join_deadline:
- await process_frame
- if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
- main.call("_confirm_server_trust")
- if session.is_joined_client() and bool(main.get("_gameplay_started")):
- break
- assert(session.is_joined_client())
- assert(
- session.supports_server_capability(
- NetworkFishShowcaseProtocol.CAPABILITY
- )
- )
- var spawn_service := main.get_node(
- "%PlayerSpawnService"
- ) as PlayerSpawnService
- var host_avatar: Player = spawn_service.get_avatar(1)
- assert(host_avatar != null)
- var host_display := host_avatar.get("_held_fish_display") as Node3D
- var snapshot_deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < snapshot_deadline and not host_display.visible:
- await process_frame
- assert(host_display.visible)
-
- var player := main.get("_player") as Player
- var animation_player := player.get_node(
- "Visuals/CharacterRig/AnimationPlayer"
- ) as AnimationPlayer
- for animation_name: StringName in [
- &"pocket_idle_idle",
- &"pocket_idle_show",
- &"pocket_show_show",
- &"pocket_show_idle",
- ]:
- assert(animation_player.has_animation(animation_name))
- var service := main.get_node(
- "%NetworkFishShowcaseService"
- ) as NetworkFishShowcaseService
- var fish_catch: FishCatch = _add_bluegill(main, player)
- assert(player.hotbar.assign_fish(1, fish_catch.catch_id))
- assert(player.hotbar.select_slot(1))
- assert(service.is_local_showcase_visible())
- await _wait_for_held_fish_visibility(player, true)
- await create_timer(2.0).timeout
- assert(service.toggle_selected_fish())
- var held_display := player.get("_held_fish_display") as Node3D
- assert(held_display.visible)
- assert(animation_player.current_animation == &"pocket_show_idle")
- var pocket_deadline: int = Time.get_ticks_msec() + 5000
- while Time.get_ticks_msec() < pocket_deadline and held_display.visible:
- await process_frame
- assert(not held_display.visible)
- print("Fish showcase multiplayer client validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _add_bluegill(main: Node, player: Player) -> FishCatch:
- var catalog := main.get("fish_catalog") as FishPool
- var fish: FishData = catalog.get_fish_by_id(&"bluegill")
- var fish_catch := FishCatchType.new()
- fish_catch.fish = fish
- fish_catch.fish_id = fish.id
- fish_catch.weight_lb = fish.get_minimum_weight()
- fish_catch.display_scale = fish.get_display_scale_for_weight(
- fish_catch.weight_lb
- )
- fish_catch.sale_value = fish.get_sale_value_for_weight(
- fish_catch.weight_lb
- )
- fish_catch.ensure_identity()
- player.inventory.add_catch(fish_catch)
- return fish_catch
-
-
-func _create_initialized_main() -> Node:
- root.size = Vector2i(1280, 720)
- var main := MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- return main
-
-
-func _wait_for_held_fish_visibility(
- player: Player,
- expected_visible: bool,
-) -> void:
- var held_fish_display := player.get("_held_fish_display") as Node3D
- assert(held_fish_display != null)
- var deadline_msec: int = Time.get_ticks_msec() + 2000
- while (
- held_fish_display.visible != expected_visible
- and Time.get_ticks_msec() < deadline_msec
- ):
- await process_frame
- assert(held_fish_display.visible == expected_visible)
diff --git a/tests/fish_showcase_multiplayer_validation.gd.uid b/tests/fish_showcase_multiplayer_validation.gd.uid
deleted file mode 100644
index fb1bfeb..0000000
--- a/tests/fish_showcase_multiplayer_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ckta4mcodq0hg
diff --git a/tests/fishing_audio_validation.gd b/tests/fishing_audio_validation.gd
deleted file mode 100644
index 02a59eb..0000000
--- a/tests/fishing_audio_validation.gd
+++ /dev/null
@@ -1,54 +0,0 @@
-extends SceneTree
-
-const FishingSpotScene := preload("res://fishing/fishing_spot.tscn")
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var fishing_spot := FishingSpotScene.instantiate() as FishingSpot
- root.add_child(fishing_spot)
- await process_frame
-
- var fight_audio := fishing_spot.get_node("%FightAudio") as AudioStreamPlayer
- var bobber_audio := fishing_spot.get_node(
- "%BobberAudio"
- ) as AudioStreamPlayer
- assert(bobber_audio != null)
- assert(bobber_audio.stream != null)
- assert(bobber_audio.bus == &"SFX")
- assert(is_equal_approx(bobber_audio.volume_db, -14.0))
- var bobber_stream := bobber_audio.stream as AudioStreamWAV
- assert(bobber_stream != null)
- assert(bobber_stream.loop_mode == AudioStreamWAV.LOOP_DISABLED)
- assert(fight_audio != null)
- assert(fight_audio.stream != null)
- assert(fight_audio.bus == &"SFX")
- assert(is_equal_approx(fight_audio.volume_db, -10.0))
- var fight_stream := fight_audio.stream as AudioStreamWAV
- assert(fight_stream != null)
- assert(fight_stream.loop_mode == AudioStreamWAV.LOOP_FORWARD)
- assert(fight_stream.loop_end > fight_stream.loop_begin)
- var reeling_audio := fishing_spot.get_node(
- "%ReelingAudio"
- ) as AudioStreamPlayer
- assert(reeling_audio != null)
- assert(reeling_audio.stream != null)
- assert(reeling_audio.bus == &"SFX")
- assert(is_equal_approx(reeling_audio.volume_db, -10.0))
- var reeling_stream := reeling_audio.stream as AudioStreamWAV
- assert(reeling_stream != null)
- assert(reeling_stream.loop_mode == AudioStreamWAV.LOOP_FORWARD)
- assert(reeling_stream.loop_end > reeling_stream.loop_begin)
-
- assert(fishing_spot.has_method("_start_fight_audio"))
- assert(fishing_spot.has_method("_play_bobber_audio"))
- assert(fishing_spot.has_method("_stop_fight_audio"))
- assert(fishing_spot.has_method("_start_reeling_audio"))
- assert(fishing_spot.has_method("_stop_reeling_audio"))
- fishing_spot.free()
- await process_frame
- print("Fishing audio validation: PASS")
- quit()
diff --git a/tests/fishing_audio_validation.gd.uid b/tests/fishing_audio_validation.gd.uid
deleted file mode 100644
index e9cd6d7..0000000
--- a/tests/fishing_audio_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bby8mv7bsjd3w
diff --git a/tests/fishing_authority_validation.gd b/tests/fishing_authority_validation.gd
deleted file mode 100644
index b3c17dd..0000000
--- a/tests/fishing_authority_validation.gd
+++ /dev/null
@@ -1,286 +0,0 @@
-extends SceneTree
-
-const MainScene = preload("res://main/main.tscn")
-const FishingSpotType = preload("res://fishing/fishing_spot.gd")
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- root.size = Vector2i(1280, 720)
- var main := MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- assert(bool(main.call(
- "_apply_world",
- WorldLayout.GENERATED,
- PlayerSaveManager.DEFAULT_WORLD_SEED,
- true,
- )))
- assert(bool(main.call("_prepare_private_host")))
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(save_manager.initialize_new_game())
- main.call("_enter_gameplay")
- for _frame: int in 8:
- await process_frame
-
- var session := main.get_node("%NetworkSession") as NetworkSession
- var service := main.get_node(
- "%NetworkFishingService"
- ) as NetworkFishingService
- var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
- var game_ui := main.get_node("UIPresentation/UIViewport/GameUI")
- var fishing_status := game_ui.get_node("%StatusLabel") as Label
- var fishing_panel := game_ui.get_node("%FishingPanel") as PanelContainer
- var player := main.get("_player") as Player
- var item_catalog := main.get("item_catalog") as ItemCatalog
- assert(session.is_host())
- assert(not session.is_open_host())
- assert(service != null and fishing_spot != null and player != null)
- assert(player.hotbar.get_selected_item_id() == &"basic_fishing_rod")
- var worms: ItemData = item_catalog.get_item_by_id(&"worms")
- assert(worms != null)
- if player.bag.get_quantity(&"worms") == 0:
- assert(player.bag.add_item(&"worms", 2))
- assert(player.equip_bait(worms))
- var bait_quantity_before: int = player.bag.get_quantity(&"worms")
- var fresh_root := main.get_node(
- "TestWorld/Regions/GeneratedWorldRegion/WaterBodies/FreshWaterBodies"
- ) as Node3D
- var pond: WaterBodyAuthoring
- for child: Node in fresh_root.get_children():
- var candidate := child as WaterBodyAuthoring
- if candidate != null and &"pond" in candidate.location_tags:
- pond = candidate
- break
- assert(pond != null)
- var pond_region := pond.get_node("FishingRegion") as FishableWaterRegion
- assert(pond_region != null)
- var pond_surface_y: float = pond_region.get_surface_height()
-
- player.global_position = pond.global_transform * Vector3(
- pond.surface_size.x * 0.5 + 0.8,
- 1.44,
- 0.0,
- )
- var pond_direction := pond.global_position - player.global_position
- pond_direction.y = 0.0
- pond_direction = pond_direction.normalized()
- var visuals := player.get_node("Visuals") as Node3D
- visuals.global_rotation.y = atan2(-pond_direction.x, -pond_direction.z)
- for _frame: int in 4:
- await physics_frame
- assert(player.get_facing_direction().dot(pond_direction) > 0.99)
-
- fishing_spot.call("_begin_aiming", player)
- assert(fishing_spot.state == FishingSpotType.FishingState.AIMING_CAST)
- assert(player.is_movement_enabled())
- fishing_spot.set("_cast_charge", 0.32)
- fishing_spot.call("_update_cast_charge", 0.0)
- var aimed_target: Vector3 = fishing_spot.get("_cast_target")
- var aimed_direction := aimed_target - player.global_position
- aimed_direction.y = 0.0
- assert(aimed_direction.normalized().dot(pond_direction) > 0.99)
- assert(is_equal_approx(aimed_target.y, pond_surface_y))
- assert(fishing_spot.is_target_fishable(aimed_target))
- fishing_spot.call("_confirm_cast")
- assert(fishing_spot.state == FishingSpotType.FishingState.CASTING)
- assert(not player.is_movement_enabled())
-
- var wait_deadline: int = Time.get_ticks_msec() + 4000
- while (
- Time.get_ticks_msec() < wait_deadline
- and fishing_spot.state != FishingSpotType.FishingState.WAITING_FOR_BITE
- ):
- await process_frame
- assert(fishing_spot.state == FishingSpotType.FishingState.WAITING_FOR_BITE)
- assert(service.has_local_attempt())
- assert(player.bag.get_quantity(&"worms") == bait_quantity_before)
- assert(fishing_status.text.is_empty())
- assert(not fishing_status.visible)
- assert(not fishing_panel.visible)
- var attempts: Dictionary = service.get("_attempts")
- var attempt: NetworkFishingAttempt = attempts.get(session.get_local_peer_id())
- assert(attempt != null)
- assert(is_equal_approx(attempt.target.y, pond_surface_y))
- assert(attempt.bobber_position.is_equal_approx(attempt.target))
-
- fishing_spot.set("_withdrawal_input_held", true)
- fishing_spot.set("_network_primary_input_held", true)
- service.submit_local_input(true, true)
- var return_deadline: int = Time.get_ticks_msec() + 4000
- while (
- Time.get_ticks_msec() < return_deadline
- and fishing_spot.state != FishingSpotType.FishingState.RETURNING
- ):
- await process_frame
- assert(fishing_spot.state == FishingSpotType.FishingState.RETURNING)
- assert(not service.has_local_attempt())
- var bobber := fishing_spot.get_node(
- "FishingPresentation/Bobber"
- ) as MeshInstance3D
- assert(bobber.visible)
-
- var ready_deadline: int = Time.get_ticks_msec() + 3000
- while (
- Time.get_ticks_msec() < ready_deadline
- and fishing_spot.state != FishingSpotType.FishingState.READY
- ):
- await process_frame
- assert(fishing_spot.state == FishingSpotType.FishingState.READY)
- assert(player.is_movement_enabled())
- assert(not bobber.visible)
- assert(player.bag.get_quantity(&"worms") == bait_quantity_before)
-
- # A private host rolls and retains the authoritative catch before the fight
- # so its quality selects the barrier band clients receive in snapshots.
- fishing_spot.call("_begin_aiming", player)
- fishing_spot.set("_cast_charge", 0.32)
- fishing_spot.call("_update_cast_charge", 0.0)
- fishing_spot.call("_confirm_cast")
- var second_wait_deadline: int = Time.get_ticks_msec() + 4000
- while (
- Time.get_ticks_msec() < second_wait_deadline
- and fishing_spot.state != FishingSpotType.FishingState.WAITING_FOR_BITE
- ):
- await process_frame
- assert(fishing_spot.state == FishingSpotType.FishingState.WAITING_FOR_BITE)
- attempts = service.get("_attempts")
- attempt = attempts.get(session.get_local_peer_id())
- assert(attempt != null)
- assert(player.bag.get_quantity(&"worms") == bait_quantity_before)
- var fishing_priority_transitions: Array[bool] = []
- fishing_spot.fishing_input_priority_changed.connect(
- func(active: bool) -> void:
- fishing_priority_transitions.append(active)
- )
- service.call("_start_bite", attempt)
- await process_frame
- assert(attempt.phase == NetworkFishingAttempt.Phase.FIGHTING)
- assert(fishing_spot.is_fishing_input_priority_active())
- assert(fishing_priority_transitions == [true])
- assert(player.bag.get_quantity(&"worms") == bait_quantity_before - 1)
- var catalog: FishPool = main.get("fish_catalog") as FishPool
- assert(catalog != null)
- var fish: FishData = catalog.get_fish_by_id(attempt.fish_id)
- var fish_catch := FishCatch.from_network_dict(
- attempt.catch_payload,
- fish,
- )
- assert(fish_catch != null and fish_catch.is_valid())
- var reference_controller := CatchController.new()
- root.add_child(reference_controller)
- reference_controller.start_authoritative_encounter(
- fish.catch_profile,
- attempt.reel_speed,
- attempt.barrier_damage,
- attempt.encounter_seed,
- fish_catch.quality,
- )
- var quality_barriers: Array = attempt.controller.get("_barriers")
- var reference_barriers: Array = reference_controller.get("_barriers")
- assert(quality_barriers.size() == reference_barriers.size())
- for barrier_index: int in quality_barriers.size():
- var quality_barrier: RefCounted = quality_barriers[barrier_index]
- var reference_barrier: RefCounted = reference_barriers[barrier_index]
- assert(
- is_equal_approx(
- float(quality_barrier.get("position")),
- float(reference_barrier.get("position")),
- )
- )
- assert(
- int(quality_barrier.get("maximum_health"))
- == int(reference_barrier.get("maximum_health"))
- )
- assert(
- int(quality_barrier.get("maximum_health"))
- >= FishQuality.BARRIER_HEALTH_MINIMUMS[fish_catch.quality]
- )
- assert(
- int(quality_barrier.get("maximum_health"))
- <= FishQuality.BARRIER_HEALTH_MAXIMUMS[fish_catch.quality]
- )
- reference_controller.queue_free()
- service.call("_on_attempt_escaped", session.get_local_peer_id())
- await process_frame
- assert(not service.has_local_attempt())
- assert(fishing_spot.is_fishing_input_priority_active())
- assert(fishing_status.text.is_empty())
- assert(not fishing_status.visible)
- assert(not fishing_panel.visible)
- var reset_test_deadline: int = Time.get_ticks_msec() + 3000
- while (
- Time.get_ticks_msec() < reset_test_deadline
- and fishing_spot.state != FishingSpotType.FishingState.READY
- ):
- await process_frame
- assert(fishing_spot.state == FishingSpotType.FishingState.READY)
- assert(not fishing_spot.is_fishing_input_priority_active())
- assert(fishing_priority_transitions == [true, false])
-
- # Leaving a session during the cast presentation must release every local
- # action and equipment lock. This is the same cleanup path used by an
- # in-game server switch or a lost connection.
- fishing_spot.call("_begin_aiming", player)
- fishing_spot.set("_cast_charge", 0.32)
- fishing_spot.call("_update_cast_charge", 0.0)
- fishing_spot.call("_confirm_cast")
- assert(fishing_spot.state == FishingSpotType.FishingState.CASTING)
- assert(not player.is_movement_enabled())
- fishing_spot.call(
- "_on_network_session_state_changed",
- NetworkSession.State.DISCONNECTING,
- )
- assert(fishing_spot.state == FishingSpotType.FishingState.READY)
- assert(player.is_movement_enabled())
- assert(fishing_spot.can_change_hotbar_selection())
- assert(fishing_spot.can_open_fishing_shop())
-
- # Escape can put the rod into RETURNING immediately before deep-water
- # recovery begins. Recovery must finish that return before snapshotting its
- # movement state, otherwise the respawn restores a permanent cast lock.
- var water_recovery := main.get_node("%WaterRecovery") as WaterRecoveryController
- assert(water_recovery != null)
- var pause_menu: PauseMenu = game_ui.get_pause_menu()
- pause_menu.open_menu()
- assert(pause_menu.visible)
- player.set_movement_enabled(false)
- fishing_spot.set("_active_player", player)
- fishing_spot.state = FishingSpotType.FishingState.RETURNING
- water_recovery.call(
- "_on_recovery_requested",
- player,
- player.global_position.y,
- )
- assert(fishing_spot.state == FishingSpotType.FishingState.READY)
- assert(bool(water_recovery.get("_prior_movement_enabled")))
- assert(bool(water_recovery.get("_prior_camera_input_enabled")))
- assert(not pause_menu.visible)
- var test_world := main.get_node("TestWorld") as TestWorld
- assert(test_world != null)
- var recovery_entry := water_recovery.get("_entry_position") as Vector3
- var resolved_recovery := water_recovery.call(
- "_resolve_recovery_position"
- ) as Vector3
- assert(resolved_recovery.is_equal_approx(
- test_world.get_water_recovery_position(recovery_entry)
- ))
- water_recovery.call("_finish_recovery")
- assert(player.is_movement_enabled())
-
- print("Fishing authority validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
diff --git a/tests/fishing_authority_validation.gd.uid b/tests/fishing_authority_validation.gd.uid
deleted file mode 100644
index 650669c..0000000
--- a/tests/fishing_authority_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cgqcyldad6dsj
diff --git a/tests/fishing_multiplayer_validation.gd b/tests/fishing_multiplayer_validation.gd
deleted file mode 100644
index 9d7ef7f..0000000
--- a/tests/fishing_multiplayer_validation.gd
+++ /dev/null
@@ -1,536 +0,0 @@
-extends SceneTree
-
-const MainScene = preload("res://main/main.tscn")
-const FishingSpotType = preload("res://fishing/fishing_spot.gd")
-const TEST_SEED: int = 864209
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var arguments: PackedStringArray = OS.get_cmdline_user_args()
- if arguments.has("host"):
- await _run_host()
- return
- if arguments.has("client"):
- await _run_client()
- return
- push_error("Fishing multiplayer validation requires host or client mode.")
- quit(1)
-
-
-func _run_host() -> void:
- var main: Node = await _create_initialized_main()
- assert(bool(main.call(
- "_apply_world",
- WorldLayout.STARTER_ISLAND,
- TEST_SEED,
- true,
- )))
- assert(bool(main.call(
- "_prepare_host_world",
- WorldLayout.STARTER_ISLAND,
- TEST_SEED,
- )))
- assert(bool(main.call("_prepare_private_host")))
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(save_manager.initialize_new_game(
- TEST_SEED,
- WorldLayout.STARTER_ISLAND,
- ))
- main.call("_enter_gameplay")
- var session := main.get_node("%NetworkSession") as NetworkSession
- assert(session.set_host_open(true))
- var service := main.get_node(
- "%NetworkFishingService"
- ) as NetworkFishingService
-
- var join_deadline: int = Time.get_ticks_msec() + 60000
- while (
- Time.get_ticks_msec() < join_deadline
- and session.get_authenticated_peer_ids().size() < 2
- ):
- await process_frame
- assert(session.get_authenticated_peer_ids().size() == 2)
- var remote_peer_id: int = 0
- for peer_id: int in session.get_authenticated_peer_ids():
- if peer_id != session.get_local_peer_id():
- remote_peer_id = peer_id
- break
- assert(remote_peer_id > 1)
- var spawn_service := main.get_node(
- "%PlayerSpawnService"
- ) as PlayerSpawnService
- var network_home := main.get_node(
- "%NetworkHomeService"
- ) as NetworkHomeService
- var home_world := main.get_node("%HomeWorldService") as HomeWorldService
- var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id)
- assert(remote_avatar != null)
- assert(remote_avatar.bag.get_quantity(&"worms") == 0)
- var remote_record := session.get_peer_record(remote_peer_id)
- assert(remote_record != null)
- assert(await _wait_for_home(
- network_home, remote_record.identity_fingerprint
- ))
- var exterior_spawn: Transform3D = home_world.get_exterior_spawn_transform(
- remote_record.identity_fingerprint
- )
- var exterior_deadline: int = Time.get_ticks_msec() + 5000
- while (
- Time.get_ticks_msec() < exterior_deadline
- and remote_avatar.global_position.distance_to(exterior_spawn.origin) > 0.25
- ):
- await physics_frame
- assert(remote_avatar.global_position.distance_to(exterior_spawn.origin) <= 0.25)
- # Give the client enough local frames to observe the initial RV-door spawn
- # before this test publishes its separate fishing-position teleport.
- for _frame: int in 30:
- await physics_frame
- var cast_setup: Dictionary = await _find_clear_fishing_setup(
- main, remote_avatar
- )
- assert(not cast_setup.is_empty(), "no clear fishing setup was found")
- var expected_position: Vector3 = cast_setup["position"]
- var expected_direction: Vector3 = cast_setup["direction"]
- session.publish_authoritative_teleport(remote_peer_id)
- var aiming_deadline: int = Time.get_ticks_msec() + 15000
- while (
- Time.get_ticks_msec() < aiming_deadline
- and int(remote_avatar.get("_fishing_visual_phase"))
- != Player.FishingVisualPhase.CASTING
- ):
- await process_frame
- assert(
- int(remote_avatar.get("_fishing_visual_phase"))
- == Player.FishingVisualPhase.CASTING
- )
- var remote_position_error := (
- remote_avatar.global_position - expected_position
- )
- remote_position_error.y = 0.0
- assert(
- remote_position_error.length() <= 0.25,
- "remote casting position drifted: %s" % remote_avatar.global_position
- )
- assert(
- remote_avatar.get_facing_direction().dot(expected_direction) > 0.99,
- "remote casting facing drifted: %s"
- % remote_avatar.get_facing_direction()
- )
-
- var saw_remote_attempt: bool = false
- var remote_presentation: RemoteFishingPresentation
- var attempt_deadline: int = Time.get_ticks_msec() + 12000
- while Time.get_ticks_msec() < attempt_deadline:
- await process_frame
- var attempts: Dictionary = service.get("_attempts")
- for peer_id: int in attempts:
- if peer_id == session.get_local_peer_id():
- continue
- var attempt: NetworkFishingAttempt = attempts[peer_id]
- assert(attempt.target.is_finite())
- assert(attempt.bobber_position.distance_to(attempt.target) <= 0.001)
- var presentations: Dictionary = service.get("_remote_presentations")
- remote_presentation = presentations.get(peer_id)
- assert(remote_presentation != null)
- var remote_bobber := remote_presentation.get(
- "_bobber"
- ) as MeshInstance3D
- assert(remote_bobber.visible)
- saw_remote_attempt = true
- if saw_remote_attempt:
- break
- assert(saw_remote_attempt)
- assert(
- int(remote_avatar.get("_fishing_visual_phase"))
- in [
- Player.FishingVisualPhase.RELEASE,
- Player.FishingVisualPhase.FISHING,
- ]
- )
- var sitting_deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < sitting_deadline and not remote_avatar.is_sitting():
- await process_frame
- assert(remote_avatar.is_sitting())
- var fishing_deadline: int = Time.get_ticks_msec() + 5000
- while (
- Time.get_ticks_msec() < fishing_deadline
- and int(remote_avatar.get("_fishing_visual_phase"))
- != Player.FishingVisualPhase.FISHING
- ):
- await process_frame
- assert(
- int(remote_avatar.get("_fishing_visual_phase"))
- == Player.FishingVisualPhase.FISHING
- )
- var remote_animation_player := remote_avatar.get_node(
- "Visuals/CharacterRig/AnimationPlayer"
- ) as AnimationPlayer
- var return_deadline: int = Time.get_ticks_msec() + 5000
- while (
- Time.get_ticks_msec() < return_deadline
- and service.has_peer_attempt(remote_peer_id)
- ):
- await process_frame
- assert(not service.has_peer_attempt(remote_peer_id))
- assert(
- int(remote_avatar.get("_fishing_visual_phase"))
- == Player.FishingVisualPhase.RETRACT
- )
- var retract_animation_deadline: int = Time.get_ticks_msec() + 3000
- while (
- Time.get_ticks_msec() < retract_animation_deadline
- and remote_animation_player.current_animation != &"retract_sit"
- ):
- await process_frame
- assert(remote_animation_player.current_animation == &"retract_sit")
- var retract_completion_deadline: int = Time.get_ticks_msec() + 5000
- while (
- Time.get_ticks_msec() < retract_completion_deadline
- and not remote_avatar.is_retract_visual_complete()
- ):
- await process_frame
- assert(remote_avatar.is_retract_visual_complete())
- # Give the client that owns the same presentation a frame boundary to finish
- # local cleanup before the host intentionally tears down this test session.
- for _frame: int in 4:
- await physics_frame
-
- print("Fishing multiplayer host validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _run_client() -> void:
- var main: Node = await _create_initialized_main()
- main.call("_on_title_join_game_requested", "127.0.0.1:7777")
- var session := main.get_node("%NetworkSession") as NetworkSession
- var joined: bool = false
- var join_deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < join_deadline:
- await process_frame
- if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
- main.call("_confirm_server_trust")
- if session.is_joined_client() and bool(main.get("_gameplay_started")):
- joined = true
- break
- assert(joined)
- var network_home := main.get_node(
- "%NetworkHomeService"
- ) as NetworkHomeService
- var home_world := main.get_node("%HomeWorldService") as HomeWorldService
- var local_fingerprint: String = session.get_local_identity_fingerprint()
- assert(await _wait_for_home(network_home, local_fingerprint))
- var player := main.get("_player") as Player
- var exterior_spawn: Transform3D = (
- home_world.get_exterior_spawn_transform(local_fingerprint)
- )
- var home_spawn_deadline: int = Time.get_ticks_msec() + 5000
- while (
- Time.get_ticks_msec() < home_spawn_deadline
- and player.global_position.distance_to(exterior_spawn.origin) > 0.25
- ):
- await physics_frame
- assert(player.global_position.distance_to(exterior_spawn.origin) <= 0.25)
-
- var service := main.get_node(
- "%NetworkFishingService"
- ) as NetworkFishingService
- var cast_rejections: Array[String] = []
- service.local_cast_rejected.connect(
- func(message: String) -> void:
- cast_rejections.append(message)
- )
- var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
- var item_catalog := main.get("item_catalog") as ItemCatalog
- var worms: ItemData = item_catalog.get_item_by_id(&"worms")
- if player.bag.get_quantity(&"worms") == 0:
- assert(player.bag.add_item(&"worms", 1))
- assert(player.equip_bait(worms))
- var worms_before_cast: int = player.bag.get_quantity(&"worms")
- var character_animation_player := player.get_node(
- "Visuals/CharacterRig/AnimationPlayer"
- ) as AnimationPlayer
- assert(character_animation_player.has_animation(&"retract_sit"))
- # The host chooses and publishes a clear target after both RV manifests have
- # settled. Waiting for that authoritative teleport also coordinates the
- # animation-observation window between these two independent processes.
- var placement_deadline: int = Time.get_ticks_msec() + 10000
- while (
- Time.get_ticks_msec() < placement_deadline
- and player.global_position.distance_to(exterior_spawn.origin) <= 1.0
- ):
- await physics_frame
- assert(player.global_position.distance_to(exterior_spawn.origin) > 1.0)
- for _frame: int in 2:
- await physics_frame
- var expected_position: Vector3 = player.global_position
- var expected_direction: Vector3 = player.get_facing_direction()
-
- player.set_casting_visual()
- assert(
- int(player.get("_fishing_visual_phase"))
- == Player.FishingVisualPhase.CASTING
- )
- # Keep the transient pull-back pose visible long enough for the separate host
- # process to observe it even when a cold validation run is CPU-bound.
- for _frame: int in 120:
- await physics_frame
- player.set_fishing_visual(false)
- player.call("_set_sitting", true, true)
- assert(player.is_sitting())
- for _frame: int in 15:
- await physics_frame
- assert(fishing_spot.state == FishingSpotType.FishingState.READY)
- assert(fishing_spot.get("_active_player") == null)
- fishing_spot.call("_begin_aiming", player)
- assert(fishing_spot.state == FishingSpotType.FishingState.AIMING_CAST)
- assert(player.is_movement_enabled())
- assert(
- int(player.get("_fishing_visual_phase"))
- == Player.FishingVisualPhase.CASTING
- )
- var cast_charge: float = 0.0
- var target: Vector3 = Vector3.ZERO
- var cast_origin: Vector3 = fishing_spot.get("_cast_origin_position")
- var target_diagnostics: Array[String] = []
- for charge_step: int in range(1, 21):
- var candidate_charge := float(charge_step) / 20.0
- fishing_spot.set("_cast_charge", candidate_charge)
- fishing_spot.call("_update_cast_charge", 0.0)
- var candidate_target: Vector3 = fishing_spot.get("_cast_target")
- var candidate_surface := fishing_spot.get(
- "_aim_surface_sample"
- ) as FishingSurfaceSample
- target_diagnostics.append(
- "charge=%.2f surface=%s path=%s target=%s" % [
- candidate_charge,
- candidate_surface.is_fishable(),
- fishing_spot.get("_cast_path_is_clear"),
- candidate_target,
- ]
- )
- if (
- candidate_surface.is_fishable()
- and bool(fishing_spot.get("_cast_path_is_clear"))
- ):
- cast_charge = candidate_charge
- target = candidate_target
- break
- assert(
- cast_charge > 0.0,
- (
- "no clear fishable test target was found; position=%s facing=%s "
- + "expected=%s diagnostics=%s"
- ) % [
- player.global_position,
- player.get_facing_direction(),
- expected_direction,
- target_diagnostics,
- ],
- )
- var evidence: Dictionary = fishing_spot.call("_build_network_evidence")
- fishing_spot.set("state", FishingSpotType.FishingState.CASTING)
- player.set_movement_enabled(false)
- player.set_release_visual()
- assert(
- int(player.get("_fishing_visual_phase"))
- == Player.FishingVisualPhase.RELEASE
- )
- assert(
- not service.request_local_cast(
- cast_origin,
- target,
- cast_charge,
- evidence,
- ).is_empty()
- )
-
- var accepted_deadline: int = Time.get_ticks_msec() + 6000
- while (
- Time.get_ticks_msec() < accepted_deadline
- and fishing_spot.state != FishingSpotType.FishingState.WAITING_FOR_BITE
- ):
- await process_frame
- assert(
- fishing_spot.state == FishingSpotType.FishingState.WAITING_FOR_BITE,
- (
- "cast acceptance timed out: state=%s local_attempt=%s target=%s "
- + "rejections=%s"
- ) % [
- fishing_spot.state,
- service.has_local_attempt(),
- target,
- cast_rejections,
- ]
- )
- assert(service.has_local_attempt())
- # Bait remains available while the bobber is waiting. It is consumed only
- # once the authoritative bite begins.
- assert(player.bag.get_quantity(&"worms") == worms_before_cast)
- var fishing_deadline: int = Time.get_ticks_msec() + 5000
- while (
- Time.get_ticks_msec() < fishing_deadline
- and int(player.get("_fishing_visual_phase"))
- != Player.FishingVisualPhase.FISHING
- ):
- await process_frame
- assert(
- int(player.get("_fishing_visual_phase"))
- == Player.FishingVisualPhase.FISHING
- )
- for _frame: int in 30:
- await physics_frame
- fishing_spot.set("_withdrawal_input_held", true)
- fishing_spot.set("_network_primary_input_held", true)
- service.submit_local_input(true, true)
-
- var return_deadline: int = Time.get_ticks_msec() + 5000
- while (
- Time.get_ticks_msec() < return_deadline
- and fishing_spot.state != FishingSpotType.FishingState.RETURNING
- ):
- await process_frame
- assert(fishing_spot.state == FishingSpotType.FishingState.RETURNING)
- assert(
- int(player.get("_fishing_visual_phase"))
- == Player.FishingVisualPhase.RETRACT
- )
- var retract_animation_deadline: int = Time.get_ticks_msec() + 3000
- while (
- Time.get_ticks_msec() < retract_animation_deadline
- and character_animation_player.current_animation != &"retract_sit"
- ):
- await process_frame
- assert(character_animation_player.current_animation == &"retract_sit")
- var bobber := fishing_spot.get_node(
- "FishingPresentation/Bobber"
- ) as MeshInstance3D
- assert(bobber.visible)
- while not player.is_retract_visual_complete():
- assert(fishing_spot.state == FishingSpotType.FishingState.RETURNING)
- await process_frame
-
- var ready_deadline: int = Time.get_ticks_msec() + 3000
- while (
- Time.get_ticks_msec() < ready_deadline
- and fishing_spot.state != FishingSpotType.FishingState.READY
- ):
- await process_frame
- assert(fishing_spot.state == FishingSpotType.FishingState.READY)
- assert(player.is_movement_enabled())
- assert(not bobber.visible)
- var host_observation_deadline: int = Time.get_ticks_msec() + 2000
- while Time.get_ticks_msec() < host_observation_deadline:
- await process_frame
- print("Fishing multiplayer client validation: PASS")
- var host_completion_deadline: int = Time.get_ticks_msec() + 10000
- while (
- Time.get_ticks_msec() < host_completion_deadline
- and session.is_joined_client()
- ):
- await process_frame
- if session.is_joined_client():
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _create_initialized_main() -> Node:
- root.size = Vector2i(1280, 720)
- var main := MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- return main
-
-
-func _wait_for_home(
- network_home: NetworkHomeService,
- fingerprint: String,
-) -> bool:
- var deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- var manifests: Dictionary = network_home.get(
- "_manifests_by_fingerprint"
- )
- if manifests.has(fingerprint):
- return true
- return false
-
-
-func _find_clear_fishing_setup(main: Node, player: Player) -> Dictionary:
- var world := main.get_node("TestWorld") as TestWorld
- var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
- var regions: Array[FishableWaterRegion] = world.get_fishable_water_regions()
- for region: FishableWaterRegion in regions:
- var body := region.get_parent() as WaterBodyAuthoring
- if body == null or body.surface_size.x <= 0.0 or body.surface_size.y <= 0.0:
- continue
- var center: Vector3 = region.global_position
- var half_size: Vector2 = body.surface_size * 0.5
- var candidate_offsets: Array[Vector3] = [
- Vector3(half_size.x + 0.8, 0.0, 0.0),
- Vector3(-half_size.x - 0.8, 0.0, 0.0),
- Vector3(0.0, 0.0, half_size.y + 0.8),
- Vector3(0.0, 0.0, -half_size.y - 0.8),
- ]
- for offset: Vector3 in candidate_offsets:
- var position := Vector3(
- center.x + offset.x,
- region.get_surface_height() + 1.44,
- center.z + offset.z,
- )
- var direction: Vector3 = center - position
- direction.y = 0.0
- direction = direction.normalized()
- player.global_position = position
- var visuals := player.get_node("Visuals") as Node3D
- visuals.global_rotation.y = atan2(-direction.x, -direction.z)
- for _frame: int in 2:
- await physics_frame
- var cast_origin: Vector3 = player.get_cast_origin_position()
- var launch_position: Vector3 = player.get_fishing_rod_tip().global_position
- for charge_step: int in range(1, 21):
- var cast_charge := float(charge_step) / 20.0
- var cast_distance := lerpf(
- fishing_spot.minimum_cast_distance,
- fishing_spot.maximum_cast_distance,
- cast_charge,
- )
- var query_position := cast_origin + direction * cast_distance
- var sample: FishingSurfaceSample = (
- fishing_spot.resolve_fishing_surface(
- query_position, cast_origin.y
- )
- )
- var target: Vector3 = sample.get_bobber_position(
- fishing_spot.solid_bobber_clearance
- )
- if (
- sample.is_fishable()
- and fishing_spot.is_cast_path_clear(
- launch_position, target
- )
- ):
- return {
- "position": player.global_position,
- "direction": direction,
- }
- return {}
diff --git a/tests/fishing_multiplayer_validation.gd.uid b/tests/fishing_multiplayer_validation.gd.uid
deleted file mode 100644
index c8fcbd8..0000000
--- a/tests/fishing_multiplayer_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://chyq7wjhpko7d
diff --git a/tests/fishing_shop_controller_validation.gd b/tests/fishing_shop_controller_validation.gd
deleted file mode 100644
index 488a01e..0000000
--- a/tests/fishing_shop_controller_validation.gd
+++ /dev/null
@@ -1,105 +0,0 @@
-extends SceneTree
-
-const FishingShopScene: PackedScene = preload(
- "res://ui/fishing_shop.tscn"
-)
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- root.size = Vector2i(1280, 720)
- var shop := FishingShopScene.instantiate() as FishingShop
- root.add_child(shop)
- shop.show()
- await process_frame
-
- var tabs: Array = shop.get("_shop_tabs") as Array
- assert(tabs.size() == FishingShop.ShopSection.size())
- var supplies_list := shop.get_node("%SuppliesList") as VBoxContainer
- var dummy_stock := Button.new()
- dummy_stock.name = "ControllerTestStock"
- dummy_stock.text = "test stock"
- supplies_list.add_child(dummy_stock)
- var activations: Array[int] = [0]
- dummy_stock.pressed.connect(
- func() -> void:
- activations[0] += 1
- )
-
- var accept := InputEventJoypadButton.new()
- accept.button_index = JOY_BUTTON_A
- accept.pressed = true
- var accept_release := InputEventJoypadButton.new()
- accept_release.button_index = JOY_BUTTON_A
- accept_release.pressed = false
- var cancel := InputEventJoypadButton.new()
- cancel.button_index = JOY_BUTTON_B
- cancel.pressed = true
-
- for section_index: int in range(FishingShop.ShopSection.ART_SUPPLIES + 1):
- shop.set("_shop_section", section_index)
- shop.call("_select_shop_section", section_index, false)
- shop.call("_enter_shop_tabs_zone")
- for _frame: int in 2:
- await process_frame
- var tab := tabs[section_index] as Button
- assert(
- root.gui_get_focus_owner() == tab,
- "section %d tab focus mismatch: focused=%s tab=%s visible=%s mode=%d"
- % [
- section_index,
- root.gui_get_focus_owner(),
- tab,
- tab.is_visible_in_tree(),
- tab.focus_mode,
- ],
- )
- assert(
- shop.get("_controller_zone")
- == FishingShop.ControllerZone.TABS
- )
- shop.call("_input", accept)
- for _frame: int in 2:
- await process_frame
- assert(int(shop.get("_shop_section")) == section_index)
- assert(
- shop.get("_controller_zone")
- == FishingShop.ControllerZone.CONTENT
- )
- assert(not _array_contains_control(
- tabs,
- root.gui_get_focus_owner(),
- ))
- if section_index == FishingShop.ShopSection.BAIT:
- assert(root.gui_get_focus_owner() == dummy_stock)
- Input.parse_input_event(accept)
- await process_frame
- Input.parse_input_event(accept_release)
- await process_frame
- assert(activations[0] == 1)
- shop.call("_input", cancel)
- for _frame: int in 2:
- await process_frame
- assert(shop.visible)
- assert(
- shop.get("_controller_zone")
- == FishingShop.ControllerZone.TABS
- )
- assert(root.gui_get_focus_owner() == tab)
-
- shop.hide()
- shop.queue_free()
- await process_frame
- print("Fishing shop controller validation: PASS")
- quit()
-
-
-func _array_contains_control(values: Array, target: Control) -> bool:
- for value: Variant in values:
- var control := value as Control
- if control == target:
- return true
- return false
diff --git a/tests/fishing_shop_controller_validation.gd.uid b/tests/fishing_shop_controller_validation.gd.uid
deleted file mode 100644
index 228bf9b..0000000
--- a/tests/fishing_shop_controller_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://duwikmb08papi
diff --git a/tests/fishing_surface_validation.gd b/tests/fishing_surface_validation.gd
deleted file mode 100644
index 8248c36..0000000
--- a/tests/fishing_surface_validation.gd
+++ /dev/null
@@ -1,615 +0,0 @@
-extends SceneTree
-
-const FishingSpotScene = preload("res://fishing/fishing_spot.tscn")
-const FishingSpotType = preload("res://fishing/fishing_spot.gd")
-const FishingPresentationType = preload(
- "res://fishing/fishing_presentation.gd"
-)
-const RemoteFishingPresentationType = preload(
- "res://fishing/remote_fishing_presentation.gd"
-)
-const PlayerScene = preload("res://player/player.tscn")
-const FishCatchType = preload("res://fish/fish_catch.gd")
-const FishingSurfaceResolverType = preload(
- "res://fishing/fishing_surface_resolver.gd"
-)
-const FishingSurfaceSampleType = preload(
- "res://fishing/fishing_surface_sample.gd"
-)
-const FishableWaterRegionType = preload(
- "res://world/fishable_water_region.gd"
-)
-const FishPoolType = preload("res://fish/fish_pool.gd")
-const WaterBodyScene = preload("res://world/water_body.tscn")
-const StarterRegionScene = preload(
- "res://world/regions/starter_island_region.tscn"
-)
-const PondPool: FishPoolType = preload(
- "res://fish/pools/starter_pond_pool.tres"
-)
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- await _validate_resolver_layers()
- await _validate_portable_water_body()
- await _validate_starter_region_surfaces()
- await _validate_presentation()
- await _validate_remote_presentation()
- await _validate_bite_wait_distribution()
- print("Fishing surface validation: PASS")
- quit()
-
-
-func _validate_resolver_layers() -> void:
- var world := Node3D.new()
- root.add_child(world)
- _add_solid_box(world, Vector3(40.0, 2.0, 12.0), Vector3(10.0, -1.0, 0.0))
- _add_water_region(world, Vector3(0.0, 2.0, 0.0), Vector2(8.0, 8.0), PondPool)
- _add_solid_box(world, Vector3(1.0, 1.0, 1.0), Vector3(2.0, 2.25, 0.0))
- _add_solid_box(world, Vector3(1.0, 1.0, 1.0), Vector3(-2.0, 6.0, 0.0))
- _add_solid_box(world, Vector3(1.0, 1.0, 1.0), Vector3(0.0, 1.49, 2.0))
- _add_solid_box(world, Vector3(1.0, 1.0, 1.0), Vector3(0.0, 1.5, -2.0))
- _add_water_region(world, Vector3(10.0, 5.0, 0.0), Vector2(6.0, 6.0), PondPool)
- _add_water_region(world, Vector3(20.0, 3.0, 0.0), Vector2(6.0, 6.0), null)
- await physics_frame
- await physics_frame
-
- var resolver := FishingSurfaceResolverType.new()
- var space_state: PhysicsDirectSpaceState3D = world.get_world_3d().direct_space_state
- var water: FishingSurfaceSampleType = resolver.resolve_surface(
- space_state,
- Vector3.ZERO,
- 4.0,
- )
- assert(water.is_fishable())
- assert(is_equal_approx(water.position.y, 2.0))
- var water_under_overhang: FishingSurfaceSampleType = resolver.resolve_surface(
- space_state,
- Vector3(-2.0, 4.0, 0.0),
- 4.0,
- 0.08,
- )
- assert(water_under_overhang.is_fishable())
- assert(is_equal_approx(water_under_overhang.position.y, 2.0))
- var shallow_water: FishingSurfaceSampleType = resolver.resolve_surface(
- space_state,
- Vector3(0.0, 2.0, 2.0),
- 4.0,
- 0.04,
- )
- assert(shallow_water.is_fishable())
- var exact_shore_contact: FishingSurfaceSampleType = resolver.resolve_surface(
- space_state,
- Vector3(0.0, 2.0, -2.0),
- 4.0,
- 0.04,
- )
- assert(exact_shore_contact.has_surface)
- assert(not exact_shore_contact.is_fishable())
-
- var covered_water: FishingSurfaceSampleType = resolver.resolve_surface(
- space_state,
- Vector3(2.0, 2.0, 0.0),
- 4.0,
- )
- assert(covered_water.has_surface)
- assert(not covered_water.is_water_surface)
- assert(is_equal_approx(covered_water.position.y, 2.75))
-
- var elevated_water: FishingSurfaceSampleType = resolver.resolve_surface(
- space_state,
- Vector3(10.0, 0.0, 0.0),
- 8.0,
- )
- assert(elevated_water.is_fishable())
- assert(is_equal_approx(elevated_water.position.y, 5.0))
-
- var unstocked_water: FishingSurfaceSampleType = resolver.resolve_surface(
- space_state,
- Vector3(20.0, 0.0, 0.0),
- 6.0,
- )
- assert(unstocked_water.has_surface)
- assert(unstocked_water.is_water_surface)
- assert(not unstocked_water.is_fishable())
- assert(is_equal_approx(unstocked_water.position.y, 3.0))
-
- var safe_reel: FishingSurfaceSampleType = resolver.resolve_withdrawal_surface(
- space_state,
- Vector3(0.0, 2.0, 0.0),
- Vector3(3.0, 2.0, 0.0),
- Vector3.RIGHT,
- 4.0,
- 0.4,
- )
- assert(safe_reel.is_fishable())
- assert(is_equal_approx(safe_reel.position.y, 2.0))
- var shoreline_reel: FishingSurfaceSampleType = (
- resolver.resolve_withdrawal_surface(
- space_state,
- Vector3(3.0, 2.0, 0.0),
- Vector3(3.7, 2.0, 0.0),
- Vector3.RIGHT,
- 4.0,
- 0.4,
- )
- )
- assert(not shoreline_reel.is_fishable())
- assert(shoreline_reel.position.is_equal_approx(Vector3(3.0, 2.0, 0.0)))
-
- world.queue_free()
- await process_frame
-
-
-func _validate_portable_water_body() -> void:
- var water_body := WaterBodyScene.instantiate() as Node3D
- water_body.position = Vector3(3.0, 7.0, -2.0)
- water_body.set("surface_size", Vector2(12.0, 8.0))
- water_body.set("fishing_depth", 6.0)
- water_body.set("recovery_depth", 7.0)
- water_body.set(
- "recovery_entry_height_reference",
- PlayerWaterTrigger.EntryHeightReference.PLAYER_ORIGIN,
- )
- water_body.set("recovery_entry_depth_threshold", 0.1)
- water_body.set("recovery_entry_confirmation_seconds", 0.2)
- water_body.set("fish_pool", PondPool)
- root.add_child(water_body)
- await process_frame
-
- var visual := water_body.get_node("VisualWater") as MeshInstance3D
- var visual_mesh := visual.mesh as PlaneMesh
- assert(visual_mesh.size.is_equal_approx(Vector2(12.0, 8.0)))
- var fishing_region := water_body.get_node(
- "FishingRegion"
- ) as FishableWaterRegionType
- var fishing_shape_node := water_body.get_node(
- "FishingRegion/Shape"
- ) as CollisionShape3D
- var fishing_shape := fishing_shape_node.shape as BoxShape3D
- assert(fishing_region.fish_pool == PondPool)
- assert(is_equal_approx(fishing_region.get_surface_height(), 7.0))
- assert(is_equal_approx(fishing_shape_node.position.y, -3.0))
- assert(fishing_shape.size.is_equal_approx(Vector3(12.0, 6.0, 8.0)))
- var recovery_region := (
- water_body.get_node("RecoveryRegion") as PlayerWaterTrigger
- )
- assert(is_equal_approx(recovery_region.position.y, -3.5))
- assert(
- recovery_region.entry_height_reference
- == PlayerWaterTrigger.EntryHeightReference.PLAYER_ORIGIN
- )
- assert(is_equal_approx(recovery_region.entry_depth_threshold, 0.1))
- assert(is_equal_approx(recovery_region.entry_confirmation_seconds, 0.2))
-
- var recovery_player := PlayerScene.instantiate() as Player
- assert(recovery_player != null)
- root.add_child(recovery_player)
- recovery_player.set_process(false)
- recovery_player.set_physics_process(false)
- var recovery_requests: Array[float] = []
- recovery_region.recovery_requested.connect(
- func(_player: Player, requested_surface_height: float) -> void:
- recovery_requests.append(requested_surface_height)
- )
- recovery_region.call("_on_body_entered", recovery_player)
- recovery_player.global_position.y = 6.8
- recovery_region.call("_physics_process", 0.1)
- assert(recovery_requests.is_empty())
- # Breaking the submersion resets confirmation rather than carrying a stale
- # partial fall into a later jump.
- recovery_player.global_position.y = 7.0
- recovery_region.call("_physics_process", 0.1)
- recovery_player.global_position.y = 6.8
- recovery_region.call("_physics_process", 0.11)
- assert(recovery_requests.is_empty())
- recovery_region.call("_physics_process", 0.1)
- assert(recovery_requests == [7.0])
- recovery_region.call("_on_body_exited", recovery_player)
- recovery_player.queue_free()
- await process_frame
-
- water_body.queue_free()
- await process_frame
-
- var mesh_derived_body := WaterBodyScene.instantiate() as WaterBodyAuthoring
- mesh_derived_body.position = Vector3(-4.0, 3.0, 6.0)
- mesh_derived_body.derive_coverage_from_visual_mesh = true
- mesh_derived_body.fishing_depth = 3.0
- mesh_derived_body.fish_pool = PondPool
- var derived_visual := mesh_derived_body.get_node(
- "VisualWater"
- ) as MeshInstance3D
- var derived_mesh := derived_visual.mesh as PlaneMesh
- derived_mesh.size = Vector2(18.0, 11.0)
- root.add_child(mesh_derived_body)
- await process_frame
-
- var derived_shape_node := mesh_derived_body.get_node(
- "FishingRegion/Shape"
- ) as CollisionShape3D
- var derived_shape := derived_shape_node.shape as BoxShape3D
- assert(derived_shape.size.is_equal_approx(Vector3(18.0, 3.0, 11.0)))
- assert(derived_shape_node.position.is_equal_approx(Vector3(0.0, -1.5, 0.0)))
- assert(derived_mesh.size.is_equal_approx(Vector2(18.0, 11.0)))
-
- mesh_derived_body.queue_free()
- await process_frame
-
- var polygon_body := WaterBodyScene.instantiate() as WaterBodyAuthoring
- polygon_body.position = Vector3(30.0, 5.0, 20.0)
- polygon_body.surface_polygon = PackedVector2Array([
- Vector2(-4.0, -4.0),
- Vector2(4.0, -4.0),
- Vector2(4.0, 4.0),
- Vector2(0.0, 1.0),
- Vector2(-4.0, 4.0),
- ])
- polygon_body.visual_surface_enabled = false
- polygon_body.fish_pool = PondPool
- root.add_child(polygon_body)
- await physics_frame
- await physics_frame
-
- var polygon_fishing_region := polygon_body.get_node(
- "FishingRegion"
- ) as FishableWaterRegionType
- var polygon_recovery_region := polygon_body.get_node(
- "RecoveryRegion"
- ) as Area3D
- assert(polygon_fishing_region.get_child_count() == 3)
- assert(polygon_recovery_region.get_child_count() == 3)
- for child: Node in polygon_fishing_region.get_children():
- var polygon_shape_node := child as CollisionShape3D
- assert(polygon_shape_node != null)
- assert(polygon_shape_node.shape is ConvexPolygonShape3D)
-
- var polygon_resolver := FishingSurfaceResolverType.new()
- var polygon_space_state := (
- polygon_body.get_world_3d().direct_space_state
- )
- var polygon_inside: FishingSurfaceSampleType = (
- polygon_resolver.resolve_surface(
- polygon_space_state,
- Vector3(30.0, 0.0, 20.0),
- 8.0,
- )
- )
- assert(polygon_inside.is_fishable())
- assert(is_equal_approx(polygon_inside.position.y, 5.0))
- var polygon_notch: FishingSurfaceSampleType = (
- polygon_resolver.resolve_surface(
- polygon_space_state,
- Vector3(30.0, 0.0, 23.0),
- 8.0,
- )
- )
- assert(not polygon_notch.has_surface)
-
- polygon_body.queue_free()
- await process_frame
-
-
-func _validate_starter_region_surfaces() -> void:
- var region := StarterRegionScene.instantiate() as Node3D
- root.add_child(region)
- var ocean_body := region.get_node(
- "WaterBodies/Ocean"
- ) as WaterBodyAuthoring
- assert(ocean_body != null)
- assert(ocean_body.derive_coverage_from_visual_mesh)
- assert(not ocean_body.manage_recovery_coverage)
- var ocean_region := region.get_node(
- "WaterBodies/Ocean/FishingRegions/OceanFishingRegion"
- ) as FishableWaterRegionType
- assert(ocean_region.get_child_count() == 1)
- var fishing_spot := FishingSpotScene.instantiate() as FishingSpotType
- root.add_child(fishing_spot)
- await physics_frame
- await physics_frame
-
- var pond: FishingSurfaceSampleType = fishing_spot.resolve_fishing_surface(
- Vector3(-9.4, 2.51, 2.1),
- 4.0,
- )
- assert(pond.is_fishable())
- assert(is_equal_approx(pond.position.y, 2.51))
- var ocean: FishingSurfaceSampleType = fishing_spot.resolve_fishing_surface(
- Vector3(50.0, -0.45, 0.0),
- 4.0,
- )
- assert(ocean.is_fishable())
- assert(is_equal_approx(ocean.position.y, -0.45))
- var ocean_visual := region.get_node(
- "WaterBodies/Ocean/VisualWater"
- ) as WaterSurfaceMotion
- assert(ocean_visual != null)
- assert(ocean_visual.is_processing())
- assert(
- absf(ocean_visual.global_position.y - ocean.position.y)
- <= ocean_visual.amplitude + 0.0001
- )
- var ocean_shapes := region.get_node(
- "WaterBodies/Ocean/FishingRegions/OceanFishingRegion"
- ).get_children()
- assert(ocean_shapes.size() == 1)
- var ocean_shape := ocean_shapes[0] as CollisionShape3D
- assert(ocean_shape != null)
- var ocean_box := ocean_shape.shape as BoxShape3D
- assert(ocean_box != null)
- var ocean_mesh := ocean_visual.mesh as PlaneMesh
- assert(ocean_mesh != null)
- assert(ocean_box.size.is_equal_approx(Vector3(
- ocean_mesh.size.x,
- 2.0,
- ocean_mesh.size.y,
- )))
- var expanded_ocean: FishingSurfaceSampleType = (
- fishing_spot.resolve_fishing_surface(
- Vector3(72.0, -0.45, 0.0),
- 4.0,
- )
- )
- assert(expanded_ocean.is_fishable())
- assert(is_equal_approx(expanded_ocean.position.y, -0.45))
- var covered_ocean: FishingSurfaceSampleType = (
- fishing_spot.resolve_fishing_surface(
- Vector3(20.0, -0.45, 0.0),
- 4.0,
- )
- )
- assert(covered_ocean.has_surface)
- assert(not covered_ocean.is_fishable())
- assert(covered_ocean.position.y > -0.45)
-
- # The ocean fishing layer covers the island's complete editable footprint.
- # Terrain at or above the waterline remains authoritative, while shoreline
- # terrain lowered below the surface becomes fishable without reshaping a
- # manually authored exclusion ring.
- var editable_shallows_found := 0
- for x_position: int in range(-24, 24, 2):
- for z_position: int in range(-21, 22, 2):
- var candidate: FishingSurfaceSampleType = (
- fishing_spot.resolve_fishing_surface(
- Vector3(x_position, -0.45, z_position),
- 4.0,
- )
- )
- if (
- candidate.is_fishable()
- and candidate.water_region.water_type
- == WaterType.Type.SALT_WATER
- ):
- editable_shallows_found += 1
- assert(editable_shallows_found > 0)
-
- fishing_spot.queue_free()
- region.queue_free()
- await process_frame
-
-
-func _validate_presentation() -> void:
- var fishing_spot := FishingSpotScene.instantiate() as FishingSpotType
- root.add_child(fishing_spot)
- var presentation := fishing_spot.get_node(
- "FishingPresentation"
- ) as FishingPresentationType
- var rod := Node3D.new()
- var rod_tip := Marker3D.new()
- rod.add_child(rod_tip)
- root.add_child(rod)
- rod_tip.position = Vector3(0.0, 1.0, 0.0)
- await process_frame
-
- var slope_normal := Vector3(0.0, 1.0, 1.0).normalized()
- presentation.begin_aim(
- rod_tip,
- rod,
- Vector3(0.0, 0.1, -1.0),
- slope_normal,
- false,
- )
- var marker := presentation.get_node("CastTargetMarker") as Node3D
- var valid_marker := presentation.get_node(
- "CastTargetMarker/ValidTargetMarker"
- ) as MeshInstance3D
- var invalid_marker := presentation.get_node(
- "CastTargetMarker/InvalidTargetMarker"
- ) as Node3D
- assert(not valid_marker.visible)
- assert(invalid_marker.visible)
- assert(invalid_marker.get_child_count() == 2)
- assert(marker.global_basis.y.normalized().dot(slope_normal) > 0.999)
-
- var landing := Vector3(0.0, 0.13, -4.0)
- presentation.begin_cast(landing, landing, true)
- await presentation.cast_completed
- var bobber := presentation.get_node("Bobber") as MeshInstance3D
- assert(bobber.visible)
- assert(bobber.global_position.is_equal_approx(landing))
- presentation.play_outcome(&"invalid")
- await process_frame
- assert(bobber.visible)
- await presentation.outcome_completed
- assert(not bobber.visible)
-
- rod.queue_free()
- fishing_spot.queue_free()
- await process_frame
-
-
-func _validate_bite_wait_distribution() -> void:
- var fishing_spot := FishingSpotScene.instantiate() as FishingSpotType
- root.add_child(fishing_spot)
- await process_frame
- var bite_rng := fishing_spot.get("_bite_rng") as RandomNumberGenerator
- bite_rng.seed = 84219
- var quick_count: int = 0
- var typical_or_long_count: int = 0
- var very_long_count: int = 0
- var total_wait_seconds: float = 0.0
- for _sample_index: int in 10000:
- var wait_seconds: float = fishing_spot.roll_bite_wait_time()
- assert(wait_seconds >= FishingSpotType.BITE_QUICK_MIN_SECONDS)
- assert(wait_seconds <= FishingSpotType.BITE_MAX_SECONDS)
- total_wait_seconds += wait_seconds
- if wait_seconds < FishingSpotType.BITE_QUICK_MAX_SECONDS:
- quick_count += 1
- else:
- typical_or_long_count += 1
- if wait_seconds >= FishingSpotType.BITE_LONG_MAX_SECONDS:
- very_long_count += 1
- var average_wait_seconds: float = total_wait_seconds / 10000.0
- var quick_ratio: float = float(quick_count) / 10000.0
- var very_long_ratio: float = float(very_long_count) / 10000.0
- var very_long_probability: float = 1.0 - (
- FishingSpotType.BITE_QUICK_PROBABILITY
- + FishingSpotType.BITE_TYPICAL_PROBABILITY
- + FishingSpotType.BITE_LONG_PROBABILITY
- )
- var expected_average_wait_seconds: float = (
- FishingSpotType.BITE_QUICK_PROBABILITY
- * (
- FishingSpotType.BITE_QUICK_MIN_SECONDS
- + FishingSpotType.BITE_QUICK_MAX_SECONDS
- )
- * 0.5
- + FishingSpotType.BITE_TYPICAL_PROBABILITY
- * (
- FishingSpotType.BITE_QUICK_MAX_SECONDS
- + FishingSpotType.BITE_TYPICAL_MAX_SECONDS
- )
- * 0.5
- + FishingSpotType.BITE_LONG_PROBABILITY
- * (
- FishingSpotType.BITE_TYPICAL_MAX_SECONDS
- + FishingSpotType.BITE_LONG_MAX_SECONDS
- )
- * 0.5
- + very_long_probability
- * (
- FishingSpotType.BITE_LONG_MAX_SECONDS
- + FishingSpotType.BITE_MAX_SECONDS
- )
- * 0.5
- )
- assert(absf(
- quick_ratio - FishingSpotType.BITE_QUICK_PROBABILITY
- ) <= 0.02)
- assert(typical_or_long_count > quick_count)
- assert(absf(very_long_ratio - very_long_probability) <= 0.005)
- assert(absf(
- average_wait_seconds - expected_average_wait_seconds
- ) <= 1.0)
- fishing_spot.queue_free()
- await process_frame
-
-
-func _validate_remote_presentation() -> void:
- var player := PlayerScene.instantiate() as Player
- root.add_child(player)
- player.set_local_control(false)
- var presentation := RemoteFishingPresentationType.new()
- root.add_child(presentation)
- presentation.setup(player)
- await process_frame
- var origin: Vector3 = player.get_fishing_rod_tip().global_position
- var target: Vector3 = origin + Vector3(-4.0, -0.5, 0.0)
- presentation.show_cast(origin, target, "cast-attempt")
- var bobber := presentation.get("_bobber") as MeshInstance3D
- assert(bobber.visible)
- assert(presentation.get("_cast_tween") != null)
- await create_timer(0.7).timeout
- assert(bobber.global_position.distance_to(target) < 0.1)
- var first_bob_y: float = bobber.global_position.y
- await create_timer(0.2).timeout
- assert(not is_equal_approx(bobber.global_position.y, first_bob_y))
-
- # A client joining after the reliable cast event reconstructs the current
- # remote fishing state from the observer summary instead of waiting for the
- # next cast. This also covers a bite/fighting transition received by an
- # already active observer.
- presentation.cleanup()
- presentation.synchronize_active("late-attempt", target, false)
- assert(bobber.visible)
- assert(
- int(player.get("_fishing_visual_phase"))
- == Player.FishingVisualPhase.FISHING
- )
- presentation.synchronize_active("late-attempt", target, true)
- assert(bool(player.get("_fighting_visual_active")))
- presentation.synchronize_active("late-attempt", target, false)
- assert(not bool(player.get("_fighting_visual_active")))
-
- var fish_catch := FishCatchType.new()
- var fish: FishData = PondPool.candidates.front()
- fish_catch.fish = fish
- fish_catch.fish_id = fish.id
- fish_catch.catch_id = &"remote-presentation-test"
- fish_catch.weight_lb = 1.0
- fish_catch.display_scale = 1.0
- fish_catch.sale_value = 1
- assert(fish_catch.is_valid())
- presentation.play_return(fish_catch)
- var catch_display := player.find_child(
- "CatchDisplay", true, false
- ) as Node3D
- assert(catch_display != null)
- var showcase_deadline_msec: int = Time.get_ticks_msec() + 2000
- while (
- not catch_display.visible
- and Time.get_ticks_msec() < showcase_deadline_msec
- ):
- await process_frame
- assert(catch_display.visible)
- await presentation.return_completed
- assert(not catch_display.visible)
-
- presentation.queue_free()
- player.queue_free()
- await process_frame
-
-
-func _add_water_region(
- parent: Node3D,
- position: Vector3,
- surface_size: Vector2,
- fish_pool: FishPoolType,
-) -> void:
- var water_root := Node3D.new()
- water_root.position = position
- parent.add_child(water_root)
- var region := FishableWaterRegionType.new()
- region.collision_layer = 4
- region.collision_mask = 0
- region.monitoring = false
- region.surface_height_mode = FishableWaterRegionType.SurfaceHeightMode.PARENT_GLOBAL_Y
- region.fish_pool = fish_pool
- water_root.add_child(region)
- var shape_node := CollisionShape3D.new()
- shape_node.position.y = -2.0
- var shape := BoxShape3D.new()
- shape.size = Vector3(surface_size.x, 4.0, surface_size.y)
- shape_node.shape = shape
- region.add_child(shape_node)
-
-
-func _add_solid_box(
- parent: Node3D,
- size: Vector3,
- position: Vector3,
-) -> void:
- var body := StaticBody3D.new()
- body.position = position
- body.collision_layer = 1
- body.collision_mask = 0
- parent.add_child(body)
- var shape_node := CollisionShape3D.new()
- var shape := BoxShape3D.new()
- shape.size = size
- shape_node.shape = shape
- body.add_child(shape_node)
diff --git a/tests/fishing_surface_validation.gd.uid b/tests/fishing_surface_validation.gd.uid
deleted file mode 100644
index 3954c8a..0000000
--- a/tests/fishing_surface_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bh8g62bagphbd
diff --git a/tests/friend_multiplayer_validation.gd b/tests/friend_multiplayer_validation.gd
deleted file mode 100644
index f8ef64d..0000000
--- a/tests/friend_multiplayer_validation.gd
+++ /dev/null
@@ -1,161 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-const TEST_PORT: int = 18196
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var arguments := OS.get_cmdline_user_args()
- if arguments.has("host"):
- await _run_host()
- return
- if arguments.has("client"):
- await _run_client()
- return
- push_error("Friend multiplayer validation needs host or client mode.")
- quit(1)
-
-
-func _run_host() -> void:
- var main := await _create_initialized_main()
- var session := main.get_node("%NetworkSession") as NetworkSession
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(bool(main.call(
- "_apply_world", WorldLayout.STARTER_ISLAND, 1, true
- )))
- assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
- assert(session.start_private_host(TEST_PORT))
- assert(save_manager.initialize_new_game())
- main.call("_enter_gameplay")
- for _frame: int in 4:
- await physics_frame
- assert(session.set_host_open(true))
-
- var remote_peer_id := await _wait_for_remote_peer(session)
- assert(remote_peer_id > 1)
- assert(session.peer_supports_capability(
- remote_peer_id, NetworkProtocol.FRIENDS_CAPABILITY
- ))
- var remote_record := session.get_peer_record(remote_peer_id)
- assert(remote_record != null)
- var game_ui := main.get_node("%GameUI") as GameUI
- var prompt_deadline := Time.get_ticks_msec() + 8000
- while (
- Time.get_ticks_msec() < prompt_deadline
- and not game_ui.is_social_prompt_open()
- ):
- await process_frame
- assert(game_ui.is_social_prompt_open())
- await create_timer(0.15).timeout
- game_ui.call("_accept_social_prompt")
-
- var relationships := main.get_node(
- "%PlayerRelationshipStore"
- ) as PlayerRelationshipStore
- var accepted_deadline := Time.get_ticks_msec() + 8000
- while (
- Time.get_ticks_msec() < accepted_deadline
- and not relationships.is_friend(remote_record.identity_fingerprint)
- ):
- await process_frame
- assert(relationships.is_friend(remote_record.identity_fingerprint))
- var friend := relationships.get_friend_record(
- remote_record.identity_fingerprint
- )
- assert(not str(friend.get("remote_presence_channel", "")).is_empty())
- assert(not str(friend.get("remote_invite_token", "")).is_empty())
-
- var disconnect_deadline := Time.get_ticks_msec() + 8000
- while (
- Time.get_ticks_msec() < disconnect_deadline
- and session.is_authenticated_peer(remote_peer_id)
- ):
- await process_frame
- assert(not session.is_authenticated_peer(remote_peer_id))
- print("Friend multiplayer host validation: PASS")
- await _cleanup(main, session)
-
-
-func _run_client() -> void:
- var main := await _create_initialized_main()
- main.call(
- "_on_title_join_game_requested",
- "127.0.0.1:%d" % TEST_PORT,
- )
- var session := main.get_node("%NetworkSession") as NetworkSession
- var join_deadline := Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < join_deadline:
- await process_frame
- if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
- main.call("_confirm_server_trust")
- if session.is_joined_client() and bool(main.get("_gameplay_started")):
- break
- assert(session.is_joined_client())
- assert(session.supports_server_capability(
- NetworkProtocol.FRIENDS_CAPABILITY
- ))
- var host_record := session.get_peer_record(1)
- assert(host_record != null)
- var service := main.get_node(
- "%NetworkPlayerListService"
- ) as NetworkPlayerListService
- assert(service.send_friend_request(
- 1,
- host_record.identity_fingerprint,
- host_record.display_name,
- ))
-
- var relationships := main.get_node(
- "%PlayerRelationshipStore"
- ) as PlayerRelationshipStore
- var accepted_deadline := Time.get_ticks_msec() + 8000
- while (
- Time.get_ticks_msec() < accepted_deadline
- and not relationships.is_friend(host_record.identity_fingerprint)
- ):
- await process_frame
- assert(relationships.is_friend(host_record.identity_fingerprint))
- var friend := relationships.get_friend_record(
- host_record.identity_fingerprint
- )
- assert(not str(friend.get("remote_presence_channel", "")).is_empty())
- assert(not str(friend.get("remote_invite_token", "")).is_empty())
- print("Friend multiplayer client validation: PASS")
- await _cleanup(main, session)
-
-
-func _create_initialized_main() -> Node:
- root.size = Vector2i(1280, 720)
- var main := MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- return main
-
-
-func _wait_for_remote_peer(session: NetworkSession) -> int:
- var deadline := Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- for peer_id: int in session.get_authenticated_peer_ids():
- if peer_id != session.get_local_peer_id():
- return peer_id
- return 0
-
-
-func _cleanup(main: Node, session: NetworkSession) -> void:
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
diff --git a/tests/friend_multiplayer_validation.gd.uid b/tests/friend_multiplayer_validation.gd.uid
deleted file mode 100644
index d243a6b..0000000
--- a/tests/friend_multiplayer_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cxltqyom4yt3j
diff --git a/tests/friend_relationship_validation.gd b/tests/friend_relationship_validation.gd
deleted file mode 100644
index ce6bd90..0000000
--- a/tests/friend_relationship_validation.gd
+++ /dev/null
@@ -1,76 +0,0 @@
-extends SceneTree
-
-const FRIEND_FINGERPRINT := (
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
-)
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var data_root := PlayerDataRoot.new()
- root.add_child(data_root)
- var portable_path := ProjectSettings.globalize_path(
- "user://friend-relationship-validation"
- )
- var created := data_root.create_unbound_root(portable_path)
- assert(bool(created.get("ok", false)))
- assert(data_root.activate_process_root(portable_path))
-
- var store := PlayerRelationshipStore.new()
- root.add_child(store)
- store.configure_storage(
- data_root.path_for(&"player_relationships"), data_root
- )
- var local_capabilities := store.create_friend_capabilities()
- var remote_capabilities := store.create_friend_capabilities()
- assert(not local_capabilities.is_empty())
- assert(not remote_capabilities.is_empty())
- assert(store.add_friend(
- FRIEND_FINGERPRINT,
- "Pond Friend",
- local_capabilities,
- {
- "presence_channel": remote_capabilities["presence_channel"],
- "invite_token": remote_capabilities["invite_token"],
- },
- ))
- assert(store.is_friend(FRIEND_FINGERPRINT))
-
- var reloaded := PlayerRelationshipStore.new()
- root.add_child(reloaded)
- reloaded.configure_storage(
- data_root.path_for(&"player_relationships"), data_root
- )
- assert(reloaded.is_friend(FRIEND_FINGERPRINT))
- var friend := reloaded.get_friend_record(FRIEND_FINGERPRINT)
- assert(
- str(friend["remote_presence_channel"])
- == str(remote_capabilities["presence_channel"])
- )
- assert(
- str(friend["remote_invite_token"])
- == str(remote_capabilities["invite_token"])
- )
- assert(reloaded.set_blocked(FRIEND_FINGERPRINT, "Pond Friend", true))
- assert(reloaded.is_blocked(FRIEND_FINGERPRINT))
- assert(reloaded.is_muted(FRIEND_FINGERPRINT))
- assert(not reloaded.is_friend(FRIEND_FINGERPRINT))
- assert(reloaded.set_blocked(FRIEND_FINGERPRINT, "Pond Friend", false))
- assert(not reloaded.is_blocked(FRIEND_FINGERPRINT))
- assert(not reloaded.is_muted(FRIEND_FINGERPRINT))
- assert(not reloaded.is_friend(FRIEND_FINGERPRINT))
-
- var hello := NetworkProtocol.make_client_hello(
- "profile",
- "Pond Friend",
- "nonce",
- )
- assert(
- NetworkProtocol.FRIENDS_CAPABILITY
- in PackedStringArray(hello.get("capability_flags", []))
- )
- print("Friend relationship validation: PASS")
- quit()
diff --git a/tests/friend_relationship_validation.gd.uid b/tests/friend_relationship_validation.gd.uid
deleted file mode 100644
index aa2acaf..0000000
--- a/tests/friend_relationship_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://d3v4e1xtu8ytq
diff --git a/tests/fur_pattern_validation.gd b/tests/fur_pattern_validation.gd
deleted file mode 100644
index a795d53..0000000
--- a/tests/fur_pattern_validation.gd
+++ /dev/null
@@ -1,437 +0,0 @@
-extends SceneTree
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- CharacterCustomizationCatalog.refresh_fur_pattern_assets()
- var legacy_snapshot := CharacterCustomizationCatalog.default_snapshot()
- legacy_snapshot[CharacterCustomizationCatalog.FUR_STYLE_ID] = "spots_bengal"
- for added_id: String in [
- CharacterCustomizationCatalog.FUR_STYLE_ARMS_ID,
- CharacterCustomizationCatalog.FUR_STYLE_HEAD_ID,
- CharacterCustomizationCatalog.FUR_STYLE_EARS_ID,
- CharacterCustomizationCatalog.FUR_STYLE_TAIL_ID,
- ]:
- legacy_snapshot.erase(added_id)
- var migrated := CharacterCustomizationCatalog.sanitized_snapshot(
- legacy_snapshot
- )
- assert(CharacterCustomizationCatalog.validate_snapshot(migrated))
- for style_field: String in CharacterCustomizationCatalog.FUR_STYLE_IDS:
- assert(migrated[style_field] == "bengal")
-
- var fur_colors := CharacterCustomizationCatalog.options_for("fur_pattern")
- assert(fur_colors.size() == 30)
- for natural_color_id: String in [
- "ivory",
- "sand",
- "tan",
- "taupe",
- "black",
- "dark_brown",
- "chocolate",
- "chestnut",
- "ginger",
- "sage",
- "forest",
- ]:
- assert(CharacterCustomizationCatalog.is_valid_option(
- "fur_pattern",
- natural_color_id,
- ))
- var custom_color := Color("2b91c7")
- var custom_color_id := CharacterCustomizationCatalog.custom_fur_color_id(
- custom_color
- )
- assert(custom_color_id == "custom_2b91c7")
- assert(
- CharacterCustomizationCatalog.custom_fur_color_id(
- Color(custom_color.r, custom_color.g, custom_color.b, 0.2)
- )
- == custom_color_id
- )
- for color_field: String in CharacterCustomizationCatalog.FUR_COLOR_IDS:
- assert(CharacterCustomizationCatalog.is_valid_option(
- color_field,
- custom_color_id,
- ))
- assert(
- CharacterCustomizationCatalog.option_color(
- color_field, custom_color_id
- ).is_equal_approx(custom_color)
- )
- assert(not CharacterCustomizationCatalog.is_valid_option(
- "fur_pattern", "custom_2b91cg"
- ))
- assert(not CharacterCustomizationCatalog.is_valid_option(
- "fur_pattern", "custom_2b91c7ff"
- ))
-
- var pattern_ids: Array[String] = []
- for option: Dictionary in CharacterCustomizationCatalog.options_for(
- CharacterCustomizationCatalog.FUR_STYLE_ID
- ):
- pattern_ids.append(str(option.get("id", "")))
- assert(pattern_ids == [
- "solid",
- "bengal",
- "calico",
- "fox",
- "paws",
- "stripes",
- "tiger",
- "tummy",
- ])
-
- var patterned := CharacterCustomizationCatalog.default_snapshot()
- patterned["species"] = "round"
- patterned["ears"] = "pointy_short"
- patterned["tail"] = "fox"
- patterned["fur_pattern"] = "orange"
- patterned[CharacterCustomizationCatalog.FUR_STYLE_ID] = "calico"
- patterned[CharacterCustomizationCatalog.FUR_STYLE_ARMS_ID] = "paws"
- patterned[CharacterCustomizationCatalog.FUR_STYLE_HEAD_ID] = "tiger"
- patterned[CharacterCustomizationCatalog.FUR_STYLE_EARS_ID] = "fox"
- patterned[CharacterCustomizationCatalog.FUR_STYLE_TAIL_ID] = "fox"
- patterned["fur_color_2"] = "cream"
- patterned["fur_color_3"] = "brown"
- patterned["fur_color_4"] = "red"
- assert(CharacterCustomizationCatalog.validate_snapshot(patterned))
-
- _assert_option_ids(
- CharacterCustomizationCatalog.fur_pattern_options_for_field(
- CharacterCustomizationCatalog.FUR_STYLE_ID, patterned
- ),
- ["solid", "bengal", "calico", "fox", "paws", "stripes", "tiger", "tummy"],
- )
- _assert_option_ids(
- CharacterCustomizationCatalog.fur_pattern_options_for_field(
- CharacterCustomizationCatalog.FUR_STYLE_ARMS_ID, patterned
- ),
- ["solid", "bengal", "fox", "paws"],
- )
- _assert_option_ids(
- CharacterCustomizationCatalog.fur_pattern_options_for_field(
- CharacterCustomizationCatalog.FUR_STYLE_HEAD_ID, patterned
- ),
- ["solid", "bengal", "fox", "tiger"],
- )
- _assert_option_ids(
- CharacterCustomizationCatalog.fur_pattern_options_for_field(
- CharacterCustomizationCatalog.FUR_STYLE_EARS_ID, patterned
- ),
- ["solid", "fox"],
- )
- _assert_option_ids(
- CharacterCustomizationCatalog.fur_pattern_options_for_field(
- CharacterCustomizationCatalog.FUR_STYLE_TAIL_ID, patterned
- ),
- ["solid", "fox"],
- )
- var pointy_snapshot := patterned.duplicate(true)
- pointy_snapshot["species"] = "pointy"
- _assert_option_ids(
- CharacterCustomizationCatalog.fur_pattern_options_for_field(
- CharacterCustomizationCatalog.FUR_STYLE_HEAD_ID, pointy_snapshot
- ),
- ["solid", "fox"],
- )
- var no_accessories := patterned.duplicate(true)
- no_accessories["ears"] = "none"
- no_accessories["tail"] = "none"
- _assert_option_ids(
- CharacterCustomizationCatalog.fur_pattern_options_for_field(
- CharacterCustomizationCatalog.FUR_STYLE_EARS_ID, no_accessories
- ),
- ["solid"],
- )
- _assert_option_ids(
- CharacterCustomizationCatalog.fur_pattern_options_for_field(
- CharacterCustomizationCatalog.FUR_STYLE_TAIL_ID, no_accessories
- ),
- ["solid"],
- )
-
- var body_texture := _assert_pattern_texture("calico", "body_main")
- var arms_texture := _assert_pattern_texture("paws", "body_arms")
- var head_texture := _assert_pattern_texture("tiger", "head_round")
- var ears_texture := _assert_pattern_texture("fox", "ears_pointy_short")
- var tail_texture := _assert_pattern_texture("fox", "tails_fox")
- _assert_pattern_texture("tummy", "body_main")
- _assert_pattern_texture("stripes", "body_main")
- _assert_pattern_texture("fox", "head_pointy")
-
- var visuals := PlayerVisualPresenter.instantiate_visuals()
- root.add_child(visuals)
- PlayerVisualPresenter.apply_appearance(visuals, patterned)
- var skeleton := visuals.find_child("Skeleton3D", true, false) as Skeleton3D
- assert(skeleton != null)
- var body := skeleton.get_node_or_null("body_main") as MeshInstance3D
- assert(body != null)
- var body_arrays := body.mesh.surface_get_arrays(0)
- var body_uvs := body_arrays[Mesh.ARRAY_TEX_UV] as PackedVector2Array
- assert(not body_uvs.is_empty())
- var uv_min := Vector2(INF, INF)
- var uv_max := Vector2(-INF, -INF)
- for uv: Vector2 in body_uvs:
- uv_min = uv_min.min(uv)
- uv_max = uv_max.max(uv)
- assert(uv_min.x < 0.05 and uv_min.y < 0.05)
- assert(uv_max.x > 0.95 and uv_max.y > 0.95)
-
- var body_material := _assert_mesh_pattern(
- skeleton, "body_main", body_texture
- )
- var arms_material := _assert_mesh_pattern(
- skeleton, "body_arms", arms_texture
- )
- var head_material := _assert_mesh_pattern(
- skeleton, "head_round", head_texture
- )
- var ears_material := _assert_mesh_pattern(
- skeleton, "ears_pointy_short", ears_texture
- )
- var tail_material := _assert_mesh_pattern(
- skeleton, "tails_fox", tail_texture
- )
- for material: ShaderMaterial in [
- body_material,
- arms_material,
- head_material,
- ears_material,
- tail_material,
- ]:
- assert(material.shader == preload("res://player/fur_pattern.gdshader"))
- assert(material.get_shader_parameter("base_color") == Color("c86c36"))
- assert(material.get_shader_parameter("red_zone_color") == Color("e6d39b"))
- assert(material.get_shader_parameter("green_zone_color") == Color("7b4a32"))
- assert(material.get_shader_parameter("blue_zone_color") == Color("a9433f"))
-
- var recolored := patterned.duplicate(true)
- recolored["fur_color_3"] = "teal"
- PlayerVisualPresenter.apply_appearance(visuals, recolored)
- for material: ShaderMaterial in [
- body_material,
- arms_material,
- head_material,
- ears_material,
- tail_material,
- ]:
- assert(material.get_shader_parameter("green_zone_color") == Color("3a8790"))
- assert(
- NetworkProfileProtocol.signature_fields({"appearance": patterned})
- != NetworkProfileProtocol.signature_fields({"appearance": recolored})
- )
- var other_arms := patterned.duplicate(true)
- other_arms[CharacterCustomizationCatalog.FUR_STYLE_ARMS_ID] = "fox"
- assert(
- NetworkProfileProtocol.signature_fields({"appearance": patterned})
- != NetworkProfileProtocol.signature_fields({"appearance": other_arms})
- )
- var custom_snapshot := patterned.duplicate(true)
- custom_snapshot["fur_pattern"] = custom_color_id
- assert(CharacterCustomizationCatalog.validate_snapshot(custom_snapshot))
- PlayerVisualPresenter.apply_appearance(visuals, custom_snapshot)
- for material: ShaderMaterial in [
- body_material,
- arms_material,
- head_material,
- ears_material,
- tail_material,
- ]:
- assert(
- (material.get_shader_parameter("base_color") as Color).is_equal_approx(
- custom_color
- )
- )
- assert(
- NetworkProfileProtocol.signature_fields({"appearance": patterned})
- != NetworkProfileProtocol.signature_fields({"appearance": custom_snapshot})
- )
-
- var solid := CharacterCustomizationCatalog.default_snapshot()
- PlayerVisualPresenter.apply_appearance(visuals, solid)
- assert(body.material_override is StandardMaterial3D)
- await _validate_fur_color_ui(patterned)
-
- print("Fur pattern validation: PASS")
- visuals.queue_free()
- await process_frame
- quit()
-
-
-func _validate_fur_color_ui(snapshot: Dictionary) -> void:
- var profile_page := ProfilePage.new()
- profile_page.size = Vector2(1280.0, 720.0)
- root.add_child(profile_page)
- await process_frame
- profile_page.set("_draft_appearance", snapshot.duplicate(true))
- profile_page.call("_select_category", "fur_pattern")
- profile_page.call("_select_fur_section", "colors")
- await process_frame
- await process_frame
- var color_panel := profile_page.find_child(
- "FurColorOptionsPanel", true, false
- ) as PanelContainer
- var channel_grid := profile_page.find_child(
- "FurColorChannelGrid", true, false
- ) as GridContainer
- var palette_grid := profile_page.find_child(
- "FurPaletteGrid", true, false
- ) as GridContainer
- var picker := profile_page.find_child(
- "FurCustomColorPicker", true, false
- ) as ColorPickerButton
- var picker_popup := picker.get_popup()
- var picker_control := picker.get_picker()
- var option_list := profile_page.get("_option_list") as Control
- assert(color_panel != null and option_list != null)
- assert(color_panel.size.x > 0.0)
- assert(color_panel.size.x <= option_list.size.x + 0.5)
- assert(channel_grid != null and channel_grid.columns == 2)
- assert(channel_grid.get_child_count() == 4)
- assert(palette_grid != null and palette_grid.columns == 6)
- assert(palette_grid.get_child_count() == 30)
- for option_button: Button in palette_grid.get_children():
- assert(
- option_button.custom_minimum_size
- == Vector2.ONE * ProfilePage.FUR_PALETTE_SWATCH_SIZE
- )
- for color_index: int in CharacterCustomizationCatalog.FUR_COLOR_IDS.size():
- var category_id: String = (
- CharacterCustomizationCatalog.FUR_COLOR_IDS[color_index]
- )
- var channel_button := profile_page.find_child(
- "FurColorSlot%d" % (color_index + 1), true, false
- ) as Button
- assert(channel_button != null)
- var swatch := channel_button.get_node(
- "ChannelContents/ActiveColorSwatch"
- ) as Panel
- var channel_label := channel_button.get_node(
- "ChannelContents/ChannelNumber"
- ) as Label
- assert(swatch.size.is_equal_approx(
- Vector2.ONE * ProfilePage.FUR_CHANNEL_SWATCH_SIZE
- ))
- assert(swatch.size.y <= channel_label.size.y)
- var swatch_style := swatch.get_theme_stylebox(
- "panel"
- ) as StyleBoxFlat
- assert(swatch_style != null)
- assert(
- swatch_style.corner_radius_top_left
- == roundi(ProfilePage.FUR_CHANNEL_SWATCH_SIZE * 0.5)
- )
- assert(swatch_style.bg_color.is_equal_approx(
- CharacterCustomizationCatalog.option_color(
- category_id,
- str(snapshot[category_id]),
- )
- ))
- var picker_style := picker.get_theme_stylebox("normal") as StyleBoxFlat
- assert(picker_style != null and picker_style.corner_radius_top_left == 14)
- var picker_window_size: Vector2i = profile_page.get_window().size
- var expected_picker_size := Vector2i(
- mini(
- ProfilePage.FUR_COLOR_PICKER_POPUP_SIZE.x,
- maxi(picker_window_size.x - 32, 420),
- ),
- mini(
- ProfilePage.FUR_COLOR_PICKER_POPUP_SIZE.y,
- maxi(picker_window_size.y - 32, 340),
- ),
- )
- assert(picker_popup.min_size == expected_picker_size)
- assert(picker_popup.theme.default_font == UtilityPageStyle.TuffyFont)
- var popup_style := picker_popup.get_theme_stylebox(
- "panel"
- ) as StyleBoxFlat
- assert(popup_style != null)
- assert(popup_style.bg_color.a == 1.0)
- assert(popup_style.corner_radius_top_left == 20)
- assert(
- picker_control.get_theme_constant("sv_width")
- == mini(
- ProfilePage.FUR_COLOR_PICKER_SV_SIZE.x,
- expected_picker_size.x - 110,
- )
- )
- assert(
- picker_control.get_theme_constant("sv_height")
- == mini(
- ProfilePage.FUR_COLOR_PICKER_SV_SIZE.y,
- expected_picker_size.y - 180,
- )
- )
- assert(not picker_control.edit_alpha)
- assert(not picker_control.edit_intensity)
- var picker_display := picker.get_node("FurCustomColorDisplay") as Panel
- var display_style := picker_display.get_theme_stylebox(
- "panel"
- ) as StyleBoxFlat
- assert(display_style != null and display_style.corner_radius_top_left == 11)
- var live_custom_color := Color("2b91c7")
- profile_page.call(
- "_select_custom_fur_color",
- live_custom_color,
- CharacterCustomizationCatalog.FUR_COLOR_IDS[0],
- )
- var first_swatch := profile_page.find_child(
- "FurColorSlot1", true, false
- ).get_node("ChannelContents/ActiveColorSwatch") as Panel
- var first_swatch_style := first_swatch.get_theme_stylebox(
- "panel"
- ) as StyleBoxFlat
- assert(first_swatch_style.bg_color.is_equal_approx(live_custom_color))
- picker_popup.popup_centered()
- for _frame: int in 2:
- await process_frame
- picker_popup.hide()
- for _frame: int in 3:
- await process_frame
- assert(is_instance_valid(picker))
- assert(picker.is_inside_tree())
- assert(
- profile_page.find_child(
- "FurCustomColorPicker", true, false
- ) == picker
- )
- profile_page.free()
- await process_frame
-
-
-func _assert_option_ids(options: Array, expected: Array[String]) -> void:
- var actual: Array[String] = []
- for option: Dictionary in options:
- actual.append(str(option.get("id", "")))
- assert(actual == expected)
-
-
-func _assert_pattern_texture(
- pattern_id: String,
- component_id: String,
-) -> Texture2D:
- var texture := CharacterCustomizationCatalog.fur_pattern_texture(
- pattern_id, component_id
- )
- assert(texture != null)
- assert(texture.get_width() == 1024)
- assert(texture.get_height() == 1024)
- return texture
-
-
-func _assert_mesh_pattern(
- skeleton: Skeleton3D,
- mesh_name: String,
- expected_texture: Texture2D,
-) -> ShaderMaterial:
- var mesh := skeleton.get_node_or_null(mesh_name) as MeshInstance3D
- assert(mesh != null)
- var material := mesh.material_override as ShaderMaterial
- assert(material != null)
- assert(material.get_shader_parameter("pattern_texture") == expected_texture)
- return material
diff --git a/tests/fur_pattern_validation.gd.uid b/tests/fur_pattern_validation.gd.uid
deleted file mode 100644
index 7333f9d..0000000
--- a/tests/fur_pattern_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bwdmyn34biahx
diff --git a/tests/gathering_marker_surface_validation.gd b/tests/gathering_marker_surface_validation.gd
deleted file mode 100644
index 72f15f0..0000000
--- a/tests/gathering_marker_surface_validation.gd
+++ /dev/null
@@ -1,101 +0,0 @@
-extends SceneTree
-
-const PlayerScene: PackedScene = preload("res://player/player.tscn")
-const GatheringControllerType = preload(
- "res://gathering/gathering_controller.gd"
-)
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var player := PlayerScene.instantiate() as Player
- root.add_child(player)
- player.set_process(false)
- player.set_physics_process(false)
-
- var controller := GatheringControllerType.new()
- root.add_child(controller)
- controller.set("_player", player)
-
- var wall := _make_surface(
- Vector3(2.0, 2.0, 0.1),
- Vector3(0.0, 0.9, -1.1),
- )
- root.add_child(wall)
- await physics_frame
- controller.call("_update_marker_target")
- var marker := controller.get("_marker") as MeshInstance3D
- assert(marker != null)
- var disc := marker.mesh as CylinderMesh
- assert(disc != null)
- assert(is_equal_approx(disc.top_radius, controller.marker_radius))
- assert(is_equal_approx(disc.bottom_radius, controller.marker_radius))
- var invalid_material := controller.get(
- "_marker_invalid_material"
- ) as StandardMaterial3D
- var valid_material := controller.get(
- "_marker_valid_material"
- ) as StandardMaterial3D
- assert(invalid_material != null)
- assert(valid_material != null)
- assert(invalid_material.albedo_color == Color.WHITE)
- assert(is_equal_approx(invalid_material.albedo_color.a, 1.0))
- assert(is_equal_approx(valid_material.albedo_color.a, 1.0))
- assert(invalid_material.no_depth_test)
- assert(valid_material.no_depth_test)
- assert(
- invalid_material.shading_mode
- == BaseMaterial3D.SHADING_MODE_UNSHADED
- )
- assert(
- valid_material.shading_mode
- == BaseMaterial3D.SHADING_MODE_UNSHADED
- )
- assert(bool(controller.get("_marker_has_surface")))
- assert(marker.global_basis.y.dot(Vector3.BACK) > 0.99)
-
- wall.queue_free()
- await process_frame
- var floor := _make_surface(
- Vector3(10.0, 0.2, 10.0),
- Vector3(0.0, -0.1, -1.7),
- )
- root.add_child(floor)
- await physics_frame
- controller.call("_update_marker_target")
- assert(bool(controller.get("_marker_has_surface")))
- assert(marker.global_basis.y.dot(Vector3.UP) > 0.99)
- assert(is_equal_approx(absf(marker.global_basis.determinant()), 1.0))
- var horizontal_marker_offset := Vector2(
- marker.global_position.x - player.global_position.x,
- marker.global_position.z - player.global_position.z,
- ).length()
- assert(
- is_equal_approx(
- horizontal_marker_offset,
- controller.marker_distance,
- )
- )
-
- floor.queue_free()
- controller.queue_free()
- player.queue_free()
- await process_frame
- print("Gathering marker surface validation: PASS")
- quit()
-
-
-func _make_surface(size: Vector3, position: Vector3) -> StaticBody3D:
- var body := StaticBody3D.new()
- body.position = position
- body.collision_layer = 1
- body.collision_mask = 0
- var collision := CollisionShape3D.new()
- var shape := BoxShape3D.new()
- shape.size = size
- collision.shape = shape
- body.add_child(collision)
- return body
diff --git a/tests/gathering_marker_surface_validation.gd.uid b/tests/gathering_marker_surface_validation.gd.uid
deleted file mode 100644
index 044defc..0000000
--- a/tests/gathering_marker_surface_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ddsf88kuxgsqp
diff --git a/tests/gathering_showcase_validation.gd b/tests/gathering_showcase_validation.gd
deleted file mode 100644
index 51d6f38..0000000
--- a/tests/gathering_showcase_validation.gd
+++ /dev/null
@@ -1,143 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-const TEST_PORT: int = 18142
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
-
- var session := main.get_node("%NetworkSession") as NetworkSession
- assert(bool(main.call(
- "_apply_world", WorldLayout.STARTER_ISLAND, 1, true
- )))
- assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
- assert(session.start_private_host(TEST_PORT))
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND))
- main.call("_enter_gameplay")
- for _frame: int in 8:
- await process_frame
-
- var player := main.get("_player") as Player
- var fishing_spot := main.get_node("%FishingSpot") as FishingSpot
- var game_ui := main.get_node("%GameUI") as GameUI
- var catalog := main.get("fish_catalog") as FishPool
- assert(
- player != null
- and fishing_spot != null
- and game_ui != null
- and catalog != null
- )
- fishing_spot.minimum_showcase_duration = 1.0
- assert(player.bag.add_item(&"crab_net"))
- assert(player.hotbar.assign_item(0, &"crab_net"))
- assert(player.hotbar.select_slot(0))
- await process_frame
- assert(player.hotbar.get_selected_item_id() == &"crab_net")
- assert(bool(player.get("_active_item_is_net")))
-
- var crab: FishData = catalog.get_fish_by_id(&"crab_brown")
- assert(crab != null and crab.is_selectable())
- var crab_catch := FishCatch.new()
- crab_catch.fish = crab
- crab_catch.fish_id = crab.id
- crab_catch.weight_lb = crab.get_minimum_weight()
- crab_catch.display_scale = crab.get_display_scale_for_weight(
- crab_catch.weight_lb
- )
- crab_catch.sale_value = crab.get_sale_value_for_weight(
- crab_catch.weight_lb
- )
- crab_catch.ensure_identity()
- player.inventory.add_catch(crab_catch)
- player.collection_log.mark_discovered(crab.id)
-
- assert(fishing_spot.present_external_catch(crab_catch))
- assert(fishing_spot.state == FishingSpot.FishingState.SHOWING_CATCH)
- assert(not player.is_movement_enabled())
- # Network attempt cleanup may restore the base movement flag before the
- # presentation finishes. The showcase owns a separate local-input lock.
- player.set_movement_enabled(true)
- assert(not bool(player.call("_is_movement_input_enabled")))
- assert(not bool(player.call("_is_camera_input_enabled")))
- Input.action_release("fish_primary")
- await process_frame
- assert(not bool(fishing_spot.get("_put_away_press_armed")))
-
- var pocket_event := InputEventAction.new()
- pocket_event.action = &"fish_primary"
- pocket_event.pressed = true
- Input.parse_input_event(pocket_event)
- await process_frame
- assert(fishing_spot.get("_pending_catch") == crab_catch)
- Input.action_release("fish_primary")
- fishing_spot.set("_showcase_input_lock_remaining", 0.0)
- await process_frame
- assert(bool(fishing_spot.get("_put_away_press_armed")))
- Input.parse_input_event(pocket_event)
- await process_frame
- assert(fishing_spot.get("_pending_catch") == null)
-
- # A radial menu can open after the showcase panel disappears but before the
- # pocket animation restores gameplay. Its camera lock must not overwrite the
- # showcase's underlying camera state when the two finish in either order.
- Input.action_press("open_emotes")
- var emote_press := InputEventAction.new()
- emote_press.action = &"open_emotes"
- emote_press.pressed = true
- game_ui.call("_input", emote_press)
- var emote_menu := game_ui.get_node("%EmoteRadialMenu") as EmoteRadialMenu
- assert(emote_menu.is_open())
- assert(not bool(player.call("_is_camera_input_enabled")))
-
- var pocket_deadline: int = Time.get_ticks_msec() + 6000
- while (
- Time.get_ticks_msec() < pocket_deadline
- and fishing_spot.state != FishingSpot.FishingState.READY
- ):
- await process_frame
- assert(fishing_spot.state == FishingSpot.FishingState.READY)
- assert(player.is_movement_enabled())
- assert(bool(player.call("_is_movement_input_enabled")))
- assert(not bool(player.call("_is_camera_input_enabled")))
- Input.action_release("open_emotes")
- var emote_release := InputEventAction.new()
- emote_release.action = &"open_emotes"
- emote_release.pressed = false
- game_ui.call("_input", emote_release)
- assert(not emote_menu.is_open())
- assert(bool(player.call("_is_camera_input_enabled")))
-
- # A focused chat field or popup can consume the wheel's release event. The
- # polled action state must still close the radial and release its camera lock.
- Input.action_press("open_emotes")
- game_ui.call("_input", emote_press)
- assert(emote_menu.is_open())
- Input.action_release("open_emotes")
- await process_frame
- assert(not emote_menu.is_open())
- assert(bool(player.call("_is_camera_input_enabled")))
- assert(player.inventory.contains_catch_id(crab_catch.catch_id))
- assert(bool(player.get("_active_item_is_net")))
- print("Gathering showcase validation: PASS")
-
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
diff --git a/tests/gathering_showcase_validation.gd.uid b/tests/gathering_showcase_validation.gd.uid
deleted file mode 100644
index a0266d4..0000000
--- a/tests/gathering_showcase_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cedarlvsw4bpm
diff --git a/tests/generated_world_runtime_validation.gd b/tests/generated_world_runtime_validation.gd
deleted file mode 100644
index 6a3f6c8..0000000
--- a/tests/generated_world_runtime_validation.gd
+++ /dev/null
@@ -1,1733 +0,0 @@
-extends SceneTree
-
-const RegionScene: PackedScene = preload(
- "res://world/generation/generated_world_region.tscn"
-)
-const GeneratedPondPool: FishPool = preload(
- "res://fish/pools/generated_pond_pool.tres"
-)
-const GeneratedLakePool: FishPool = preload(
- "res://fish/pools/generated_lake_pool.tres"
-)
-const GeneratedRiverPool: FishPool = preload(
- "res://fish/pools/generated_river_pool.tres"
-)
-const PrecipitationOcclusionType = preload(
- "res://world/environment/precipitation_occlusion.gd"
-)
-const FIRST_SEED := 13001
-const SECOND_SEED := 13002
-const RIVER_FALLBACK_SEED := 13012
-const EXPECTED_PROP_IDS: Array[StringName] = [
- &"prop_bridge",
- &"prop_dock",
- &"prop_log",
- &"prop_log_post_1",
- &"prop_log_post_2",
- &"prop_mushroom",
- &"prop_palm",
- &"prop_pine",
- &"prop_pine_large",
- &"prop_timber_1",
- &"prop_timber_2",
- &"prop_tree_1",
- &"prop_tree_2",
- &"prop_tree_3",
- &"prop_tree_large",
-]
-const EXPECTED_BIOME_IDS: Array[StringName] = [
- &"biome_plains",
- &"biome_forest",
- &"biome_pine_forest",
- &"biome_coast",
-]
-const PROCEDURAL_PROP_IDS: Array[StringName] = [
- &"prop_log",
- &"prop_mushroom",
- &"prop_palm",
- &"prop_pine",
- &"prop_pine_large",
- &"prop_tree_1",
- &"prop_tree_2",
- &"prop_tree_3",
- &"prop_tree_large",
-]
-
-
-func _initialize() -> void:
- call_deferred(&"_run")
-
-
-func _run() -> void:
- var region := RegionScene.instantiate() as GeneratedWorldRegion
- assert(region != null)
- var configured_generator := region.get_node(
- "Terrain/TerrainChunkGenerator"
- ) as TerrainChunkGenerator
- assert(configured_generator != null)
- configured_generator.elevated_cliff_third_level_chance = 1.0
- configured_generator.elevated_cliff_third_tier_base_chance = 0.0
- configured_generator.elevated_cliff_third_tier_stack_chance = 1.0
- configured_generator.elevated_cliff_double_third_tier_chance = 0.0
- root.add_child(region)
- await process_frame
- assert(region.generate_world(FIRST_SEED))
- await physics_frame
-
- var generator := region.get_node(
- "Terrain/TerrainChunkGenerator"
- ) as TerrainChunkGenerator
- assert(generator != null)
- assert(region.get_generation_seed() == FIRST_SEED)
- _validate_generated_region(region, generator)
-
- var first_layout := generator.placement_keys()
- var first_stacked_layout := generator.stacked_elevated_placement_keys()
- var first_biomes := _biome_signature(region, generator)
- var first_decorations := _decoration_signature(region)
- assert(region.generate_world(FIRST_SEED))
- assert(generator.placement_keys() == first_layout)
- assert(_biome_signature(region, generator) == first_biomes)
- assert(_decoration_signature(region) == first_decorations)
- generator._elevated_feature_center = Vector2i(-1, -1)
- assert(generator.generate_from_placement_keys(first_layout))
- assert(generator.stacked_elevated_placement_keys() == first_stacked_layout)
-
- configured_generator.elevated_cliff_third_tier_base_chance = 1.0
- configured_generator.elevated_cliff_third_tier_stack_chance = 0.0
- assert(region.generate_world(SECOND_SEED))
- await process_frame
- await physics_frame
- await physics_frame
- _validate_generated_region(region, generator)
- assert(generator.placement_keys() != first_layout)
- assert(_biome_signature(region, generator) != first_biomes)
- var second_layout := generator.placement_keys()
-
- configured_generator.elevated_cliff_double_third_tier_chance = 1.0
- assert(region.generate_world(RIVER_FALLBACK_SEED))
- await process_frame
- await physics_frame
- await physics_frame
- _validate_generated_region(region, generator)
- assert(generator.placement_keys() != first_layout)
- assert(generator.placement_keys() != second_layout)
- assert(generator._river_feature_candidate_index >= 0)
-
- region.queue_free()
- await process_frame
- print("Generated world runtime validation: PASS")
- quit()
-
-
-func _validate_generated_region(
- region: GeneratedWorldRegion,
- generator: TerrainChunkGenerator,
-) -> void:
- var placements := generator.placement_keys()
- var expected_chunk_count := generator.grid_size.x * generator.grid_size.y
- var center := Vector2i(
- generator.grid_size.x / 2,
- generator.grid_size.y / 2,
- )
- var center_index := center.y * generator.grid_size.x + center.x
- assert(generator.grid_size == Vector2i(20, 20))
- assert(generator._lake_feature_footprint_size.x >= 7)
- assert(
- generator._lake_feature_footprint_size.x
- == generator._lake_feature_footprint_size.y
- )
- assert(maxi(
- generator._river_feature_footprint_size.x,
- generator._river_feature_footprint_size.y,
- ) >= 4)
- assert(mini(
- generator._river_feature_footprint_size.x,
- generator._river_feature_footprint_size.y,
- ) >= 4)
- assert(generator._river_feature_flow_direction in [
- Vector2i.RIGHT,
- Vector2i.DOWN,
- Vector2i.LEFT,
- Vector2i.UP,
- ])
- assert(generator._river_feature_outlet_direction in [
- Vector2i.RIGHT,
- Vector2i.DOWN,
- Vector2i.LEFT,
- Vector2i.UP,
- ])
- assert(
- generator._river_feature_flow_direction.x
- * generator._river_feature_outlet_direction.x
- + generator._river_feature_flow_direction.y
- * generator._river_feature_outlet_direction.y
- == 0
- )
- assert(generator.elevated_cliff_coastal_feature_required)
- assert(placements.size() == expected_chunk_count)
- assert(placements[center_index].begins_with("chunk_spawn@"))
- assert(_placement_count(placements, "chunk_spawn") == 1)
- assert(_placement_count(placements, "chunk_0001") >= 1)
- var stream_count := (
- _placement_count(placements, "chunk_0003")
- + _placement_count(placements, "chunk_0016")
- )
- assert(stream_count >= 1 and stream_count <= 3)
- assert(_placement_count(placements, "chunk_0004") == 2)
- var beach_count := _placement_count(placements, "chunk_0002")
- var grass_coast_count := _placement_count(placements, "chunk_0005")
- var grass_corner_count := _placement_count(placements, "chunk_0007")
- var beach_corner_count := _placement_count(placements, "chunk_0008")
- var has_secondary_elevation := (
- generator._secondary_elevated_feature_center.x >= 0
- )
- var has_third_tier_base := (
- _placement_count(placements, "chunk_0028") > 0
- )
- var coastline_edge_count := (
- 2 * (generator.grid_size.x - 2)
- + 2 * (generator.grid_size.y - 2)
- )
- assert(
- beach_count
- == coastline_edge_count - (4 if has_secondary_elevation else 0) - 2
- and grass_coast_count == 0
- )
- assert(
- beach_corner_count == (3 if has_secondary_elevation else 4)
- and grass_corner_count == 0
- )
- assert(
- _placement_count(placements, "chunk_0009")
- == (
- (1 if has_secondary_elevation else 0)
- + (0 if has_third_tier_base else 9)
- )
- )
- assert(
- _placement_count(placements, "chunk_0010")
- == (
- (1 if has_secondary_elevation else 0)
- + (0 if has_third_tier_base else 4)
- )
- )
- assert(
- _placement_count(placements, "chunk_0011")
- + _placement_count(placements, "chunk_0012")
- == (
- (2 if has_secondary_elevation else 0)
- + (0 if has_third_tier_base else 12)
- )
- )
- assert(
- _placement_count(placements, "chunk_0012")
- == (
- (1 if has_secondary_elevation else 0)
- + (0 if has_third_tier_base else 1)
- )
- )
- assert(
- _placement_count(placements, "chunk_0028")
- == (9 if has_third_tier_base else 0)
- )
- assert(
- _placement_count(placements, "chunk_0027")
- == (4 if has_third_tier_base else 0)
- )
- assert(
- _placement_count(placements, "chunk_0026")
- == (10 if has_third_tier_base else 0)
- )
- assert(_placement_count(placements, "chunk_0029") == 1)
- assert(_placement_count(placements, "chunk_0030") == 1)
- assert(_placement_count(placements, "chunk_0031") == 1)
- assert(_placement_count(placements, "chunk_0032") == 1)
- assert(_placement_count(placements, "chunk_0033") == 1)
- assert(_placement_count(placements, "chunk_0034") == 1)
- assert(_placement_count(placements, "chunk_0035") == 1)
- assert(_placement_count(placements, "chunk_0036") == 1)
- assert(_placement_count(placements, "chunk_0038") == 2)
- assert(
- _placement_count(placements, "chunk_0014")
- == (2 if has_secondary_elevation else 0)
- )
- assert(
- _placement_count(placements, "chunk_0015")
- == (1 if has_secondary_elevation else 0)
- )
- assert(_placement_count(placements, "chunk_0018") == 0)
- assert(_placement_count(placements, "chunk_0020") == 4)
- assert(_placement_count(placements, "chunk_0022") == 4)
- assert(_placement_count(placements, "chunk_0023") == 4)
- var lake_edge_count := _placement_count(placements, "chunk_0017")
- var lake_edge_variant_count := _placement_count(placements, "chunk_0037")
- var lake_fill_count := _placement_count(placements, "chunk_0019")
- var lake_narrow_count := _placement_count(placements, "chunk_0021")
- var lake_size := generator._lake_feature_footprint_size.x
- assert(lake_size in [7, 8])
- assert(lake_fill_count == (lake_size - 2) * (lake_size - 2))
- assert(lake_edge_count + lake_edge_variant_count == (lake_size - 6) * 4)
- assert(lake_edge_count > 0)
- assert(lake_edge_variant_count > 0)
- assert(lake_narrow_count == 8)
- assert(_placement_count(placements, "chunk_9999") == 0)
- assert(_placement_count(placements, "chunk_9998") == 0)
- assert(
- _placement_count(placements, "chunk_9997")
- == (1 if has_secondary_elevation else 0)
- )
- assert(
- _placement_count(placements, "chunk_9996")
- == (1 if has_secondary_elevation else 0)
- )
- assert(_placement_count(placements, "chunk_9995") == 0)
- var river_coordinate_count := generator._river_feature_coordinates.size()
- var river_edge_count := (
- _placement_count(placements, "chunk_0024")
- + _placement_count(placements, "chunk_0025")
- + _placement_count(placements, "chunk_0038")
- )
- assert(river_coordinate_count >= 12)
- assert(generator._river_feature_placements.size() == river_coordinate_count)
- assert(river_edge_count == river_coordinate_count - 8)
- var stone_bridge_specs: Array[Dictionary] = []
- for spec: Dictionary in generator._river_feature_placements:
- if spec.get("id", &"") == &"chunk_0038":
- stone_bridge_specs.append(spec)
- assert(stone_bridge_specs.size() == 2)
- var first_bridge_coordinate := stone_bridge_specs[0].get(
- "coordinate",
- Vector2i(-1, -1),
- ) as Vector2i
- var second_bridge_coordinate := stone_bridge_specs[1].get(
- "coordinate",
- Vector2i(-1, -1),
- ) as Vector2i
- var bridge_delta := second_bridge_coordinate - first_bridge_coordinate
- assert(absi(bridge_delta.x) + absi(bridge_delta.y) == 1)
- assert(posmod(
- int(stone_bridge_specs[1].get("turns", -1))
- - int(stone_bridge_specs[0].get("turns", -1)),
- 4,
- ) == 2)
- var river_source_coordinates := generator._river_source_coordinates(
- generator._river_feature_origin,
- generator._river_feature_footprint_size,
- generator._river_feature_flow_direction,
- )
- assert(river_source_coordinates.size() == 2)
- for source_coordinate: Vector2i in river_source_coordinates:
- var source_index := (
- source_coordinate.y * generator.grid_size.x + source_coordinate.x
- )
- assert(
- placements[source_index].begins_with("chunk_0029@")
- or placements[source_index].begins_with("chunk_0030@")
- )
- var source_offset := source_coordinate - generator._elevated_feature_center
- assert(
- maxi(absi(source_offset.x), absi(source_offset.y))
- == TerrainChunkGenerator.ELEVATED_FEATURE_RADIUS
- )
- var outlet_coordinates: Array[Vector2i] = []
- for index: int in placements.size():
- if (
- placements[index].begins_with("chunk_0031@")
- or placements[index].begins_with("chunk_0032@")
- ):
- var outlet_coordinate := Vector2i(
- index % generator.grid_size.x,
- index / generator.grid_size.x,
- )
- outlet_coordinates.append(outlet_coordinate)
- assert(
- not generator._coordinate_is_inside_grid(
- outlet_coordinate
- + generator._river_feature_outlet_direction
- )
- )
- assert(outlet_coordinates.size() == 2)
- var outlet_delta := outlet_coordinates[1] - outlet_coordinates[0]
- assert(absi(outlet_delta.x) + absi(outlet_delta.y) == 1)
- for beach_coordinate: Vector2i in [
- outlet_coordinates[0] - outlet_delta,
- outlet_coordinates[1] + outlet_delta,
- ]:
- assert(generator._coordinate_is_inside_grid(beach_coordinate))
- assert(generator._coordinate_is_on_boundary(beach_coordinate))
- var beach_index := (
- beach_coordinate.y * generator.grid_size.x + beach_coordinate.x
- )
- assert(placements[beach_index].begins_with("chunk_0002@"))
- for index: int in placements.size():
- var coordinate := Vector2i(
- index % generator.grid_size.x,
- index / generator.grid_size.x,
- )
- if placements[index].begins_with("chunk_0001@"):
- assert(generator._distance_from_map_boundary(coordinate) <= 1)
- elif placements[index].begins_with("chunk_0013@"):
- assert(generator._distance_from_map_boundary(coordinate) <= 2)
- assert(_placement_count(placements, "chunk_0013") >= 1)
- assert(
- generator.get_generated_chunks_root().get_child_count()
- == expected_chunk_count
- )
- var generated_root := generator.get_generated_chunks_root()
- var terrain_collision_batches := generated_root.find_children(
- "TerrainCollisionBatch_*",
- "StaticBody3D",
- true,
- false,
- )
- assert(not terrain_collision_batches.is_empty())
- assert(
- terrain_collision_batches.size()
- <= ceili(float(generator.grid_size.x) / generator.collision_batch_size)
- * ceili(float(generator.grid_size.y) / generator.collision_batch_size)
- )
- assert(
- generated_root.find_children(
- "TerrainCollision",
- "StaticBody3D",
- true,
- false,
- ).is_empty()
- )
- assert(
- generator.get_primary_terrain_meshes().size()
- == (
- expected_chunk_count
- + generator.stacked_elevated_placement_keys().size()
- )
- )
- _validate_elevated_cliff_feature(generator)
-
- var shop := region.get_node("Interactables/FishingShopWorld") as Node3D
- var storage := region.get_node("Interactables/PlayerStorageBox") as Node3D
- assert(shop != null and shop.has_node("Shopkeeper"))
- assert(storage != null and storage.has_node("InteractionArea"))
- var spawn_chunk_center := generator.chunk_position(center)
- var shop_offset := shop.position - spawn_chunk_center
- var storage_offset := storage.position - spawn_chunk_center
- assert(absf(shop_offset.x) < 5.0 and absf(shop_offset.z) < 5.0)
- assert(absf(storage_offset.x) < 5.0 and absf(storage_offset.z) < 5.0)
- assert(region.get_fishing_shop() != null)
- assert(region.get_player_storage() != null)
- assert(region.get_player_spawn_transform().origin.y > 0.0)
- _validate_prop_catalog(region)
- _validate_biome_catalog(region, generator)
-
- var ocean := region.get_node("WaterBodies/OceanWater") as WaterBodyAuthoring
- var fresh_root := region.get_node("WaterBodies/FreshWaterBodies") as Node3D
- assert(ocean != null and fresh_root != null)
- assert(ocean.water_type == WaterType.Type.SALT_WATER)
- assert(is_equal_approx(ocean.position.y, GeneratedWorldRegion.WATER_HEIGHT))
- var ocean_recovery := ocean.get_node("RecoveryRegion") as PlayerWaterTrigger
- assert(
- ocean_recovery.entry_height_reference
- == PlayerWaterTrigger.EntryHeightReference.BODY_CENTER
- )
- assert(is_equal_approx(ocean_recovery.entry_depth_threshold, 0.35))
- var fresh_placement_count := 0
- var river_placement_count := 0
- var polygon_sizes_by_coordinate: Dictionary[Vector2i, int] = {}
- for record: Dictionary in generator.placement_records():
- var tags: PackedStringArray = record.get("tags", PackedStringArray())
- if "coast" in tags:
- _validate_ocean_facing_record(record, generator.grid_size)
- if "fresh_water" in tags:
- fresh_placement_count += 1
- if "river" in tags:
- river_placement_count += 1
- assert(not "pond" in tags)
- assert(not "lake" in tags)
- var surface_polygon: PackedVector2Array = record.get(
- "water_surface_polygon",
- PackedVector2Array(),
- )
- if surface_polygon.size() >= 3:
- polygon_sizes_by_coordinate[
- record.get("coordinate", Vector2i.ZERO)
- ] = surface_polygon.size()
- _validate_complete_coastline(generator)
- assert(fresh_root.get_child_count() == fresh_placement_count)
- assert(river_placement_count == river_coordinate_count)
- assert(
- polygon_sizes_by_coordinate.size()
- == 20 + river_placement_count
- )
- var river_body_count := 0
- for child: Node in fresh_root.get_children():
- var fresh := child as WaterBodyAuthoring
- assert(fresh != null)
- assert(fresh.water_type == WaterType.Type.FRESH_WATER)
- assert(is_equal_approx(fresh.position.y, GeneratedWorldRegion.WATER_HEIGHT))
- assert(fresh.visible)
- assert(fresh.surface_size.x <= 10.0 and fresh.surface_size.y <= 10.0)
- assert(not fresh.visual_surface_enabled)
- assert(not (fresh.get_node("VisualWater") as MeshInstance3D).visible)
- var generated_bed := fresh.get_node_or_null(
- "GeneratedBed",
- ) as MeshInstance3D
- assert(generated_bed != null and generated_bed.mesh != null)
- assert(
- is_equal_approx(
- generated_bed.global_position.y,
- GeneratedWorldRegion.GENERATED_FRESH_WATER_BED_HEIGHT,
- )
- )
- assert(
- generated_bed.material_override != null
- and generated_bed.material_override.resource_name == "dirt"
- )
- var fresh_recovery := (
- fresh.get_node("RecoveryRegion") as PlayerWaterTrigger
- )
- assert(
- fresh_recovery.entry_height_reference
- == PlayerWaterTrigger.EntryHeightReference.PLAYER_ORIGIN
- )
- assert(is_equal_approx(fresh_recovery.entry_depth_threshold, 0.1))
- assert(is_equal_approx(
- fresh_recovery.entry_confirmation_seconds,
- 0.2,
- ))
- var coordinate_tokens := child.name.trim_prefix("FreshWater_").split("_")
- assert(coordinate_tokens.size() == 2)
- var coordinate := Vector2i(
- int(coordinate_tokens[0]),
- int(coordinate_tokens[1]),
- )
- if polygon_sizes_by_coordinate.has(coordinate):
- assert(
- fresh.surface_polygon.size()
- == polygon_sizes_by_coordinate[coordinate]
- )
- assert(
- fresh.get_node("FishingRegion").get_child_count() > 1
- )
- else:
- assert(fresh.surface_polygon.is_empty())
- if &"river" in fresh.location_tags:
- river_body_count += 1
- assert(fresh.fish_pool == GeneratedRiverPool)
- elif &"lake" in fresh.location_tags:
- assert(fresh.fish_pool == GeneratedLakePool)
- else:
- assert(&"pond" in fresh.location_tags)
- assert(fresh.fish_pool == GeneratedPondPool)
- assert(river_body_count == river_placement_count)
-
- _validate_authored_chunk_surfaces(region, generator)
- _validate_projected_terrain_materials(
- generator.get_generated_chunks_root()
- )
-
- var decorations := region.get_node("Decorations") as Node3D
- var grass_prop_surface_triangles := region.get_spawn_surface_triangles(
- [&"grass_lite"],
- -INF,
- 0.35,
- )
- var sand_prop_surface_triangles := region.get_spawn_surface_triangles(
- [&"sand"],
- -INF,
- 0.35,
- )
- _validate_water_recovery_positions(
- region,
- grass_prop_surface_triangles,
- sand_prop_surface_triangles,
- )
- var anchors := region.get_node(
- "GatherableAnchors/ReachableTreeTrunks"
- ) as GatherableAnchorSet3D
- assert(decorations != null and decorations.get_child_count() > 0)
- assert(anchors != null)
- assert(anchors.get_spawn_positions().size() > 0)
- _validate_tree_gatherable_anchors(region, decorations, anchors)
- var palm_clusters: Dictionary[int, Array] = {}
- var tree_scales: Array[float] = []
- var tree_yaws: Dictionary[float, bool] = {}
- var ocean_edges_by_coordinate: Dictionary[Vector2i, int] = {}
- for record: Dictionary in generator.placement_records():
- var record_edges := int(record.get("ocean_facing_edges", 0))
- if record_edges != 0:
- ocean_edges_by_coordinate[
- record.get("coordinate", Vector2i.ZERO)
- ] = record_edges
- for child: Node in decorations.get_children():
- var prop_id := StringName(child.get_meta(&"terrain_prop_id", &""))
- assert(PROCEDURAL_PROP_IDS.has(prop_id))
- var definition := region.get_prop_catalog().definition_for_id(prop_id)
- assert(definition != null)
- if definition.procedural_group in [&"grass_tree", &"sand_tree"]:
- assert(
- _has_precipitation_occlusion_layer(child),
- "Precipitation occlusion layer missing from %s (%s)."
- % [child.name, prop_id],
- )
- var biome_id := StringName(
- child.get_meta(&"terrain_biome_id", &"")
- )
- var biome := region.get_biome_catalog().definition_for_id(biome_id)
- assert(biome != null)
- var biome_rule := biome.prop_rule_for_group(
- definition.procedural_group
- )
- assert(biome_rule != null and biome_rule.allows_prop(definition))
- var coordinate: Vector2i = child.get_meta(
- &"terrain_chunk_coordinate",
- Vector2i.ZERO,
- )
- var spawn_coordinate := Vector2i(
- generator.grid_size.x / 2,
- generator.grid_size.y / 2,
- )
- var spawn_distance := (
- absi(coordinate.x - spawn_coordinate.x)
- + absi(coordinate.y - spawn_coordinate.y)
- )
- assert(spawn_distance >= definition.minimum_spawn_chunk_distance)
- _validate_decoration_transform(
- region,
- child as Node3D,
- definition,
- grass_prop_surface_triangles,
- sand_prop_surface_triangles,
- )
- if prop_id == &"prop_palm":
- var cluster_id := int(
- child.get_meta(&"terrain_prop_cluster_id", -1)
- )
- var members: Array = palm_clusters.get(cluster_id, [])
- members.append(child)
- palm_clusters[cluster_id] = members
- var ocean_direction := _ocean_direction(
- ocean_edges_by_coordinate.get(coordinate, 0)
- )
- if ocean_direction.is_zero_approx():
- ocean_direction = _nearest_ocean_direction(
- region,
- generator,
- (child as Node3D).position,
- )
- var local_overhang := Vector3(
- definition.local_overhang_direction.x,
- 0.0,
- definition.local_overhang_direction.y,
- ).normalized()
- var actual_overhang := local_overhang.rotated(
- Vector3.UP,
- (child as Node3D).rotation.y,
- )
- assert(
- actual_overhang.dot(ocean_direction)
- >= cos(deg_to_rad(definition.ocean_facing_spread_degrees))
- - 0.001
- )
- elif prop_id in [
- &"prop_tree_1",
- &"prop_tree_2",
- &"prop_tree_3",
- &"prop_tree_large",
- &"prop_pine",
- &"prop_pine_large",
- ]:
- tree_scales.append(
- float(child.get_meta(&"terrain_prop_visual_scale", 1.0))
- )
- tree_yaws[snappedf((child as Node3D).rotation.y, 0.01)] = true
- if prop_id in [
- &"prop_tree_1",
- &"prop_tree_2",
- &"prop_tree_3",
- &"prop_tree_large",
- ]:
- assert(
- _has_material_variant_override(
- child.get_node_or_null("Visual"),
- definition.material_variants,
- )
- )
- assert(
- _has_material_variant_override(
- child.get_node_or_null("Visual"),
- definition.secondary_material_variants,
- )
- )
- assert(not palm_clusters.is_empty())
- for members: Array in palm_clusters.values():
- assert(members.size() >= 1 and members.size() <= 3)
- for first_index: int in members.size():
- for second_index: int in range(first_index + 1, members.size()):
- var first := members[first_index] as Node3D
- var second := members[second_index] as Node3D
- var separation := Vector2(
- first.position.x - second.position.x,
- first.position.z - second.position.z,
- ).length()
- assert(separation > 0.5 and separation < 6.1)
- assert(tree_scales.max() - tree_scales.min() > 0.25)
- assert(tree_yaws.size() >= 8)
- assert(_decoration_group_count(region, &"grass_tree") > 0)
- assert(_decoration_group_count(region, &"sand_tree") > 0)
- assert(
- _decoration_biome_group_count(
- region,
- &"biome_forest",
- &"grass_tree",
- ) >= 4
- )
- assert(
- _decoration_biome_group_count(
- region,
- &"biome_pine_forest",
- &"grass_tree",
- ) >= 4
- )
-
-
-func _validate_elevated_cliff_feature(
- generator: TerrainChunkGenerator,
-) -> void:
- var elevated_coordinates: Dictionary[Vector2i, StringName] = {}
- var top_coordinates: Array[Vector2i] = []
- var ramp_records: Array[Dictionary] = []
- var coastal_ids: Array[StringName] = [
- &"chunk_0014",
- &"chunk_0015",
- &"chunk_9999",
- &"chunk_9998",
- &"chunk_9997",
- &"chunk_9996",
- ]
- var has_coastal_feature := false
- for record: Dictionary in generator.placement_records():
- var stable_id: StringName = record.get("stable_id", &"")
- if stable_id not in [
- &"chunk_0009",
- &"chunk_0010",
- &"chunk_0011",
- &"chunk_0012",
- &"chunk_0026",
- &"chunk_0027",
- &"chunk_0028",
- &"chunk_0029",
- &"chunk_0030",
- &"chunk_0014",
- &"chunk_0015",
- &"chunk_9999",
- &"chunk_9998",
- &"chunk_9997",
- &"chunk_9996",
- ]:
- continue
- var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
- if stable_id in coastal_ids:
- has_coastal_feature = true
- assert(
- coordinate.x in [0, generator.grid_size.x - 1]
- or coordinate.y in [0, generator.grid_size.y - 1]
- )
- else:
- assert(coordinate.x > 0 and coordinate.x < generator.grid_size.x - 1)
- assert(coordinate.y > 0 and coordinate.y < generator.grid_size.y - 1)
- elevated_coordinates[coordinate] = stable_id
- if stable_id in [&"chunk_0009", &"chunk_0028"]:
- top_coordinates.append(coordinate)
- elif stable_id == &"chunk_0012":
- ramp_records.append(record)
- assert(elevated_coordinates.size() == (34 if has_coastal_feature else 25))
- var feature_center := generator._elevated_feature_center
- assert(feature_center != Vector2i(-1, -1))
- var center_index := feature_center.y * generator.grid_size.x + feature_center.x
- assert(center_index >= 0 and center_index < generator.placement_keys().size())
- var primary_top_id := StringName(
- generator.placement_keys()[center_index].get_slice("@", 0)
- )
- assert(primary_top_id in [&"chunk_0009", &"chunk_0028"])
- var primary_is_third_tier := primary_top_id == &"chunk_0028"
- assert(
- top_coordinates.size()
- == (9 + (1 if has_coastal_feature else 0))
- )
- assert(
- ramp_records.size()
- == (
- (0 if primary_is_third_tier else 1)
- + (1 if has_coastal_feature else 0)
- )
- )
- var detected_feature_center := Vector2i(-1, -1)
- for candidate: Vector2i in top_coordinates:
- var neighboring_top_count := 0
- for row_offset: int in range(-1, 2):
- for column_offset: int in range(-1, 2):
- neighboring_top_count += int(
- elevated_coordinates.get(
- candidate + Vector2i(column_offset, row_offset),
- &"",
- ) == primary_top_id
- )
- if neighboring_top_count == 9:
- detected_feature_center = candidate
- break
- assert(detected_feature_center == feature_center)
- for row_offset: int in range(-2, 3):
- for column_offset: int in range(-2, 3):
- assert(
- elevated_coordinates.has(
- feature_center + Vector2i(column_offset, row_offset)
- )
- )
- if has_coastal_feature:
- var secondary_top := generator._secondary_elevated_feature_center
- assert(secondary_top != Vector2i(-1, -1))
- for row_offset: int in range(-1, 2):
- for column_offset: int in range(-1, 2):
- assert(
- elevated_coordinates.has(
- secondary_top + Vector2i(column_offset, row_offset)
- )
- )
- var found_primary_ramp := false
- for ramp_record: Dictionary in ramp_records:
- var ramp_coordinate: Vector2i = ramp_record.get(
- "coordinate", Vector2i(-1, -1)
- )
- var ramp_turns := int(ramp_record.get("rotation_quarters", 0))
- var high_edge := TerrainChunkTopology.rotated_edge(
- TerrainChunkTopology.Edge.WEST,
- ramp_turns,
- )
- var high_offset := TerrainChunkTopology.grid_offset(high_edge)
- assert(
- elevated_coordinates.get(ramp_coordinate + high_offset, &"")
- == &"chunk_0009"
- )
- found_primary_ramp = (
- found_primary_ramp
- or elevated_coordinates.get(
- ramp_coordinate + high_offset * 2,
- &"",
- ) == &"chunk_0009"
- )
- var low_coordinate := ramp_coordinate - high_offset
- var low_index := (
- low_coordinate.y * generator.grid_size.x + low_coordinate.x
- )
- assert(
- generator.placement_keys()[low_index].begins_with("chunk_0000@")
- or generator.placement_keys()[low_index].begins_with("chunk_spawn@")
- )
- assert(found_primary_ramp == not primary_is_third_tier)
-
- var generated := generator.get_generated_chunks_root()
- assert(generated != null)
- var layered_count := 0
- for chunk_root: Node in generated.get_children():
- var stable_id := StringName(
- chunk_root.get_meta(&"terrain_chunk_id", &"")
- )
- if stable_id not in [
- &"chunk_0009",
- &"chunk_0010",
- &"chunk_0011",
- &"chunk_0012",
- &"chunk_0026",
- &"chunk_0027",
- &"chunk_0028",
- &"chunk_0029",
- &"chunk_0030",
- &"chunk_0014",
- &"chunk_0015",
- &"chunk_9999",
- &"chunk_9998",
- &"chunk_9997",
- &"chunk_9996",
- ]:
- continue
- if stable_id in [&"chunk_0014", &"chunk_0015"]:
- assert(chunk_root.get_node_or_null("TerrainBaseLayer") == null)
- continue
- if stable_id in [&"chunk_0029", &"chunk_0030"]:
- assert(chunk_root.get_node_or_null("TerrainBaseLayer") == null)
- var source_mesh := TerrainChunkAnalyzer.find_primary_mesh(
- chunk_root,
- stable_id,
- )
- assert(source_mesh != null and source_mesh.mesh != null)
- continue
- layered_count += 1
- var base_layer := chunk_root.get_node_or_null("TerrainBaseLayer")
- var overlay := chunk_root.get_node_or_null("TerrainOverlay")
- assert(base_layer != null and overlay != null)
- var transition_base_mesh := &"chunk_0000"
- if stable_id in [&"chunk_9999", &"chunk_9998"]:
- transition_base_mesh = &"chunk_0007"
- elif stable_id in [&"chunk_9997", &"chunk_9996"]:
- transition_base_mesh = &"chunk_0008"
- var base_mesh := TerrainChunkAnalyzer.find_primary_mesh(
- base_layer,
- transition_base_mesh,
- )
- var overlay_mesh := TerrainChunkAnalyzer.find_primary_mesh(
- overlay,
- (
- &"chunk_0010"
- if stable_id in [
- &"chunk_9999",
- &"chunk_9998",
- &"chunk_9997",
- &"chunk_9996",
- ]
- else stable_id
- ),
- )
- assert(base_mesh != null and base_mesh.mesh != null)
- assert(overlay_mesh != null and overlay_mesh.mesh != null)
- assert(layered_count == (29 if has_coastal_feature else 23))
- var stacked_keys := generator.stacked_elevated_placement_keys()
- assert(stacked_keys.size() in [0, 4])
- if stacked_keys.is_empty():
- return
- for key: String in stacked_keys:
- assert(
- key.begins_with("chunk_0010@")
- or key.begins_with("chunk_0027@")
- )
- var stacked_count := 0
- for chunk_root: Node in generated.get_children():
- for child: Node in chunk_root.get_children():
- if not bool(child.get_meta(&"terrain_stacked_elevation", false)):
- continue
- stacked_count += 1
- var stacked_id := StringName(
- child.get_meta(&"terrain_chunk_id", &"")
- )
- assert(stacked_id in [&"chunk_0010", &"chunk_0027"])
- var expected_height := (
- generator.elevated_cliff_level_height * 2.0
- if primary_is_third_tier
- else generator.elevated_cliff_level_height
- )
- assert(
- is_equal_approx(
- (child as Node3D).position.y,
- expected_height,
- )
- )
- var stacked_mesh := TerrainChunkAnalyzer.find_primary_mesh(
- child,
- stacked_id,
- )
- assert(stacked_mesh != null and stacked_mesh.mesh != null)
- assert(stacked_count == 4)
-
-
-func _validate_ocean_facing_record(
- record: Dictionary,
- grid_size: Vector2i,
-) -> void:
- var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
- var ocean_edges := int(record.get("ocean_facing_edges", 0))
- assert(ocean_edges != 0)
- for edge_value: int in TerrainChunkTopology.Edge.values():
- if (ocean_edges & (1 << edge_value)) == 0:
- continue
- var ocean_coordinate := (
- coordinate
- + TerrainChunkTopology.grid_offset(
- edge_value as TerrainChunkTopology.Edge
- )
- )
- assert(
- ocean_coordinate.x < 0
- or ocean_coordinate.y < 0
- or ocean_coordinate.x >= grid_size.x
- or ocean_coordinate.y >= grid_size.y
- )
-
-
-func _validate_complete_coastline(generator: TerrainChunkGenerator) -> void:
- for record: Dictionary in generator.placement_records():
- var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
- var ocean_edges := int(record.get("ocean_facing_edges", 0))
- for edge_value: int in TerrainChunkTopology.Edge.values():
- var neighbor := (
- coordinate
- + TerrainChunkTopology.grid_offset(
- edge_value as TerrainChunkTopology.Edge
- )
- )
- var outside := (
- neighbor.x < 0
- or neighbor.y < 0
- or neighbor.x >= generator.grid_size.x
- or neighbor.y >= generator.grid_size.y
- )
- assert(
- ((ocean_edges & (1 << edge_value)) != 0) == outside
- )
-
-
-func _ocean_direction(ocean_edges: int) -> Vector3:
- var result := Vector3.ZERO
- for edge_value: int in TerrainChunkTopology.Edge.values():
- if (ocean_edges & (1 << edge_value)) != 0:
- result += TerrainChunkTopology.edge_normal(
- edge_value as TerrainChunkTopology.Edge
- )
- return result.normalized()
-
-
-func _nearest_ocean_direction(
- region: GeneratedWorldRegion,
- generator: TerrainChunkGenerator,
- position: Vector3,
-) -> Vector3:
- var half_extents := region.get_playable_half_extents()
- var distance_x := half_extents.x - absf(position.x)
- var distance_z := half_extents.y - absf(position.z)
- var direction_x := Vector3.RIGHT if position.x >= 0.0 else Vector3.LEFT
- var direction_z := Vector3.BACK if position.z >= 0.0 else Vector3.FORWARD
- if absf(distance_x - distance_z) <= generator.catalog.chunk_size * 0.35:
- return (direction_x + direction_z).normalized()
- return direction_x if distance_x < distance_z else direction_z
-
-
-func _validate_prop_catalog(region: GeneratedWorldRegion) -> void:
- var catalog := region.get_prop_catalog()
- assert(catalog != null)
- assert(catalog.validation_errors().is_empty())
- assert(catalog.definitions.size() == EXPECTED_PROP_IDS.size())
- for stable_id: StringName in EXPECTED_PROP_IDS:
- var definition := catalog.definition_for_id(stable_id)
- assert(definition != null and definition.packed_scene != null)
- var instance := definition.packed_scene.instantiate() as Node3D
- assert(instance != null)
- assert(instance.position.is_zero_approx())
- assert(_find_mesh_instance(instance) != null)
- instance.free()
- if definition.has_gatherable_surface():
- assert(
- definition.gatherable_surface_clearance >= 0.12
- )
- var mushroom := catalog.definition_for_id(&"prop_mushroom")
- assert(mushroom != null and mushroom.has_collision())
- assert(mushroom.visual_offset.y < 0.0)
- for tree_id: StringName in [
- &"prop_tree_1",
- &"prop_tree_2",
- &"prop_tree_3",
- &"prop_tree_large",
- ]:
- var tree := catalog.definition_for_id(tree_id)
- assert(tree != null)
- assert(tree.minimum_visual_scale < tree.maximum_visual_scale)
- assert(tree.material_variants.size() >= 3)
- assert(not tree.variant_material_slot_names.is_empty())
- assert(tree.secondary_material_variants.size() >= 3)
- assert(not tree.secondary_variant_material_slot_names.is_empty())
- var pine := catalog.definition_for_id(&"prop_pine")
- assert(pine != null)
- assert(pine.minimum_visual_scale < pine.maximum_visual_scale)
- assert(pine.material_variants.is_empty())
- assert(not pine.has_gatherable_surface())
- var large_pine := catalog.definition_for_id(&"prop_pine_large")
- assert(large_pine != null)
- assert(large_pine.minimum_visual_scale < large_pine.maximum_visual_scale)
- assert(large_pine.material_variants.is_empty())
- assert(not large_pine.has_gatherable_surface())
- var palm := catalog.definition_for_id(&"prop_palm")
- assert(palm != null)
- assert(is_equal_approx(palm.minimum_visual_scale, 0.5))
- assert(is_equal_approx(palm.maximum_visual_scale, 1.0))
- assert(palm.minimum_cluster_size == 2)
- assert(palm.maximum_cluster_size == 3)
- assert(palm.minimum_cluster_radius > palm.clearance_radius)
- assert(palm.maximum_cluster_radius > palm.minimum_cluster_radius)
- assert(palm.prefer_ocean_facing)
- assert(not palm.local_overhang_direction.is_zero_approx())
- var aligned_tree_collisions: Dictionary[StringName, Vector2] = {
- &"prop_tree_1": Vector2(0.08, 0.01),
- &"prop_tree_2": Vector2(0.1, 0.075),
- &"prop_tree_3": Vector2(-0.1, 0.18),
- &"prop_tree_large": Vector2(0.084, 0.504),
- &"prop_pine": Vector2(-0.044, 0.083),
- &"prop_pine_large": Vector2(-0.04, 0.075),
- &"prop_palm": Vector2(0.15, -0.1),
- }
- for tree_id: StringName in aligned_tree_collisions:
- var aligned_tree := catalog.definition_for_id(tree_id)
- assert(aligned_tree != null and aligned_tree.has_cylinder_collision())
- var collision_center := Vector2(
- aligned_tree.collision_offset.x,
- aligned_tree.collision_offset.z,
- )
- assert(collision_center.is_equal_approx(
- aligned_tree_collisions[tree_id]
- ))
- var forest := region.get_biome_catalog().definition_for_id(&"biome_forest")
- var forest_rule := forest.prop_rule_for_group(&"grass_tree")
- var large_tree := catalog.definition_for_id(&"prop_tree_large")
- var small_tree_weight := (
- catalog.definition_for_id(&"prop_tree_1").selection_weight
- + catalog.definition_for_id(&"prop_tree_2").selection_weight
- + catalog.definition_for_id(&"prop_tree_3").selection_weight
- )
- assert(forest_rule.allows_prop(large_tree))
- assert(large_tree.selection_weight <= small_tree_weight)
- var pine_forest := region.get_biome_catalog().definition_for_id(
- &"biome_pine_forest"
- )
- var pine_rule := pine_forest.prop_rule_for_group(&"grass_tree")
- assert(pine_rule.allows_prop(large_pine))
- assert(
- is_equal_approx(large_pine.selection_weight, pine.selection_weight)
- )
- assert(forest_rule.minimum_placements == 4)
- assert(forest_rule.placement_attempts_per_chunk == 4)
- assert(pine_rule.minimum_placements == 4)
- assert(pine_rule.placement_attempts_per_chunk == 4)
-
-
-func _validate_biome_catalog(
- region: GeneratedWorldRegion,
- generator: TerrainChunkGenerator,
-) -> void:
- var catalog := region.get_biome_catalog()
- assert(catalog != null)
- assert(catalog.validation_errors().is_empty())
- assert(catalog.definitions.size() == EXPECTED_BIOME_IDS.size())
- var counts: Dictionary[StringName, int] = {}
- for record: Dictionary in generator.placement_records():
- var coordinate: Vector2i = record.get("coordinate", Vector2i.ZERO)
- var biome_id := region.get_biome_at(coordinate)
- var tags: PackedStringArray = record.get("tags", PackedStringArray())
- if "mixed_surface" in tags:
- assert(biome_id == &"")
- elif "grass" in tags or "sand" in tags:
- assert(biome_id != &"")
- var biome := catalog.definition_for_id(biome_id)
- assert(biome != null and biome.supports_chunk_tags(tags))
- counts[biome_id] = counts.get(biome_id, 0) + 1
- else:
- assert(biome_id == &"")
- for biome_id: StringName in EXPECTED_BIOME_IDS:
- assert(counts.get(biome_id, 0) > 0)
- var center := Vector2i(
- generator.grid_size.x / 2,
- generator.grid_size.y / 2,
- )
- assert(region.get_biome_at(center) == &"biome_plains")
- for child: Node in generator.get_generated_chunks_root().get_children():
- var coordinate: Vector2i = child.get_meta(
- &"terrain_chunk_coordinate",
- Vector2i.ZERO,
- )
- assert(
- StringName(child.get_meta(&"terrain_biome_id", &""))
- == region.get_biome_at(coordinate)
- )
-
-
-func _validate_decoration_transform(
- region: GeneratedWorldRegion,
- prop: Node3D,
- definition: TerrainPropDefinition,
- grass_surface_triangles: Array[PackedVector3Array],
- sand_surface_triangles: Array[PackedVector3Array],
-) -> void:
- assert(prop != null)
- assert(prop.scale.is_equal_approx(Vector3.ONE))
- var visual_root := prop.get_node_or_null("Visual") as Node3D
- var visual := _find_mesh_instance(visual_root)
- var collision := prop.get_node_or_null(
- "TrunkCollision/CollisionShape"
- ) as CollisionShape3D
- assert(visual_root != null)
- assert(visual_root.position.is_equal_approx(definition.visual_offset))
- var visual_scale := float(
- prop.get_meta(&"terrain_prop_visual_scale", 1.0)
- )
- assert(visual_scale >= definition.minimum_visual_scale - 0.001)
- assert(visual_scale <= definition.maximum_visual_scale + 0.001)
- assert(
- visual_root.scale.is_equal_approx(Vector3.ONE * visual_scale)
- )
- assert(visual != null and visual.mesh != null)
- if definition.has_collision():
- assert(collision != null)
- assert(
- collision.shape is CylinderShape3D
- or collision.shape is BoxShape3D
- )
- assert(collision.global_basis.get_scale().is_equal_approx(Vector3.ONE))
- if collision.shape is CylinderShape3D:
- var cylinder := collision.shape as CylinderShape3D
- assert(is_equal_approx(
- cylinder.radius,
- definition.collision_radius * visual_scale,
- ))
- assert(is_equal_approx(
- cylinder.height,
- definition.collision_height * visual_scale,
- ))
- assert(collision.position.is_equal_approx(
- definition.collision_offset * visual_scale
- + Vector3.UP * cylinder.height * 0.5
- ))
- else:
- assert(collision == null)
- var query := PhysicsRayQueryParameters3D.create(
- # Stay below neighboring cliff overhangs while still beginning above the
- # authored walkable surface. Regional collision batches intentionally
- # keep every original shape on one body, so excluding an overhang body
- # would also exclude the ground beneath the prop.
- prop.global_position + Vector3.UP * 0.1,
- prop.global_position + Vector3.DOWN * 8.0,
- 1,
- )
- var excluded_colliders: Array[RID] = []
- var decorations := region.get_node("Decorations") as Node3D
- for decoration: Node in decorations.get_children():
- var prop_body := decoration.get_node_or_null(
- "TrunkCollision",
- ) as CollisionObject3D
- if prop_body != null:
- excluded_colliders.append(prop_body.get_rid())
- query.exclude = excluded_colliders
- var hit := region.get_world_3d().direct_space_state.intersect_ray(query)
- # Stacked cliff walls can overhang a neighboring cell. Match the placement
- # sampler by ignoring steep faces until the ray reaches walkable terrain.
- while (
- not hit.is_empty()
- and absf((hit.get("normal", Vector3.ZERO) as Vector3).dot(Vector3.UP))
- < 0.35
- ):
- var steep_collider := hit.get("collider") as CollisionObject3D
- assert(steep_collider != null)
- excluded_colliders.append(steep_collider.get_rid())
- query.exclude = excluded_colliders
- hit = region.get_world_3d().direct_space_state.intersect_ray(query)
- assert(
- not hit.is_empty(),
- "No terrain collision below %s (%s) at %s."
- % [prop.name, definition.stable_id, prop.global_position],
- )
- var ground_position: Vector3 = hit["position"]
- assert(
- absf(ground_position.y - prop.global_position.y) <= 0.01,
- "Terrain height %s does not match %s (%s) at %s."
- % [
- ground_position.y,
- prop.name,
- definition.stable_id,
- "%s in chunk %s; hit %s" % [
- prop.global_position,
- prop.get_meta(&"terrain_chunk_coordinate", Vector2i(-1, -1)),
- (hit.get("collider") as Node).get_path(),
- ],
- ],
- )
- var required_surface_triangles: Array[PackedVector3Array] = (
- sand_surface_triangles
- if definition.procedural_group == &"sand_tree"
- else grass_surface_triangles
- )
- var required_surface_height := _triangle_surface_height_at(
- prop.global_position,
- required_surface_triangles,
- )
- assert(
- required_surface_height > -INF
- and absf(required_surface_height - prop.global_position.y) <= 0.01,
- "%s (%s) is not rooted on its required terrain material at %s."
- % [prop.name, definition.stable_id, prop.global_position],
- )
-
-
-func _triangle_surface_height_at(
- position: Vector3,
- triangles: Array[PackedVector3Array],
-) -> float:
- var highest := -INF
- var segment_start := position + Vector3.UP * 100.0
- var segment_end := position + Vector3.DOWN * 100.0
- for triangle: PackedVector3Array in triangles:
- if triangle.size() != 3:
- continue
- var hit: Variant = Geometry3D.segment_intersects_triangle(
- segment_start,
- segment_end,
- triangle[0],
- triangle[1],
- triangle[2],
- )
- if hit is Vector3:
- highest = maxf(highest, (hit as Vector3).y)
- return highest
-
-
-func _validate_water_recovery_positions(
- region: GeneratedWorldRegion,
- grass_triangles: Array[PackedVector3Array],
- sand_triangles: Array[PackedVector3Array],
-) -> void:
- var fresh_water_root := region.get_node(
- "WaterBodies/FreshWaterBodies"
- ) as Node3D
- assert(fresh_water_root != null and fresh_water_root.get_child_count() > 0)
- var fresh_water := fresh_water_root.get_child(0) as WaterBodyAuthoring
- assert(fresh_water != null)
- var entry_position := fresh_water.global_position
- var fallback_position := region.get_player_spawn_transform().origin
- var recovery_position := region.get_water_recovery_position(
- entry_position,
- fallback_position,
- )
- assert(recovery_position.is_finite())
- assert(
- _horizontal_distance_squared(entry_position, recovery_position)
- < _horizontal_distance_squared(entry_position, fallback_position),
- "Fresh-water recovery returned the distant generated-world spawn.",
- )
- assert(
- recovery_position.y
- > GeneratedWorldRegion.WATER_HEIGHT
- + GeneratedWorldRegion.WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE
- )
- var recovery_triangles: Array[PackedVector3Array] = []
- recovery_triangles.append_array(grass_triangles)
- recovery_triangles.append_array(sand_triangles)
- var center_height := _triangle_surface_height_at(
- recovery_position,
- recovery_triangles,
- )
- assert(center_height > -INF)
- assert(absf(center_height - recovery_position.y) <= 0.01)
- for direction: Vector2 in GeneratedWorldRegion.PROP_SURFACE_SAMPLE_DIRECTIONS:
- var footprint_position := recovery_position + Vector3(
- direction.x * GeneratedWorldRegion.WATER_RECOVERY_FOOTPRINT_RADIUS,
- 0.0,
- direction.y * GeneratedWorldRegion.WATER_RECOVERY_FOOTPRINT_RADIUS,
- )
- var footprint_height := _triangle_surface_height_at(
- footprint_position,
- recovery_triangles,
- )
- assert(footprint_height > -INF)
- assert(
- absf(footprint_height - recovery_position.y)
- <= GeneratedWorldRegion.PROP_MAXIMUM_SUPPORT_HEIGHT_DIFFERENCE
- )
- assert(
- region.get_water_recovery_position(
- entry_position,
- fallback_position,
- ).is_equal_approx(recovery_position)
- )
- assert(
- region.get_water_recovery_position(
- Vector3(INF, 0.0, 0.0),
- fallback_position,
- ).is_equal_approx(fallback_position)
- )
-
-
-func _horizontal_distance_squared(a: Vector3, b: Vector3) -> float:
- return Vector2(a.x - b.x, a.z - b.z).length_squared()
-
-
-func _validate_tree_gatherable_anchors(
- region: GeneratedWorldRegion,
- decorations: Node3D,
- anchors: GatherableAnchorSet3D,
-) -> void:
- var eligible_props: Dictionary[StringName, Node3D] = {}
- var expected_anchor_count := 0
- for child: Node in decorations.get_children():
- var prop := child as Node3D
- if prop == null:
- continue
- var prop_id := StringName(prop.get_meta(&"terrain_prop_id", &""))
- var definition := region.get_prop_catalog().definition_for_id(prop_id)
- var socket_count := _count_authored_beetle_sockets(prop)
- if (
- definition != null
- and definition.has_gatherable_surface()
- and socket_count > 0
- ):
- eligible_props[prop.name] = prop
- expected_anchor_count += socket_count
- var positions := anchors.get_spawn_positions()
- assert(positions.size() == expected_anchor_count)
- assert(positions.size() >= 12)
- for child: Node in anchors.get_children():
- var anchor := child as Marker3D
- assert(anchor != null)
- assert(bool(anchor.get_meta(&"authored_beetle_socket", false)))
- var prop_name := StringName(anchor.get_meta(&"terrain_prop_name", &""))
- assert(eligible_props.has(prop_name))
- var prop: Node3D = eligible_props[prop_name]
- var prop_id := StringName(prop.get_meta(&"terrain_prop_id", &""))
- var definition := region.get_prop_catalog().definition_for_id(prop_id)
- assert(definition != null and definition.has_gatherable_surface())
- var local_anchor := prop.to_local(anchor.global_position)
- assert(prop_id != &"prop_palm")
- assert(local_anchor.y >= 0.25 and local_anchor.y <= 2.0)
-
-
-func _count_authored_beetle_sockets(root_node: Node) -> int:
- var result := 0
- if String(root_node.name).to_lower().contains("beetle_socket"):
- result += 1
- for child: Node in root_node.get_children():
- result += _count_authored_beetle_sockets(child)
- return result
-
-
-func _find_mesh_instance(root_node: Node) -> MeshInstance3D:
- if root_node is MeshInstance3D:
- return root_node as MeshInstance3D
- if root_node == null:
- return null
- for child: Node in root_node.get_children():
- var mesh_instance := _find_mesh_instance(child)
- if mesh_instance != null:
- return mesh_instance
- return null
-
-
-func _has_precipitation_occlusion_layer(root_node: Node) -> bool:
- var mesh_instance := root_node as MeshInstance3D
- if (
- mesh_instance != null
- and mesh_instance.get_layer_mask_value(
- PrecipitationOcclusionType.RENDER_LAYER_NUMBER
- )
- ):
- return true
- for child: Node in root_node.get_children():
- if _has_precipitation_occlusion_layer(child):
- return true
- return false
-
-
-func _has_material_variant_override(
- root_node: Node,
- variants: Array[Material],
-) -> bool:
- if root_node == null:
- return false
- var mesh_instance := root_node as MeshInstance3D
- if mesh_instance != null and mesh_instance.mesh != null:
- for surface_index: int in mesh_instance.mesh.get_surface_count():
- var override := mesh_instance.get_surface_override_material(
- surface_index
- )
- if override != null and override in variants:
- return true
- for child: Node in root_node.get_children():
- if _has_material_variant_override(child, variants):
- return true
- return false
-
-
-func _validate_authored_chunk_surfaces(
- region: GeneratedWorldRegion,
- generator: TerrainChunkGenerator,
-) -> void:
- var generated := generator.get_generated_chunks_root()
- var found_beach := false
- var found_stream := false
- var found_pond := false
- var found_grass_ocean_edge := false
- var found_grass_beach_transition := false
- var found_grass_ocean_corner := false
- var found_beach_ocean_corner := false
- var found_grass_sand_diagonal := false
- var beach_family := (
- _placement_count(generator.placement_keys(), "chunk_0002") > 0
- )
- for chunk_root: Node in generated.get_children():
- if chunk_root.name.begins_with("chunk_0002r"):
- var beach := chunk_root.find_child(
- "chunk_0002", true, false
- ) as MeshInstance3D
- assert(beach != null and beach.mesh != null)
- var material := beach.get_active_material(0)
- assert(material != null and material.resource_name == "sand")
- found_beach = true
- elif (
- chunk_root.name.begins_with("chunk_0003r")
- or chunk_root.name.begins_with("chunk_0016r")
- ):
- var stream_id := (
- &"chunk_0016"
- if chunk_root.name.begins_with("chunk_0016r")
- else &"chunk_0003"
- )
- var stream := chunk_root.find_child(
- String(stream_id), true, false
- ) as MeshInstance3D
- assert(stream != null and stream.mesh != null)
- var stream_materials := _material_names(stream)
- assert("dirt" in stream_materials)
- assert("grass_lite" in stream_materials)
- assert("dirt_wall" in stream_materials)
- if stream_id == &"chunk_0003":
- found_stream = true
- elif chunk_root.name.begins_with("chunk_0004r"):
- var pond := chunk_root.find_child(
- "chunk_0004", true, false
- ) as MeshInstance3D
- assert(pond != null and pond.mesh != null)
- for surface_index: int in pond.mesh.get_surface_count():
- var arrays := pond.mesh.surface_get_arrays(surface_index)
- var normals := arrays[Mesh.ARRAY_NORMAL] as PackedVector3Array
- for normal: Vector3 in normals:
- assert(normal.y >= -0.001)
- _validate_pond_collision(region, pond)
- found_pond = true
- elif chunk_root.name.begins_with("chunk_0005r"):
- var grass_ocean_edge := chunk_root.find_child(
- "chunk_0005", true, false
- ) as MeshInstance3D
- assert(grass_ocean_edge != null and grass_ocean_edge.mesh != null)
- var edge_materials := _material_names(grass_ocean_edge)
- assert("grass_lite" in edge_materials)
- assert("cliff_wall" in edge_materials)
- found_grass_ocean_edge = true
- elif chunk_root.name.begins_with("chunk_0006r"):
- var grass_beach_transition := chunk_root.find_child(
- "chunk_0006", true, false
- ) as MeshInstance3D
- assert(
- grass_beach_transition != null
- and grass_beach_transition.mesh != null
- )
- var transition_materials := _material_names(
- grass_beach_transition
- )
- assert("sand" in transition_materials)
- assert("grass_lite" in transition_materials)
- assert("cliff_wall" in transition_materials)
- found_grass_beach_transition = true
- elif chunk_root.name.begins_with("chunk_0007r"):
- var grass_ocean_corner := chunk_root.find_child(
- "chunk_0007", true, false
- ) as MeshInstance3D
- assert(grass_ocean_corner != null and grass_ocean_corner.mesh != null)
- var corner_materials := _material_names(grass_ocean_corner)
- assert("grass_lite" in corner_materials)
- assert("cliff_wall" in corner_materials)
- found_grass_ocean_corner = true
- elif chunk_root.name.begins_with("chunk_0008r"):
- var beach_ocean_corner := chunk_root.find_child(
- "chunk_0008", true, false
- ) as MeshInstance3D
- assert(beach_ocean_corner != null and beach_ocean_corner.mesh != null)
- var beach_corner_materials := _material_names(beach_ocean_corner)
- assert("sand" in beach_corner_materials)
- found_beach_ocean_corner = true
- elif chunk_root.name.begins_with("chunk_0013r"):
- var grass_sand_diagonal := chunk_root.find_child(
- "chunk_0013", true, false
- ) as MeshInstance3D
- assert(grass_sand_diagonal != null and grass_sand_diagonal.mesh != null)
- var diagonal_materials := _material_names(grass_sand_diagonal)
- assert("grass_lite" in diagonal_materials)
- assert("sand" in diagonal_materials)
- found_grass_sand_diagonal = true
- assert(found_stream)
- assert(found_pond)
- assert(found_grass_sand_diagonal)
- if beach_family:
- assert(found_beach)
- assert(found_beach_ocean_corner)
- assert(not found_grass_ocean_edge)
- assert(not found_grass_ocean_corner)
- else:
- assert(not found_beach)
- assert(not found_beach_ocean_corner)
- assert(found_grass_ocean_edge)
- assert(found_grass_ocean_corner)
- assert(not found_grass_beach_transition)
-
-
-func _material_names(mesh_instance: MeshInstance3D) -> PackedStringArray:
- var result := PackedStringArray()
- for surface_index: int in mesh_instance.mesh.get_surface_count():
- var material := mesh_instance.get_active_material(surface_index)
- if material != null:
- result.append(material.resource_name)
- return result
-
-
-func _validate_projected_terrain_materials(root: Node) -> void:
- var expected_sizes := {
- "grass_lite": 1.75,
- "sand": 2.6,
- "dirt": 2.5,
- }
- var surface_counts := {
- "grass_lite": 0,
- "sand": 0,
- "dirt": 0,
- }
- _validate_projected_material_node(root, expected_sizes, surface_counts)
- for material_name: String in expected_sizes:
- assert(surface_counts[material_name] > 0)
-
-
-func _validate_projected_material_node(
- root: Node,
- expected_sizes: Dictionary,
- surface_counts: Dictionary,
-) -> void:
- var mesh_instance := root as MeshInstance3D
- if mesh_instance != null and mesh_instance.mesh != null:
- for surface_index: int in mesh_instance.mesh.get_surface_count():
- var material := mesh_instance.get_active_material(surface_index)
- if material == null or not expected_sizes.has(material.resource_name):
- continue
- var shader_material := material as ShaderMaterial
- assert(shader_material != null and shader_material.shader != null)
- assert(
- shader_material.shader.resource_path
- == "res://world/materials/terrain_surface_projection.gdshader"
- )
- assert(
- is_equal_approx(
- float(shader_material.get_shader_parameter(
- &"tile_world_size"
- )),
- float(expected_sizes[material.resource_name]),
- )
- )
- surface_counts[material.resource_name] += 1
- for child: Node in root.get_children():
- _validate_projected_material_node(
- child,
- expected_sizes,
- surface_counts,
- )
-
-
-func _validate_pond_collision(
- region: GeneratedWorldRegion,
- pond: MeshInstance3D,
-) -> void:
- var best_centroid := Vector3.ZERO
- var best_height := -INF
- for surface_index: int in pond.mesh.get_surface_count():
- var arrays := pond.mesh.surface_get_arrays(surface_index)
- var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
- var normals := arrays[Mesh.ARRAY_NORMAL] as PackedVector3Array
- var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
- var triangle_count := (
- indices.size() / 3 if not indices.is_empty() else vertices.size() / 3
- )
- for triangle_index: int in triangle_count:
- var offset := triangle_index * 3
- var index_a: int = indices[offset] if not indices.is_empty() else offset
- var index_b: int = (
- indices[offset + 1] if not indices.is_empty() else offset + 1
- )
- var index_c: int = (
- indices[offset + 2] if not indices.is_empty() else offset + 2
- )
- var a: Vector3 = vertices[index_a]
- var b: Vector3 = vertices[index_b]
- var c: Vector3 = vertices[index_c]
- var normal := (
- (normals[index_a] + normals[index_b] + normals[index_c]) / 3.0
- ).normalized()
- var centroid := (a + b + c) / 3.0
- if normal.y > 0.5 and centroid.y > best_height:
- best_height = centroid.y
- best_centroid = centroid
- assert(best_height > -INF)
- var target := pond.global_transform * best_centroid
- var query := PhysicsRayQueryParameters3D.create(
- target + Vector3.UP * 2.0,
- target + Vector3.DOWN * 2.0,
- 1,
- )
- var hit := region.get_world_3d().direct_space_state.intersect_ray(query)
- assert(not hit.is_empty())
- var collider := hit.get("collider") as Node
- assert(
- collider != null
- and collider.has_meta(&"terrain_collision_batch")
- )
-
-
-func _decoration_group_count(
- region: GeneratedWorldRegion,
- group: StringName,
-) -> int:
- var count := 0
- var decorations := region.get_node("Decorations") as Node3D
- for child: Node in decorations.get_children():
- var stable_id := StringName(child.get_meta(&"terrain_prop_id", &""))
- var definition := region.get_prop_catalog().definition_for_id(stable_id)
- if definition != null and definition.procedural_group == group:
- count += 1
- return count
-
-
-func _decoration_biome_group_count(
- region: GeneratedWorldRegion,
- biome_id: StringName,
- group: StringName,
-) -> int:
- var count := 0
- var decorations := region.get_node("Decorations") as Node3D
- for child: Node in decorations.get_children():
- if StringName(child.get_meta(&"terrain_biome_id", &"")) != biome_id:
- continue
- var prop_id := StringName(child.get_meta(&"terrain_prop_id", &""))
- var definition := region.get_prop_catalog().definition_for_id(prop_id)
- if definition != null and definition.procedural_group == group:
- count += 1
- return count
-
-
-func _biome_signature(
- region: GeneratedWorldRegion,
- generator: TerrainChunkGenerator,
-) -> PackedStringArray:
- var result := PackedStringArray()
- for row: int in generator.grid_size.y:
- for column: int in generator.grid_size.x:
- result.append(
- String(region.get_biome_at(Vector2i(column, row)))
- )
- return result
-
-
-func _placement_count(keys: PackedStringArray, stable_id: String) -> int:
- var count := 0
- for key: String in keys:
- if key.begins_with(stable_id + "@"):
- count += 1
- return count
-
-
-func _decoration_signature(region: GeneratedWorldRegion) -> PackedStringArray:
- var result := PackedStringArray()
- var decorations := region.get_node("Decorations") as Node3D
- for child: Node in decorations.get_children():
- var prop := child as Node3D
- result.append("%s:%s:%s" % [
- prop.name,
- prop.position,
- prop.rotation,
- ])
- return result
diff --git a/tests/generated_world_runtime_validation.gd.uid b/tests/generated_world_runtime_validation.gd.uid
deleted file mode 100644
index 060cb22..0000000
--- a/tests/generated_world_runtime_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://opamwsr0t7px
diff --git a/tests/home_spawn_multiplayer_validation.gd b/tests/home_spawn_multiplayer_validation.gd
deleted file mode 100644
index 1f16147..0000000
--- a/tests/home_spawn_multiplayer_validation.gd
+++ /dev/null
@@ -1,178 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-const TEST_PORT: int = 18222
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var arguments: PackedStringArray = OS.get_cmdline_user_args()
- if arguments.has("host"):
- await _run_host()
- return
- if arguments.has("client"):
- await _run_client()
- return
- push_error("Home spawn validation needs host or client mode.")
- quit(1)
-
-
-func _run_host() -> void:
- var main: Node = await _create_initialized_main()
- var session := main.get_node("%NetworkSession") as NetworkSession
- var network_home := main.get_node(
- "%NetworkHomeService"
- ) as NetworkHomeService
- var home_world := main.get_node("%HomeWorldService") as HomeWorldService
- var spawn_service := main.get_node(
- "%PlayerSpawnService"
- ) as PlayerSpawnService
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(bool(main.call(
- "_apply_world", WorldLayout.STARTER_ISLAND, 1, true
- )))
- assert(bool(main.call(
- "_prepare_host_world", WorldLayout.STARTER_ISLAND, 1
- )))
- assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND))
- assert(session.start_private_host(TEST_PORT))
- main.call("_enter_gameplay")
- assert(session.set_host_open(true))
-
- var host_fingerprint: String = session.get_local_identity_fingerprint()
- assert(await _wait_for_home(network_home, host_fingerprint))
- var host_avatar: Player = spawn_service.get_avatar(1)
- var host_spawn: Transform3D = home_world.get_exterior_spawn_transform(
- host_fingerprint
- )
- assert(_same_door_position(host_avatar.global_position, host_spawn.origin))
- assert(network_home.request_enter(host_fingerprint))
- await physics_frame
- host_avatar.global_position = home_world.get_interior_exit_position(
- host_fingerprint
- )
- assert(network_home.request_exit())
- await physics_frame
- assert(_same_door_position(host_avatar.global_position, host_spawn.origin))
-
- var remote_peer_id: int = await _wait_for_remote_peer(session)
- assert(remote_peer_id > 1)
- var remote_record := session.get_peer_record(remote_peer_id)
- var remote_fingerprint: String = remote_record.identity_fingerprint
- assert(await _wait_for_home(network_home, remote_fingerprint))
- var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id)
- var remote_spawn: Transform3D = home_world.get_exterior_spawn_transform(
- remote_fingerprint
- )
- assert(_same_door_position(
- remote_avatar.global_position, remote_spawn.origin
- ))
-
- var disconnect_deadline: int = Time.get_ticks_msec() + 12000
- while (
- Time.get_ticks_msec() < disconnect_deadline
- and session.is_authenticated_peer(remote_peer_id)
- ):
- await process_frame
- assert(not session.is_authenticated_peer(remote_peer_id))
- print("Home spawn multiplayer host validation: PASS")
- await _cleanup(main, session)
-
-
-func _run_client() -> void:
- var main: Node = await _create_initialized_main()
- main.call(
- "_on_title_join_game_requested",
- "127.0.0.1:%d" % TEST_PORT,
- )
- var session := main.get_node("%NetworkSession") as NetworkSession
- var network_home := main.get_node(
- "%NetworkHomeService"
- ) as NetworkHomeService
- var home_world := main.get_node("%HomeWorldService") as HomeWorldService
- var deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
- main.call("_confirm_server_trust")
- if session.is_joined_client() and bool(main.get("_gameplay_started")):
- break
- assert(session.is_joined_client())
- var fingerprint: String = session.get_local_identity_fingerprint()
- assert(await _wait_for_home(network_home, fingerprint))
- var player := main.get("_player") as Player
- var exterior_spawn: Transform3D = (
- home_world.get_exterior_spawn_transform(fingerprint)
- )
- deadline = Time.get_ticks_msec() + 5000
- while (
- Time.get_ticks_msec() < deadline
- and not _same_door_position(
- player.global_position, exterior_spawn.origin
- )
- ):
- await physics_frame
- assert(_same_door_position(
- player.global_position, exterior_spawn.origin
- ))
- print("Home spawn multiplayer client validation: PASS")
- await _cleanup(main, session)
-
-
-func _create_initialized_main() -> Node:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- return main
-
-
-func _wait_for_home(
- network_home: NetworkHomeService,
- fingerprint: String,
-) -> bool:
- var deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- var manifests: Dictionary = network_home.get(
- "_manifests_by_fingerprint"
- )
- if manifests.has(fingerprint):
- return true
- return false
-
-
-func _wait_for_remote_peer(session: NetworkSession) -> int:
- var deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- for peer_id: int in session.get_authenticated_peer_ids():
- if peer_id != session.get_local_peer_id():
- return peer_id
- return 0
-
-
-func _same_door_position(first: Vector3, second: Vector3) -> bool:
- return (
- Vector2(first.x, first.z).distance_to(Vector2(second.x, second.z))
- < 0.08
- and absf(first.y - second.y) < 0.3
- )
-
-
-func _cleanup(main: Node, session: NetworkSession) -> void:
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
diff --git a/tests/home_spawn_multiplayer_validation.gd.uid b/tests/home_spawn_multiplayer_validation.gd.uid
deleted file mode 100644
index 06150f2..0000000
--- a/tests/home_spawn_multiplayer_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bndtkip5p6xma
diff --git a/tests/inventory_notepad_art_validation.gd b/tests/inventory_notepad_art_validation.gd
deleted file mode 100644
index d871b49..0000000
--- a/tests/inventory_notepad_art_validation.gd
+++ /dev/null
@@ -1,706 +0,0 @@
-extends SceneTree
-
-const PlayerMenuScene = preload("res://ui/player_menu.tscn")
-const HotbarScene = preload("res://ui/hotbar.tscn")
-const UIReferencePresentationType = preload(
- "res://ui/ui_reference_presentation.gd"
-)
-const FishSaleResultType = preload("res://economy/fish_sale_result.gd")
-const CurrencyPresentationType = preload(
- "res://ui/currency_presentation.gd"
-)
-const PelicanBuyer = preload("res://economy/buyers/pelicans.tres")
-const EXPECTED_HOST_SIZE := Vector2(278.0, 484.0)
-const EXPECTED_ART_SIZE := Vector2(306.28125, 580.8)
-const EXPECTED_ART_POSITION := Vector2(-4.140625, -28.4)
-const LEGACY_COOLER_PANEL_RECT := Rect2(54.0, 166.0, 882.0, 484.0)
-const LEGACY_COOLER_NOTEPAD_RECT := Rect2(952.0, 166.0, 278.0, 484.0)
-const INVENTORY_PANEL_RECT := Rect2(199.0, 166.0, 882.0, 484.0)
-const NOTEPAD_HOST_RECT := Rect2(501.0, 166.0, 278.0, 484.0)
-const NOTEPAD_ART_RECT := Rect2(
- NOTEPAD_HOST_RECT.position + EXPECTED_ART_POSITION,
- EXPECTED_ART_SIZE,
-)
-const HOTBAR_BUBBLE_FIELD_RECT := Rect2(245.0, 608.0, 790.0, 104.0)
-const TARGET_SIZES: Array[Vector2] = [
- Vector2(1280.0, 720.0),
- Vector2(1920.0, 1080.0),
- Vector2(2560.0, 1440.0),
- Vector2(3840.0, 2160.0),
- Vector2(640.0, 480.0),
- Vector2(1024.0, 768.0),
- Vector2(1729.0, 973.0),
- Vector2(2560.0, 1080.0),
- Vector2(3440.0, 1440.0),
-]
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- _print_runtime_parameters()
- var presentation_stage := Control.new()
- presentation_stage.name = "CanonicalUIStage"
- _apply_stage_layout(presentation_stage, Vector2(root.size))
- root.add_child(presentation_stage)
- var player_menu := PlayerMenuScene.instantiate() as PlayerMenu
- presentation_stage.add_child(player_menu)
- await process_frame
- var notepads: Array[Node] = player_menu.find_children(
- "*", "InventoryNotepad", true, false
- )
- assert(notepads.size() == 3)
- for node: Node in notepads:
- var notepad := node as InventoryNotepad
- assert(notepad != null)
- assert(notepad.size.is_equal_approx(EXPECTED_HOST_SIZE))
- assert(notepad.mouse_filter == Control.MOUSE_FILTER_PASS)
- var art_rect: Rect2 = notepad.get_art_rect()
- assert(art_rect.size.is_equal_approx(EXPECTED_ART_SIZE))
- assert(art_rect.position.is_equal_approx(EXPECTED_ART_POSITION))
- assert(art_rect.get_center().is_equal_approx(
- EXPECTED_HOST_SIZE * 0.5
- + InventoryNotepad.NOTEPAD_ART_OFFSET
- ))
- _validate_shared_inventory_geometry(player_menu)
- _validate_first_open_inventory(player_menu)
- await _validate_inventory_grid_centering(player_menu, presentation_stage)
- _validate_utility_page_geometry(player_menu)
- await _validate_profile_content_bounds(player_menu)
- await _validate_hotbar_centering(presentation_stage)
- _validate_inventory_layering(player_menu)
- _validate_sale_confirmation_copy(player_menu)
- _validate_tackle_to_items_transition(player_menu)
- _validate_cooler_notepad_typography(player_menu)
- _validate_resolution_matrix()
- await _capture_inventory_pages(player_menu)
- print("Inventory notepad artwork validation: PASS")
- presentation_stage.queue_free()
- for _frame: int in 10:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _print_runtime_parameters() -> void:
- var window: Window = root
- var screen_index: int = window.current_screen
- print(
- (
- "Runtime UI: window=%s screen=%s screen_scale=%.3f dpi=%d "
- + "content_size=%s content_mode=%d content_aspect=%d "
- + "content_factor=%.3f"
- )
- % [
- window.size,
- DisplayServer.screen_get_size(screen_index),
- DisplayServer.screen_get_scale(screen_index),
- DisplayServer.screen_get_dpi(screen_index),
- window.content_scale_size,
- window.content_scale_mode,
- window.content_scale_aspect,
- window.content_scale_factor,
- ]
- )
-
-
-func _apply_stage_layout(stage: Control, display_size: Vector2) -> void:
- stage.set_anchors_preset(Control.PRESET_TOP_LEFT)
- stage.position = UIReferencePresentationType.get_offset(display_size)
- stage.size = UIReferencePresentationType.REFERENCE_SIZE
- stage.scale = Vector2.ONE * UIReferencePresentationType.get_scale(
- display_size
- )
-
-
-func _validate_shared_inventory_geometry(player_menu: PlayerMenu) -> void:
- for node_name: StringName in [
- &"BagOuterWall",
- &"TackleMainPanel",
- ]:
- var main_panel := player_menu.get_node("%%%s" % node_name) as Control
- assert(main_panel != null)
- assert(main_panel.position.is_equal_approx(INVENTORY_PANEL_RECT.position))
- assert(
- main_panel.size.is_equal_approx(INVENTORY_PANEL_RECT.size),
- "%s was %s, expected %s" % [
- node_name,
- main_panel.size,
- INVENTORY_PANEL_RECT.size,
- ],
- )
- var cooler_panel := player_menu.get_node("%CoolerOuterWall") as Control
- var cooler_notepad := player_menu.get_node("%DetailConstellation") as Control
- assert(cooler_panel.position.is_equal_approx(
- LEGACY_COOLER_PANEL_RECT.position
- ))
- assert(cooler_panel.size.is_equal_approx(LEGACY_COOLER_PANEL_RECT.size))
- assert(cooler_notepad.position.is_equal_approx(
- LEGACY_COOLER_NOTEPAD_RECT.position
- ))
- assert(cooler_notepad.size.is_equal_approx(
- LEGACY_COOLER_NOTEPAD_RECT.size
- ))
- for node_name: StringName in [
- &"BagDetailConstellation",
- &"TackleDetailPanel",
- ]:
- var host := player_menu.get_node("%%%s" % node_name) as Control
- assert(host != null)
- assert(host.position.is_equal_approx(NOTEPAD_HOST_RECT.position))
- assert(host.size.is_equal_approx(EXPECTED_HOST_SIZE))
- assert(is_equal_approx(INVENTORY_PANEL_RECT.get_center().x, 640.0))
-
-
-func _validate_first_open_inventory(player_menu: PlayerMenu) -> void:
- var legacy_slots: Array = player_menu.get("_bag_slot_nodes") as Array
- assert(legacy_slots.is_empty())
- var old_slot_nodes: Array[Node] = player_menu.find_children(
- "*", "BagStorageSlot", true, false
- )
- assert(old_slot_nodes.is_empty())
-
-
-func _validate_inventory_grid_centering(
- player_menu: PlayerMenu,
- stage: Control,
-) -> void:
- var grid := player_menu.get("_general_inventory_grid") as Control
- assert(grid != null)
- grid.custom_minimum_size = Vector2(782.0, 342.0)
- grid.size = grid.custom_minimum_size
- player_menu.call("_layout_general_inventory_grid")
- await process_frame
- assert(is_equal_approx(grid.position.y, 39.0))
- assert(
- is_equal_approx(
- grid.get_global_rect().get_center().x,
- stage.get_global_rect().get_center().x,
- ),
- "inventory center %s did not match stage center %s" % [
- grid.get_global_rect().get_center().x,
- stage.get_global_rect().get_center().x,
- ],
- )
-
-
-func _validate_utility_page_geometry(player_menu: PlayerMenu) -> void:
- for page_name: StringName in [
- &"TheNetPage",
- &"MailPage",
- &"ProfilePage",
- &"PlayersPage",
- ]:
- var page := player_menu.get_node("%%%s" % page_name) as Control
- assert(page != null)
- var shell := page.find_child(
- "UtilityMainBox", true, false
- ) as Control
- assert(shell != null)
- assert(shell.position.is_equal_approx(UtilityPageStyle.LAPTOP_RECT.position))
- assert(shell.size.is_equal_approx(UtilityPageStyle.LAPTOP_RECT.size))
- assert(is_equal_approx(shell.get_rect().get_center().x, 640.0))
-
-
-func _validate_profile_content_bounds(player_menu: PlayerMenu) -> void:
- var profile := player_menu.get_node("%ProfilePage") as ProfilePage
- assert(profile != null)
- var menu_was_visible: bool = player_menu.visible
- var profile_was_visible: bool = profile.visible
- player_menu.visible = true
- profile.visible = true
- await process_frame
- await process_frame
- var option_list := profile.get("_option_list") as Control
- var body := option_list.get_parent() as Control
- var preview := profile.get("_preview") as Control
- var shell := profile.find_child("UtilityMainBox", true, false) as Control
- var content_zone := profile.find_child(
- "ProfileContentZone", true, false
- ) as Control
- var profile_layout := profile.find_child(
- "ProfileLayout", true, false
- ) as Control
- var account_row := profile.find_child(
- "ProfileAccountRow", true, false
- ) as Control
- var body_panel := profile.find_child(
- "ProfileBodyPanel", true, false
- ) as Control
- assert(option_list != null and body != null and preview != null)
- assert(shell != null and content_zone != null and profile_layout != null)
- assert(account_row != null and body_panel != null)
- var content_frame_size := Vector2(
- UtilityPageStyle.LAPTOP_RECT.size.x - 40.0,
- UtilityPageStyle.LAPTOP_RECT.size.y - 42.0,
- )
- assert(
- content_zone.size.is_equal_approx(content_frame_size),
- (
- "profile content zone changed size: %s != %s; "
- + "layout min=%s account min=%s body min=%s"
- ) % [
- content_zone.size,
- content_frame_size,
- profile_layout.get_combined_minimum_size(),
- account_row.get_combined_minimum_size(),
- body_panel.get_combined_minimum_size(),
- ],
- )
- assert(
- profile_layout.size.is_equal_approx(
- UtilityPageStyle.LAPTOP_CONTENT_SIZE
- ),
- "profile layout changed size: %s != %s" % [
- profile_layout.size,
- UtilityPageStyle.LAPTOP_CONTENT_SIZE,
- ],
- )
- assert(
- profile_layout.get_combined_minimum_size().x
- <= profile_layout.size.x,
- "profile layout exceeded shared content width: %s > %s" % [
- profile_layout.get_combined_minimum_size().x,
- profile_layout.size.x,
- ],
- )
- var content_rect: Rect2 = profile_layout.get_global_rect()
- for section: Control in [account_row, body_panel]:
- var section_rect: Rect2 = section.get_global_rect()
- assert(section_rect.position.x >= content_rect.position.x - 0.5)
- assert(section_rect.end.x <= content_rect.end.x + 0.5)
- var category_ids: Array[String] = []
- for category_id: String in CharacterCustomizationCatalog.CATEGORY_IDS:
- category_ids.append(category_id)
- category_ids.append("voice")
- for category_id: String in category_ids:
- profile.call("_select_category", category_id)
- await process_frame
- await process_frame
- assert(
- shell.position.is_equal_approx(
- UtilityPageStyle.LAPTOP_RECT.position
- ),
- "%s profile shell position changed: %s" % [
- category_id,
- shell.position,
- ],
- )
- assert(
- shell.size.is_equal_approx(UtilityPageStyle.LAPTOP_RECT.size),
- "%s profile shell size changed: %s != %s" % [
- category_id,
- shell.size,
- UtilityPageStyle.LAPTOP_RECT.size,
- ],
- )
- assert(
- content_zone.size.is_equal_approx(content_frame_size),
- "%s profile content zone changed size: %s != %s" % [
- category_id,
- content_zone.size,
- content_frame_size,
- ],
- )
- assert(
- profile_layout.size.is_equal_approx(
- UtilityPageStyle.LAPTOP_CONTENT_SIZE
- ),
- "%s profile layout changed size: %s != %s" % [
- category_id,
- profile_layout.size,
- UtilityPageStyle.LAPTOP_CONTENT_SIZE,
- ],
- )
- assert(
- profile_layout.get_combined_minimum_size().x
- <= profile_layout.size.x,
- "%s profile layout exceeded shared content width: %s > %s" % [
- category_id,
- profile_layout.get_combined_minimum_size().x,
- profile_layout.size.x,
- ],
- )
- var category_body_rect: Rect2 = body_panel.get_global_rect()
- assert(
- category_body_rect.end.x <= content_rect.end.x + 0.5,
- "%s body overflowed shared content: %s > %s" % [
- category_id,
- category_body_rect.end.x,
- content_rect.end.x,
- ],
- )
- assert(
- category_body_rect.end.y <= content_rect.end.y + 0.5,
- "%s body overflowed shared content vertically: %s > %s" % [
- category_id,
- category_body_rect.end.y,
- content_rect.end.y,
- ],
- )
- assert(
- body.get_combined_minimum_size().x <= body.size.x,
- "%s customization content overflowed its body: %s > %s" % [
- category_id,
- body.get_combined_minimum_size().x,
- body.size.x,
- ],
- )
- assert(
- preview.get_global_rect().end.x
- <= content_rect.end.x + 0.5,
- "%s preview overflowed the utility content area" % category_id,
- )
- profile.call("_select_category", "fur_pattern")
- profile.call("_select_fur_section", "colors")
- await process_frame
- await process_frame
- var color_panel := profile.find_child(
- "FurColorOptionsPanel", true, false
- ) as Control
- var color_scroll := profile.find_child(
- "FurColorScroll", true, false
- ) as ScrollContainer
- assert(color_panel != null and color_scroll != null)
- assert(
- color_panel.get_global_rect().end.y
- <= body_panel.get_global_rect().end.y + 0.5,
- "fur color panel overflowed profile body: %s > %s" % [
- color_panel.get_global_rect().end.y,
- body_panel.get_global_rect().end.y,
- ],
- )
- assert(
- profile_layout.get_combined_minimum_size().y
- <= profile_layout.size.y,
- "fur colors expanded profile layout vertically: %s > %s" % [
- profile_layout.get_combined_minimum_size().y,
- profile_layout.size.y,
- ],
- )
- profile.visible = profile_was_visible
- player_menu.visible = menu_was_visible
-
-
-func _validate_hotbar_centering(parent: Control) -> void:
- var hotbar := HotbarScene.instantiate() as HotbarUI
- parent.add_child(hotbar)
- await process_frame
- var presentation := hotbar.get_node(
- "%HotbarPresentationScaleRoot"
- ) as Control
- var field := hotbar.get_node("%BubbleField") as Control
- hotbar.set_player_menu_context(true)
- var displayed_center_x: float = (
- presentation.position.x
- + field.get_rect().get_center().x * presentation.scale.x
- )
- assert(is_equal_approx(displayed_center_x, 640.0))
- var displayed_top: float = (
- presentation.position.y + field.position.y * presentation.scale.y
- )
- var displayed_bottom: float = (
- displayed_top + field.size.y * presentation.scale.y
- )
- assert(displayed_top < INVENTORY_PANEL_RECT.end.y)
- assert(displayed_bottom > INVENTORY_PANEL_RECT.end.y)
- hotbar.queue_free()
-
-
-func _validate_inventory_layering(player_menu: PlayerMenu) -> void:
- var bag_panel := player_menu.get_node("%BagOuterWall") as Control
- var sale_confirmation := player_menu.get_node("%SaleConfirmation") as Control
- var cooler_panel := player_menu.get_node("%CoolerOuterWall") as Control
- var tackle_panel := player_menu.get_node("%TackleMainPanel") as Control
- var inventory_tabs := player_menu.get_node("%InventorySubTabs") as Control
- var items_tab := player_menu.get_node("%ItemsSubTab") as Button
- var bait_list := player_menu.get_node("%BaitItemList") as GridContainer
- var lure_list := player_menu.get_node("%LureItemList") as GridContainer
- assert(bag_panel != null)
- assert(sale_confirmation != null)
- assert(cooler_panel != null)
- assert(tackle_panel != null)
- assert(inventory_tabs != null)
- assert(items_tab != null and not items_tab.visible)
- assert(not (player_menu.get_node("%CoolerSubTab") as Button).visible)
- assert((player_menu.get_node("%BagSubTab") as Button).text == "Inventory")
- assert(bait_list != null and bait_list.columns == 4)
- assert(lure_list != null and lure_list.columns == 4)
- assert(bait_list.get_theme_constant("h_separation") == 28)
- assert(lure_list.get_theme_constant("h_separation") == 28)
- assert(sale_confirmation.z_index > cooler_panel.z_index)
- assert(sale_confirmation.z_index > tackle_panel.z_index)
- assert(
- (player_menu.get_node("%BagDetailConstellation") as Control).z_index
- > bag_panel.z_index
- )
- assert(
- (player_menu.get_node("%BagModalBlocker") as Control).z_index
- > bag_panel.z_index
- )
- assert(
- (player_menu.get_node("%BagDetailConstellation") as Control).z_index
- > (player_menu.get_node("%BagModalBlocker") as Control).z_index
- )
- assert(
- (player_menu.get_node("%TackleDetailPanel") as Control).z_index
- > tackle_panel.z_index
- )
- # The contextual Hotbar uses z=90 while the Player Menu is open.
- assert(sale_confirmation.z_index > 90)
- assert(sale_confirmation.position.is_equal_approx(Vector2(380.0, 265.0)))
- assert(sale_confirmation.size.is_equal_approx(Vector2(520.0, 200.0)))
- var confirmation_style := sale_confirmation.get_theme_stylebox(
- "panel"
- ) as StyleBoxFlat
- assert(confirmation_style != null)
- assert(
- confirmation_style.bg_color.is_equal_approx(
- UtilityPageStyle.OCEAN_PANEL_DEEP
- )
- )
-
-
-func _validate_sale_confirmation_copy(player_menu: PlayerMenu) -> void:
- var preview := FishSaleResultType.new()
- preview.fish_count = 1
- preview.payout = 3
- preview.base_value = 12
- var message: String = str(player_menu.call(
- "_sale_confirmation_text",
- preview,
- PelicanBuyer,
- preview.base_value,
- ))
- assert(message == (
- "[center]You can sell this fish to the pelicans now for %s, "
- + "but the shop is willing to pay %s![/center]"
- ) % [
- CurrencyPresentationType.bbcode_amount(preview.payout, 22),
- CurrencyPresentationType.bbcode_amount(preview.base_value, 22),
- ])
- assert((player_menu.get_node("%ConfirmSaleButton") as Button).text == "sell now")
- assert((player_menu.get_node("%CancelSaleButton") as Button).text == "nevermind")
-
-
-func _validate_tackle_to_items_transition(player_menu: PlayerMenu) -> void:
- var inventory_notepad := player_menu.get_node(
- "%BagDetailBubble"
- ) as InventoryNotepad
- assert(inventory_notepad != null)
- assert(inventory_notepad.title_text == "inventory notes")
- player_menu.call(
- "_show_section_immediate", PlayerMenu.Section.TACKLE_BOX
- )
- player_menu.call("_show_inventory_tab", 0)
- player_menu.call("_cancel_page_tween")
- player_menu.call("_show_section_immediate", PlayerMenu.Section.BAG)
- player_menu.call("_update_bag_detail")
- assert(player_menu.get("_current_section") == PlayerMenu.Section.BAG)
- assert(not (player_menu.get_node("%BagDetailConstellation") as Control).visible)
- assert(
- (player_menu.get_node("%BagSpriteDetailData") as Label).text
- == "select an item for details."
- )
- player_menu.visible = true
- player_menu.call("_set_content_interactive", true)
- var no_actions: Array[BaseButton] = []
- player_menu.call(
- "_open_inventory_notepad",
- PlayerMenu.Section.BAG,
- StringName("modal-test"),
- no_actions,
- )
- assert((player_menu.get_node("%BagDetailConstellation") as Control).visible)
- assert((player_menu.get_node("%BagModalBlocker") as Control).visible)
- assert(
- (player_menu.get_node("%BagModalBlocker") as Control).mouse_filter
- == Control.MOUSE_FILTER_STOP
- )
- assert(
- (player_menu.get_node("%InventoryTab") as Button).focus_mode
- == Control.FOCUS_NONE
- )
- assert(
- (player_menu.get_node("%BagSpriteDetailData") as Label).get_theme_font(
- "font"
- ) == InventoryNotepad.NOTEPAD_FONT
- )
- player_menu.call(
- "_release_controller_ownership", false, false
- )
- assert(not (player_menu.get_node("%BagModalBlocker") as Control).visible)
- player_menu.call("_set_content_interactive", false)
- player_menu.visible = false
- player_menu.call("_cancel_page_tween")
-
-
-func _validate_cooler_notepad_typography(player_menu: PlayerMenu) -> void:
- var sort_choice := player_menu.get_node("%CoolerSortOption") as Control
- var sort_direction := player_menu.get_node(
- "%CoolerSortDirection"
- ) as Button
- var empty_selection := player_menu.get_node(
- "%CoolerSelectionEmpty"
- ) as Label
- var favorite := player_menu.get_node("%FavoriteBubble") as Button
- var sell := player_menu.get_node("%SellBubble") as Button
- assert(sort_choice != null)
- assert(sort_direction != null)
- assert(empty_selection != null)
- assert(favorite != null and sell != null)
- assert(player_menu.get_node_or_null("%SellAllBubble") == null)
- var displayed_value := sort_choice.get_node("%DisplayedValue") as Label
- assert(displayed_value != null)
- assert(displayed_value.get_theme_font_size("font_size") == 17)
- for choice_name: StringName in [
- &"CatchOrderChoice",
- &"NameChoice",
- &"RarityChoice",
- ]:
- var choice := sort_choice.get_node("%%%s" % choice_name) as Button
- assert(choice != null)
- assert(choice.get_theme_font_size("font_size") == 17)
- assert(sort_direction.get_theme_font_size("font_size") == 17)
- assert(empty_selection.get_theme_font_size("font_size") == 20)
- assert(favorite.get_theme_font_size("font_size") == 19)
- assert(sell.get_theme_font_size("font_size") == 19)
-
-
-func _validate_resolution_matrix() -> void:
- for target_size: Vector2 in TARGET_SIZES:
- var stage_rect: Rect2 = UIReferencePresentationType.get_rect(
- target_size
- )
- var stage_scale: float = UIReferencePresentationType.get_scale(
- target_size
- )
- assert(is_equal_approx(
- stage_rect.size.x / stage_rect.size.y,
- 1280.0 / 720.0,
- ))
- assert(stage_rect.get_center().is_equal_approx(target_size * 0.5))
- _validate_transformed_rect(
- "Inventory panel", INVENTORY_PANEL_RECT, stage_rect, stage_scale
- )
- _validate_transformed_rect(
- "Notepad host", NOTEPAD_HOST_RECT, stage_rect, stage_scale
- )
- _validate_transformed_rect(
- "Notepad art", NOTEPAD_ART_RECT, stage_rect, stage_scale
- )
- _validate_transformed_rect(
- "Hotbar", HOTBAR_BUBBLE_FIELD_RECT, stage_rect, stage_scale
- )
- var screen_host: Rect2 = _to_screen_rect(
- NOTEPAD_HOST_RECT, stage_rect.position, stage_scale
- )
- var screen_art: Rect2 = _to_screen_rect(
- NOTEPAD_ART_RECT, stage_rect.position, stage_scale
- )
- assert(is_equal_approx(
- screen_art.size.y,
- screen_host.size.y * InventoryNotepad.NOTEPAD_CANONICAL_OVERSCAN,
- ))
- assert(screen_art.get_center().is_equal_approx(
- screen_host.get_center()
- + InventoryNotepad.NOTEPAD_ART_OFFSET * stage_scale
- ))
- print(
- (
- "UI geometry: window=%s stage=%s scale=%.6f "
- + "inventory=%s notepad=%s art=%s hotbar=%s"
- )
- % [
- target_size,
- stage_rect,
- stage_scale,
- _to_screen_rect(
- INVENTORY_PANEL_RECT,
- stage_rect.position,
- stage_scale,
- ),
- screen_host,
- screen_art,
- _to_screen_rect(
- HOTBAR_BUBBLE_FIELD_RECT,
- stage_rect.position,
- stage_scale,
- ),
- ]
- )
-
-
-func _validate_transformed_rect(
- label: String,
- canonical_rect: Rect2,
- stage_rect: Rect2,
- stage_scale: float,
-) -> void:
- var screen_rect: Rect2 = _to_screen_rect(
- canonical_rect, stage_rect.position, stage_scale
- )
- assert(
- screen_rect.position.is_equal_approx(
- stage_rect.position + canonical_rect.position * stage_scale
- ),
- "%s position does not use the single stage transform" % label,
- )
- assert(
- screen_rect.size.is_equal_approx(canonical_rect.size * stage_scale),
- "%s size does not use the single stage transform" % label,
- )
-
-
-func _to_screen_rect(
- canonical_rect: Rect2,
- stage_offset: Vector2,
- stage_scale: float,
-) -> Rect2:
- return Rect2(
- stage_offset + canonical_rect.position * stage_scale,
- canonical_rect.size * stage_scale,
- )
-
-
-func _capture_inventory_pages(player_menu: PlayerMenu) -> void:
- if not OS.has_environment("straywild_NOTEPAD_CAPTURE"):
- return
- player_menu.visible = true
- var sections: Array[PlayerMenu.Section] = [
- PlayerMenu.Section.BAG,
- PlayerMenu.Section.TACKLE_BOX,
- ]
- var suffixes: Array[String] = ["inventory", "tackle"]
- for index: int in sections.size():
- player_menu.call("_show_section_immediate", sections[index])
- await process_frame
- await process_frame
- await _save_capture(suffixes[index])
- player_menu.call("_show_section_immediate", PlayerMenu.Section.BAG)
- var sale_confirmation := player_menu.get_node("%SaleConfirmation") as Control
- var confirmation_message := player_menu.get_node(
- "%ConfirmationMessage"
- ) as Label
- assert(sale_confirmation != null)
- assert(confirmation_message != null)
- confirmation_message.text = (
- "You can sell this fish to the pelicans now, "
- + "but the shop will pay more!"
- )
- sale_confirmation.visible = true
- await process_frame
- await process_frame
- await _save_capture("sale-confirmation")
- sale_confirmation.visible = false
-
-
-func _save_capture(suffix: String) -> void:
- var image: Image = root.get_texture().get_image()
- var path: String = "%s-%s.png" % [
- OS.get_environment("straywild_NOTEPAD_CAPTURE"),
- suffix,
- ]
- assert(image.save_png(path) == OK)
diff --git a/tests/inventory_notepad_art_validation.gd.uid b/tests/inventory_notepad_art_validation.gd.uid
deleted file mode 100644
index 59a9b8b..0000000
--- a/tests/inventory_notepad_art_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://np6jfuf8tej8
diff --git a/tests/inventory_storage_persistence_validation.gd b/tests/inventory_storage_persistence_validation.gd
deleted file mode 100644
index 794f438..0000000
--- a/tests/inventory_storage_persistence_validation.gd
+++ /dev/null
@@ -1,83 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-const ItemCatalogResource: ItemCatalog = preload(
- "res://items/catalog/item_catalog.tres"
-)
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
-
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- var player := main.get("_player") as Player
- assert(save_manager != null and player != null)
- const TEST_WORLD_SEED := 918273
- assert(save_manager.initialize_new_game(
- TEST_WORLD_SEED,
- WorldLayout.STARTER_ISLAND,
- ))
- save_manager.set_autosave_enabled(true)
- assert(player.bag.add_item(&"art_kit"))
- assert(player.bag.add_item(&"coffee"))
- assert(player.bag.add_item(&"worms"))
- assert(player.bag.add_item(&"the_standby"))
- assert(player.equip_bait(ItemCatalogResource.get_item_by_id(&"worms")))
- assert(player.equip_lure(ItemCatalogResource.get_item_by_id(&"the_standby")))
- assert(player.bag.move_item_to_storage_slot(&"basic_fishing_rod", 14))
- assert(player.bag.move_item_to_storage_slot(&"coffee", 12))
- assert(save_manager.save_now())
-
- var decoded: Dictionary = ProgressionSaveCodec.read_local_save(
- str(save_manager.get("_save_path"))
- )
- assert(bool(decoded.get("ok", false)))
- var parsed: Dictionary = decoded["data"]
- var records: Array = parsed["bag"]["items"]
- assert(str(parsed["world"]["layout"]) == String(WorldLayout.STARTER_ISLAND))
- assert(int(parsed["world"]["seed"]) == TEST_WORLD_SEED)
- assert(_saved_slot(records, &"basic_fishing_rod") == 14)
- assert(_saved_slot(records, &"coffee") == 12)
-
- assert(player.bag.move_item_to_storage_slot(&"basic_fishing_rod", 0))
- assert(player.bag.move_item_to_storage_slot(&"coffee", 0))
- player.unequip_bait()
- player.unequip_lure()
- assert(save_manager.load_player_data())
- assert(save_manager.get_world_layout() == WorldLayout.STARTER_ISLAND)
- assert(save_manager.get_world_seed() == TEST_WORLD_SEED)
- assert(player.bag.get_storage_slot(&"basic_fishing_rod") == 14)
- assert(player.bag.get_storage_slot(&"coffee") == 12)
- assert(player.active_bait_id == &"worms")
- assert(player.active_lure_id == &"the_standby")
-
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- print("Inventory storage persistence validation: PASS")
- quit()
-
-
-func _saved_slot(records: Array, item_id: StringName) -> int:
- for value: Variant in records:
- if (
- typeof(value) == TYPE_DICTIONARY
- and str((value as Dictionary).get("item_id", ""))
- == String(item_id)
- ):
- return int((value as Dictionary).get("storage_slot", -1))
- return -1
diff --git a/tests/inventory_storage_persistence_validation.gd.uid b/tests/inventory_storage_persistence_validation.gd.uid
deleted file mode 100644
index 6edda3b..0000000
--- a/tests/inventory_storage_persistence_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bwp2jhs116hmw
diff --git a/tests/inventory_storage_validation.gd b/tests/inventory_storage_validation.gd
deleted file mode 100644
index eccb66d..0000000
--- a/tests/inventory_storage_validation.gd
+++ /dev/null
@@ -1,127 +0,0 @@
-extends SceneTree
-
-const Catalog: ItemCatalog = preload("res://items/catalog/item_catalog.tres")
-const Bluegill: FishData = preload("res://fish/species/bluegill/bluegill.tres")
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var state := Node.new()
- root.add_child(state)
- var bag := PlayerBag.new()
- var catches := FishInventory.new()
- var storage_capacity := PlayerCoolerCapacity.new()
- var layout := PlayerInventoryLayout.new()
- var hotbar := PlayerHotbar.new()
- for node: Node in [bag, catches, storage_capacity, layout, hotbar]:
- state.add_child(node)
- bag.setup(Catalog)
- layout.setup(bag, catches, Catalog, storage_capacity)
- bag.set_inventory_layout(layout)
- catches.set_inventory_layout(layout)
- hotbar.setup(bag, Catalog, catches, layout)
-
- assert(bag.add_item(&"basic_fishing_rod", 1))
- assert(bag.add_item(&"coffee", 3))
- var fish_catch := _make_bluegill()
- assert(catches.add_catch(fish_catch))
-
- var inventory_grid := GeneralInventoryGrid.new()
- inventory_grid.set_slot_presentation(Vector2(78.0, 78.0), 10)
- root.add_child(inventory_grid)
- inventory_grid.setup(
- layout,
- bag,
- catches,
- hotbar,
- Catalog,
- PlayerInventoryLayout.InventoryContainer.INVENTORY,
- )
- var storage_grid := GeneralInventoryGrid.new()
- root.add_child(storage_grid)
- storage_grid.setup(
- layout,
- bag,
- catches,
- hotbar,
- Catalog,
- PlayerInventoryLayout.InventoryContainer.STORAGE,
- )
- await process_frame
-
- var inventory_slots: Array = inventory_grid.get("_slots")
- var storage_slots: Array = storage_grid.get("_slots")
- assert(inventory_slots.size() == 36)
- assert(inventory_grid.get_slots().size() == 9)
- assert(inventory_grid.custom_minimum_size == Vector2(782.0, 342.0))
- assert(storage_capacity.get_capacity() == 9)
- assert(storage_slots.size() == 72)
- assert(storage_grid.get_slots().size() == 9)
- assert((storage_slots[0] as Control).custom_minimum_size == Vector2(52.0, 52.0))
- assert((storage_slots[9] as GeneralInventorySlot).disabled)
-
- var coffee_slot := _find_slot(inventory_slots, &"coffee")
- assert(coffee_slot != null)
- (inventory_slots[7] as GeneralInventorySlot).call(
- "_drop_data",
- Vector2.ZERO,
- {"kind": "bag_item", "item_id": "coffee"},
- )
- assert(int(layout.get_entry(
- PlayerInventoryLayout.EntryKind.ITEM, &"coffee"
- ).get("slot", -1)) == 7)
- assert(coffee_slot.entry_identity.is_empty())
-
- (storage_slots[0] as GeneralInventorySlot).call(
- "_drop_data",
- Vector2.ZERO,
- {"kind": "cooler_fish", "catch_id": String(fish_catch.catch_id)},
- )
- assert(layout.get_container(
- PlayerInventoryLayout.EntryKind.CATCH, fish_catch.catch_id
- ) == PlayerInventoryLayout.InventoryContainer.STORAGE)
-
- assert(hotbar.assign_item(0, &"basic_fishing_rod"))
- assert(not layout.is_item_in_inventory(&"basic_fishing_rod"))
- (inventory_slots[8] as GeneralInventorySlot).call(
- "_drop_data",
- Vector2.ZERO,
- {"kind": "hotbar_slot", "slot_index": 0},
- )
- assert(hotbar.get_item_id(0).is_empty())
- assert(layout.is_item_in_inventory(&"basic_fishing_rod"))
- assert(int(layout.get_entry(
- PlayerInventoryLayout.EntryKind.ITEM, &"basic_fishing_rod"
- ).get("slot", -1)) == 8)
-
- print("Inventory storage validation: PASS")
- state.free()
- inventory_grid.free()
- storage_grid.free()
- quit()
-
-
-func _make_bluegill() -> FishCatch:
- var fish_catch := FishCatch.new()
- fish_catch.fish = Bluegill
- fish_catch.fish_id = Bluegill.id
- fish_catch.catch_id = &"bluegill:inventory_storage_test"
- fish_catch.weight_lb = Bluegill.get_minimum_weight()
- fish_catch.display_scale = Bluegill.get_display_scale_for_weight(
- fish_catch.weight_lb
- )
- fish_catch.sale_value = Bluegill.get_sale_value_for_weight(
- fish_catch.weight_lb
- )
- return fish_catch
-
-
-func _find_slot(slots: Array, identity: StringName) -> GeneralInventorySlot:
- for candidate: Variant in slots:
- var slot := candidate as GeneralInventorySlot
- if slot != null and slot.entry_identity == identity:
- return slot
- return null
diff --git a/tests/inventory_storage_validation.gd.uid b/tests/inventory_storage_validation.gd.uid
deleted file mode 100644
index 1c8717c..0000000
--- a/tests/inventory_storage_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://w6e06q8yfwp6
diff --git a/tests/job_multiplayer_validation.gd b/tests/job_multiplayer_validation.gd
deleted file mode 100644
index 6c0fa5b..0000000
--- a/tests/job_multiplayer_validation.gd
+++ /dev/null
@@ -1,132 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-const TEST_PORT: int = 18139
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var arguments: PackedStringArray = OS.get_cmdline_user_args()
- if arguments.has("host"):
- await _run_host()
- return
- if arguments.has("client"):
- await _run_client()
- return
- push_error("Job multiplayer validation needs host or client mode.")
- quit(1)
-
-
-func _run_host() -> void:
- var main: Node = await _create_initialized_main()
- var session := main.get_node("%NetworkSession") as NetworkSession
- var jobs := main.get_node("%PlayerJobService") as PlayerJobService
- assert(bool(main.call(
- "_apply_world", WorldLayout.STARTER_ISLAND, 1, true
- )))
- assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
- assert(session.start_dedicated_host(TEST_PORT, 8, "127.0.0.1"))
- jobs.begin_progression_session()
- await process_frame
- assert(session.set_host_open(true))
- assert(session.is_dedicated_host())
- var host_board: Dictionary = jobs.get_host_board_network_data()
- assert(PlayerJobService.validate_board(host_board))
-
- var remote_peer_id: int = 0
- var join_deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < join_deadline and remote_peer_id == 0:
- await process_frame
- for peer_id: int in session.get_authenticated_peer_ids():
- if peer_id != session.get_local_peer_id():
- remote_peer_id = peer_id
- break
- assert(remote_peer_id > 1)
- assert(session.peer_supports_capability(
- remote_peer_id, NetworkProtocol.JOBS_CAPABILITY
- ))
-
- var disconnect_deadline: int = Time.get_ticks_msec() + 12000
- while (
- Time.get_ticks_msec() < disconnect_deadline
- and session.is_authenticated_peer(remote_peer_id)
- ):
- await process_frame
- assert(not session.is_authenticated_peer(remote_peer_id))
- print("Job multiplayer host validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _run_client() -> void:
- var main: Node = await _create_initialized_main()
- main.call(
- "_on_title_join_game_requested",
- "127.0.0.1:%d" % TEST_PORT,
- )
- var session := main.get_node("%NetworkSession") as NetworkSession
- var jobs := main.get_node("%PlayerJobService") as PlayerJobService
- var weather := main.get_node("%WorldWeatherService") as WorldWeatherService
- var join_deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < join_deadline:
- await process_frame
- if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
- main.call("_confirm_server_trust")
- if (
- session.is_joined_client()
- and bool(main.get("_gameplay_started"))
- and not jobs.get_plan_id().is_empty()
- ):
- break
- assert(session.is_joined_client())
- assert(session.supports_server_capability(NetworkProtocol.JOBS_CAPABILITY))
- assert(not jobs.get_plan_id().is_empty())
- assert(jobs.get_daily_jobs().size() == JobCatalog.DAILY_JOB_COUNT)
- assert(jobs.get_forecast().size() == JobCatalog.WEATHER_SEGMENT_COUNT)
- var preserved_plan_id: String = jobs.get_plan_id()
- jobs.clear_remote_board()
- assert(not jobs.has_active_board())
- assert(jobs.get_plan_id() == preserved_plan_id)
- var retry_deadline: int = Time.get_ticks_msec() + 5000
- while (
- Time.get_ticks_msec() < retry_deadline
- and not jobs.has_active_board()
- ):
- await process_frame
- assert(jobs.has_active_board())
- assert(jobs.get_plan_id() == preserved_plan_id)
- assert(jobs.get_daily_jobs().size() == JobCatalog.DAILY_JOB_COUNT)
- assert(jobs.get_forecast().size() == JobCatalog.WEATHER_SEGMENT_COUNT)
- assert(weather.get_daily_plan_id().is_empty())
- var game_ui := main.get_node("%GameUI") as GameUI
- var player_menu := game_ui.get("_player_menu") as PlayerMenu
- player_menu.call("_show_section_immediate", PlayerMenu.Section.NET)
- assert((player_menu.get_node("%TheNetPage") as TheNetPage).visible)
- print("Job multiplayer client validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _create_initialized_main() -> Node:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- return main
diff --git a/tests/job_multiplayer_validation.gd.uid b/tests/job_multiplayer_validation.gd.uid
deleted file mode 100644
index 1b526ba..0000000
--- a/tests/job_multiplayer_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c4y0pmxk2bhmu
diff --git a/tests/job_system_validation.gd b/tests/job_system_validation.gd
deleted file mode 100644
index 65b7164..0000000
--- a/tests/job_system_validation.gd
+++ /dev/null
@@ -1,502 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-const Catalog: FishPool = preload("res://fish/pools/fish_catalog.tres")
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- var session := main.get_node("%NetworkSession") as NetworkSession
- assert(session != null)
- assert(bool(main.call(
- "_apply_world", WorldLayout.STARTER_ISLAND, 1, true
- )))
- assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
- var occupied_port := PacketPeerUDP.new()
- assert(occupied_port.bind(18137, "127.0.0.1") == OK)
- assert(session.start_private_host(18137, 3))
- assert(session.get_host_port() == 18138)
- occupied_port.close()
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND))
- main.call("_enter_gameplay")
- await physics_frame
- await physics_frame
-
- var jobs := main.get_node("%PlayerJobService") as PlayerJobService
- var weather := main.get_node("%WorldWeatherService") as WorldWeatherService
- var network_fishing := main.get(
- "_network_fishing"
- ) as NetworkFishingService
- var network_sale := main.get("_network_sale") as NetworkSaleService
- var player := main.get("_player") as Player
- assert(jobs != null and weather != null and player != null)
- assert(network_fishing != null and network_sale != null)
-
- var board: Dictionary = jobs.get_host_board_network_data()
- assert(PlayerJobService.validate_board(board))
- assert(jobs.get_daily_jobs().size() == JobCatalog.DAILY_JOB_COUNT)
- assert(weather.get_daily_plan_id() == jobs.get_plan_id())
- _validate_weather_guarantees(board)
- _validate_late_board_weather_fallback()
- _validate_remote_tamper_rejection(jobs, board)
- _validate_creature_jobs(jobs)
-
- var sell_job: Dictionary = _find_job(
- jobs.get_daily_jobs(), JobCatalog.Kind.SELL_TOTAL
- )
- assert(not sell_job.is_empty())
- var sell_target: int = int(sell_job.get("target", 0))
- var sold_ids: Array[StringName] = []
- for index: int in sell_target * 2:
- sold_ids.append(StringName("job-sale-%d" % index))
- network_sale.local_sale_finished.emit(
- "job-test", true, "sold", sold_ids, 1
- )
- await process_frame
- var pending: Array[Dictionary] = jobs.get_pending_rewards()
- assert(pending.size() == 1)
- var refreshed_sell: Dictionary = _find_job(
- jobs.get_daily_jobs(), JobCatalog.Kind.SELL_TOTAL
- )
- assert(int(refreshed_sell.get("progress", -1)) == sell_target)
- assert(int(refreshed_sell.get("completed_count", 0)) == 1)
-
- var game_ui := main.get_node("%GameUI") as GameUI
- var player_menu := game_ui.get("_player_menu") as PlayerMenu
- assert(player_menu != null)
- player_menu.open_menu()
- for _frame: int in 24:
- await process_frame
- assert(player_menu.visible)
- player_menu.call("_show_section_immediate", PlayerMenu.Section.NET)
- var net_page := player_menu.get_node("%TheNetPage") as TheNetPage
- assert(net_page.visible)
- assert((player_menu.get_node("%TheNetTab") as Button).button_pressed)
- assert(
- (player_menu.get_node("%NavigationCluster") as Control).get_child_count()
- == 7
- )
- net_page.call("_select_view", TheNetPage.View.LIFETIME)
- await process_frame
- _validate_fishnet_bounds(net_page)
- await _validate_compact_value_tooltips(net_page)
- await _validate_unavailable_fishnet_layout()
-
- var wallet_before: int = player.wallet.get_balance()
- var experience_before: int = player.experience.get_total_experience()
- var first_claim_id: String = str(pending[0].get("claim_id", ""))
- net_page.call("_claim", first_claim_id)
- await process_frame
- await process_frame
- assert(bool(game_ui.get("_experience_animation_active")))
- var experience_presentation := (
- game_ui.get_node("%ExperiencePresentation") as Control
- )
- assert(experience_presentation.visible)
- assert((game_ui.get_node("%ExperienceProgressPanel") as Control).visible)
- assert((game_ui.get_node("%ExperienceBubble") as Control).visible)
- assert(
- experience_presentation.get_index()
- == experience_presentation.get_parent().get_child_count() - 1
- )
- assert(not (game_ui.get_node("%GameplayTransientHUD") as Control).visible)
- assert(player_menu.visible)
- assert(not jobs.claim(first_claim_id))
- assert(
- player.wallet.get_balance()
- == wallet_before + int(pending[0].get("fish_coin", 0))
- )
- assert(
- player.experience.get_total_experience()
- == experience_before + int(pending[0].get("experience", 0))
- )
- refreshed_sell = _find_job(
- jobs.get_daily_jobs(), JobCatalog.Kind.SELL_TOTAL
- )
- assert(int(refreshed_sell.get("progress", -1)) == sell_target)
- assert(int(refreshed_sell.get("completed_count", 0)) == 1)
- assert(not bool(refreshed_sell.get("claimable", true)))
- network_sale.local_sale_finished.emit(
- "job-test-repeat", true, "sold", sold_ids, 1
- )
- await process_frame
- assert(jobs.get_pending_rewards().is_empty())
- refreshed_sell = _find_job(
- jobs.get_daily_jobs(), JobCatalog.Kind.SELL_TOTAL
- )
- assert(int(refreshed_sell.get("progress", -1)) == sell_target)
- assert(int(refreshed_sell.get("completed_count", 0)) == 1)
-
- var fresh_job: Dictionary = _find_job(
- jobs.get_daily_jobs(), JobCatalog.Kind.CATCH_WATER
- )
- assert(not fresh_job.is_empty())
- var bluegill: FishData = Catalog.get_fish_by_id(&"bluegill")
- assert(bluegill != null)
- for index: int in int(fresh_job.get("target", 0)):
- var fish_catch := FishCatch.new()
- fish_catch.fish = bluegill
- fish_catch.fish_id = bluegill.id
- fish_catch.catch_id = StringName("job-catch-%d" % index)
- fish_catch.catch_sequence = index + 1
- fish_catch.weight_lb = bluegill.get_minimum_weight()
- fish_catch.display_scale = bluegill.get_display_scale_for_weight(
- fish_catch.weight_lb
- )
- fish_catch.quality = FishQuality.Tier.BORING
- fish_catch.sale_value = bluegill.get_sale_value_for_weight(
- fish_catch.weight_lb
- )
- assert(fish_catch.is_valid())
- network_fishing.local_catch_received.emit(fish_catch)
- await process_frame
- var refreshed_fresh: Dictionary = _find_job(
- jobs.get_daily_jobs(), JobCatalog.Kind.CATCH_WATER
- )
- assert(int(refreshed_fresh.get("completed_count", 0)) == 1)
-
- var pause_menu := game_ui.get_pause_menu()
- var join_page := pause_menu.get_node("%JoinGamePage") as JoinGamePage
- assert(join_page != null)
- pause_menu.show()
- join_page.set_status("Connection timed out.")
- join_page.show()
- join_page.call(
- "_on_discovery_status_changed", "2 public rooms found", false
- )
- var join_status := join_page.get_node("%Status") as Label
- assert(join_status.text == "Could not reach the server.")
- join_page.hide()
- join_page.call("_set_mode", JoinGamePage.Mode.DIRECT)
- join_page.call("_set_mode", JoinGamePage.Mode.DISCOVER)
- join_page.show()
- join_page.call(
- "_on_discovery_status_changed", "2 public rooms found", false
- )
- assert(join_status.text == "2 public rooms found")
- join_page.close_page()
- pause_menu.hide()
- join_page.call("_refresh")
- var session_summary := (
- join_page.get_node("%SessionSummary") as Label
- )
- assert(not join_page.has_node(
- "Paper/Margin/Layout/Actions/OpenCloseButton"
- ))
- assert("UDP 18138" in session_summary.text)
- assert(session.set_host_open(true))
- await process_frame
- assert("UDP 18138" in session_summary.text)
- assert(session.set_host_open(false))
-
- assert(save_manager.save_now())
- var decoded: Dictionary = ProgressionSaveCodec.read_local_save(
- str(save_manager.get("_save_path"))
- )
- assert(bool(decoded.get("ok", false)))
- var save_data: Dictionary = decoded["data"]
- assert(int(save_data.get("save_version", -1)) == 11)
- assert(PlayerJobService.validate_save_data(save_data.get("jobs", {})))
- assert(not PlayerHomeState.sanitize_save_data(
- save_data.get("home", {})
- ).is_empty())
-
- _validate_pause_session_switch(main, session)
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- print("Job system validation: PASS")
- quit()
-
-
-func _validate_pause_session_switch(
- main: Node,
- session: NetworkSession,
-) -> void:
- var discovery := main.get_node("%DiscoveryClient") as DiscoveryClient
- discovery.set("_pending_join_endpoint", "127.0.0.1:18141")
- discovery.set("_pending_join_room_id", "session-switch-room")
- discovery.set("_pending_join_token", "session-switch-token")
- discovery.call(
- "_set_public_join_state",
- DiscoveryClient.PublicJoinState.CONNECTING,
- )
- main.call("_on_pause_join_game_requested", "127.0.0.1:18141")
- assert(session.state == NetworkSession.State.CONNECTING)
- assert(discovery.get("_pending_join_token") == "session-switch-token")
- var join_probe_timer := discovery.get("_join_probe_timer") as Timer
- assert(join_probe_timer != null and not join_probe_timer.is_stopped())
- session.cancel_connection()
- assert(session.state == NetworkSession.State.INACTIVE)
- assert(str(discovery.get("_pending_join_token")).is_empty())
-
-
-func _validate_unavailable_fishnet_layout() -> void:
- var unavailable_page := TheNetPage.new()
- unavailable_page.size = Vector2(1280.0, 720.0)
- root.add_child(unavailable_page)
- for _frame: int in 3:
- await process_frame
- unavailable_page.call("_refresh_forecast", true)
- await process_frame
- var forecast_list := unavailable_page.get("_forecast_list") as HBoxContainer
- assert(forecast_list != null)
- assert(forecast_list.size.x >= 272.0)
- assert(forecast_list.size.y <= 66.0)
- assert(forecast_list.get_child_count() == 1)
- var placeholder := forecast_list.get_child(0) as Label
- assert(placeholder != null)
- assert(placeholder.size.x >= 252.0)
- assert(placeholder.size.y <= 66.0)
- assert(placeholder.autowrap_mode == TextServer.AUTOWRAP_OFF)
- var tabs := unavailable_page.get("_tabs") as HBoxContainer
- assert(tabs != null)
- assert(tabs.position.y + tabs.size.y < UtilityPageStyle.LAPTOP_RECT.end.y)
- unavailable_page.queue_free()
- for _frame: int in 2:
- await process_frame
-
-
-func _validate_fishnet_bounds(page: TheNetPage) -> void:
- var laptop := page.get_node("UtilityMainBox") as PanelContainer
- var forecast_column := page.find_child(
- "ForecastColumn", true, false
- ) as VBoxContainer
- var refresh_label := page.find_child(
- "DailyRefreshLabel", true, false
- ) as Label
- var jobs_scroll := page.find_child("JobsScroll", true, false) as ScrollContainer
- assert(laptop != null)
- assert(forecast_column != null and refresh_label != null)
- assert(jobs_scroll != null)
- var laptop_rect: Rect2 = laptop.get_global_rect()
- assert(forecast_column.get_global_rect().end.x <= laptop_rect.end.x)
- assert(refresh_label.get_global_rect().end.x <= laptop_rect.end.x)
- assert(refresh_label.get_global_rect().end.y <= laptop_rect.end.y)
- assert(jobs_scroll.horizontal_scroll_mode == ScrollContainer.SCROLL_MODE_DISABLED)
- var list := page.get("_list") as VBoxContainer
- assert(list != null)
- for row: Control in list.get_children():
- assert(row.size.x <= jobs_scroll.size.x + 0.5)
-
-
-func _validate_compact_value_tooltips(page: TheNetPage) -> void:
- assert(str(page.call("_compact_integer", 2400)) == "2.4k")
- assert(str(page.call("_compact_integer", 2500)) == "2.5k")
- var list := page.get("_list") as VBoxContainer
- assert(list != null)
- var jobs: Array[Dictionary] = [{
- "title": "compact tooltip check",
- "description": "compact values keep their exact values on hover",
- "target": 2500,
- "progress": 2400,
- "fish_coin": 1200,
- "experience": 3400,
- }]
- page.call("_build_job_rows", jobs, "")
- await process_frame
- var row := list.get_child(list.get_child_count() - 1) as PanelContainer
- assert(row != null)
- var progress_bar: ProgressBar
- var progress_count: Label
- var reward: VBoxContainer
- for child: Node in row.find_children("*", "ProgressBar", true, false):
- progress_bar = child as ProgressBar
- break
- for child: Node in row.find_children("*", "Label", true, false):
- var label := child as Label
- if label != null and label.text == "2.4k / 2.5k":
- progress_count = label
- break
- for child: Node in row.find_children("*", "VBoxContainer", true, false):
- var candidate := child as VBoxContainer
- if candidate != null and candidate.tooltip_text == (
- "1200 fish coins · 3400 xp"
- ):
- reward = candidate
- break
- assert(progress_bar != null)
- assert(progress_bar.tooltip_text == "2400 / 2500")
- assert(progress_bar.mouse_filter == Control.MOUSE_FILTER_STOP)
- assert(progress_count != null)
- assert(progress_count.tooltip_text == "2400 / 2500")
- assert(progress_count.mouse_filter == Control.MOUSE_FILTER_STOP)
- assert(reward != null)
- assert(reward.mouse_filter == Control.MOUSE_FILTER_STOP)
- row.queue_free()
- await process_frame
-
-
-func _validate_creature_jobs(jobs: PlayerJobService) -> void:
- var fish_count: int = 0
- var insect_count: int = 0
- var shellfish_count: int = 0
- var candidates: Array[FishData] = []
- for fish: FishData in Catalog.candidates:
- if fish == null or not fish.is_selectable():
- continue
- candidates.append(fish)
- match fish.get_creature_group():
- FishData.CreatureGroup.FISH:
- fish_count += 1
- FishData.CreatureGroup.INSECT:
- insect_count += 1
- FishData.CreatureGroup.SHELLFISH:
- shellfish_count += 1
- assert(fish_count > 0 and insect_count > 0 and shellfish_count > 0)
-
- var lifetime: Array[Dictionary] = jobs.get_lifetime_jobs()
- var fish_discovery: Dictionary = _find_job(
- lifetime, JobCatalog.Kind.DISCOVER_SPECIES
- )
- assert(int(fish_discovery.get("target", -1)) == fish_count)
- for creature_group: int in [
- FishData.CreatureGroup.INSECT,
- FishData.CreatureGroup.SHELLFISH,
- ]:
- assert(not _find_creature_job(
- lifetime,
- JobCatalog.Kind.CATCH_CREATURE_GROUP,
- creature_group,
- ).is_empty())
- assert(not _find_creature_job(
- lifetime,
- JobCatalog.Kind.DISCOVER_CREATURE_GROUP,
- creature_group,
- ).is_empty())
- assert(not _find_creature_job(
- lifetime,
- JobCatalog.Kind.MASTER_CREATURE_GROUP,
- creature_group,
- ).is_empty())
-
- var daily_groups: Dictionary[int, bool] = {}
- for seed_index: int in 64:
- for job: Dictionary in JobCatalog.generate_daily_jobs(
- "creature-job-validation-%d" % seed_index,
- candidates,
- true,
- ):
- if int(job.get("kind", -1)) == JobCatalog.Kind.CATCH_CREATURE_GROUP:
- daily_groups[int(job.get("creature_group", -1))] = true
- assert(daily_groups.has(FishData.CreatureGroup.INSECT))
- assert(daily_groups.has(FishData.CreatureGroup.SHELLFISH))
-
- var beetle: FishData = Catalog.get_fish_by_id(&"beetle_stag_common")
- assert(beetle != null)
- var beetle_catch := _make_test_catch(beetle, &"job-beetle")
- jobs.call("_on_authoritative_catch", beetle_catch)
- var statistics: Dictionary = jobs.to_save_data().get("statistics", {})
- assert(int(statistics.get("fish_caught", -1)) == 0)
- assert(int(statistics.get("insects_caught", -1)) == 1)
-
-
-func _find_creature_job(
- jobs: Array[Dictionary],
- kind: JobCatalog.Kind,
- creature_group: int,
-) -> Dictionary:
- for job: Dictionary in jobs:
- if (
- int(job.get("kind", -1)) == kind
- and int(job.get("creature_group", -1)) == creature_group
- ):
- return job
- return {}
-
-
-func _make_test_catch(fish: FishData, catch_id: StringName) -> FishCatch:
- var fish_catch := FishCatch.new()
- fish_catch.fish = fish
- fish_catch.fish_id = fish.id
- fish_catch.catch_id = catch_id
- fish_catch.catch_sequence = 1
- fish_catch.weight_lb = fish.get_minimum_weight()
- fish_catch.display_scale = fish.get_display_scale_for_weight(
- fish_catch.weight_lb
- )
- fish_catch.quality = FishQuality.Tier.BORING
- fish_catch.sale_value = fish.get_sale_value_for_weight(fish_catch.weight_lb)
- assert(fish_catch.is_valid())
- return fish_catch
-
-
-func _validate_weather_guarantees(board: Dictionary) -> void:
- var jobs: Array = board.get("jobs", [])
- var schedule: Array = board.get("weather_schedule", [])
- var anchor_index: int = int(board.get("schedule_anchor_index", 0))
- for required_weather: int in JobCatalog.weather_requirements(jobs):
- var occurrences: int = 0
- var has_later_window: bool = false
- for index: int in schedule.size():
- var entry: Dictionary = schedule[index]
- if int(entry.get("weather", -1)) != required_weather:
- continue
- occurrences += 1
- if index > anchor_index:
- has_later_window = true
- assert(occurrences >= 2)
- assert(has_later_window)
-
-
-func _validate_late_board_weather_fallback() -> void:
- var late_jobs: Array[Dictionary] = JobCatalog.generate_daily_jobs(
- "late-board-validation", [], false
- )
- assert(JobCatalog.weather_requirements(late_jobs).is_empty())
- var anchored_jobs: Array[Dictionary] = JobCatalog.generate_daily_jobs(
- "anchored-board-validation", [], true
- )
- var anchored_schedule: Array[Dictionary] = (
- JobCatalog.generate_weather_schedule(
- "anchored-board-validation", anchored_jobs, 9
- )
- )
- for required_weather: int in JobCatalog.weather_requirements(anchored_jobs):
- var occurrence_indices: Array[int] = []
- for index: int in anchored_schedule.size():
- var entry: Dictionary = anchored_schedule[index]
- if (
- index >= 9
- and int(entry.get("weather", -1)) == required_weather
- ):
- occurrence_indices.append(index)
- assert(occurrence_indices.size() >= 2)
- assert(occurrence_indices[0] >= 9)
- assert(occurrence_indices[-1] > 9)
-
-
-func _validate_remote_tamper_rejection(
- jobs: PlayerJobService,
- board: Dictionary,
-) -> void:
- var tampered: Dictionary = board.duplicate(true)
- var tampered_jobs: Array = tampered.get("jobs", [])
- var first_job: Dictionary = tampered_jobs[0]
- first_job["fish_coin"] = int(first_job.get("fish_coin", 0)) + 1
- tampered_jobs[0] = first_job
- tampered["jobs"] = tampered_jobs
- assert(PlayerJobService.validate_board(tampered))
- assert(not jobs.apply_remote_board(tampered))
-
-
-func _find_job(jobs: Array[Dictionary], kind: JobCatalog.Kind) -> Dictionary:
- for job: Dictionary in jobs:
- if int(job.get("kind", -1)) == int(kind):
- return job
- return {}
diff --git a/tests/job_system_validation.gd.uid b/tests/job_system_validation.gd.uid
deleted file mode 100644
index d37784b..0000000
--- a/tests/job_system_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://b2grposd30lkw
diff --git a/tests/keyboard_mouse_mapping_validation.gd b/tests/keyboard_mouse_mapping_validation.gd
deleted file mode 100644
index 1b95e01..0000000
--- a/tests/keyboard_mouse_mapping_validation.gd
+++ /dev/null
@@ -1,274 +0,0 @@
-extends SceneTree
-
-const KeyboardMouseMappingManagerType = preload(
- "res://settings/keyboard_mouse_mapping_manager.gd"
-)
-const KeyboardMouseMappingPanelType = preload(
- "res://ui/keyboard_mouse_mapping_panel.gd"
-)
-const SettingsPanelScene = preload("res://ui/settings_panel.tscn")
-const FishingSpotScene = preload("res://fishing/fishing_spot.tscn")
-const FishingSpotType = preload("res://fishing/fishing_spot.gd")
-const CatchDifficultyProfileType = preload(
- "res://fishing/catch_difficulty_profile.gd"
-)
-const PlayerType = preload("res://player/player.gd")
-
-
-func _initialize() -> void:
- _run.call_deferred()
-
-
-func _run() -> void:
- var manager := KeyboardMouseMappingManagerType.new()
- root.add_child(manager)
- await process_frame
- var defaults: Dictionary = manager.get_active_bindings()
- assert(
- defaults.size()
- == KeyboardMouseMappingManagerType.ROLE_ORDER.size()
- )
- assert(
- not manager.get_binding(
- KeyboardMouseMappingManagerType.ROLE_INTERACT
- ).is_empty()
- )
- assert(
- manager.get_binding_label(
- KeyboardMouseMappingManagerType.ROLE_INTERACT
- ) == "e"
- )
- assert(
- KeyboardMouseMappingManagerType.ROLE_LABELS[
- KeyboardMouseMappingManagerType.ROLE_CAMERA_ZOOM_IN
- ] == "hotbar select (+SHIFT to zoom)"
- )
- assert(
- KeyboardMouseMappingManagerType.ROLE_LABELS[
- KeyboardMouseMappingManagerType.ROLE_CAMERA_ZOOM_OUT
- ] == "hotbar select (+SHIFT to zoom)"
- )
- assert(
- str(defaults[
- str(KeyboardMouseMappingManagerType.ROLE_PRIMARY_ACTION)
- ].get("kind", "")) == "mouse_button"
- )
- assert(
- str(defaults[
- str(KeyboardMouseMappingManagerType.ROLE_ALTERNATE_REEL)
- ].get("kind", "")) == "key"
- )
- assert(_has_physical_key(&"reel_alternate", KEY_QUOTELEFT))
- assert(_event_count(&"reel_alternate", true) == 0)
- assert(
- str(defaults[
- str(KeyboardMouseMappingManagerType.ROLE_CHAT)
- ].get("kind", "")) == "key"
- )
-
- var joypad_events_before: int = _event_count(&"jump", true)
- var rebind_key := InputEventKey.new()
- rebind_key.physical_keycode = KEY_R
- rebind_key.pressed = true
- var jump_binding: Dictionary = manager.binding_from_event(rebind_key)
- assert(manager.validate_binding(jump_binding))
- assert(manager.set_binding(
- KeyboardMouseMappingManagerType.ROLE_JUMP,
- jump_binding,
- ))
- assert(manager.has_custom_mapping())
- assert(FileAccess.file_exists(
- KeyboardMouseMappingManagerType.MAPPING_PATH
- ))
- assert(_has_physical_key(&"jump", KEY_R))
- assert(_event_count(&"jump", true) == joypad_events_before)
-
- var mouse_event := InputEventMouseButton.new()
- mouse_event.button_index = MOUSE_BUTTON_XBUTTON1
- mouse_event.pressed = true
- var mouse_binding: Dictionary = manager.binding_from_event(mouse_event)
- assert(manager.set_binding(
- KeyboardMouseMappingManagerType.ROLE_PRIMARY_ACTION,
- mouse_binding,
- ))
- assert(_has_mouse_button(&"fish_primary", MOUSE_BUTTON_XBUTTON1))
- assert(not _has_mouse_button(&"fish_primary", MOUSE_BUTTON_LEFT))
- var alternate_reel_key := InputEventKey.new()
- alternate_reel_key.physical_keycode = KEY_G
- alternate_reel_key.pressed = true
- assert(manager.set_binding(
- KeyboardMouseMappingManagerType.ROLE_ALTERNATE_REEL,
- manager.binding_from_event(alternate_reel_key),
- ))
- assert(_has_physical_key(&"reel_alternate", KEY_G))
- assert(_event_count(&"reel_alternate", true) == 0)
-
- var joypad_event := InputEventJoypadButton.new()
- joypad_event.button_index = JOY_BUTTON_A
- joypad_event.pressed = true
- assert(manager.binding_from_event(joypad_event).is_empty())
-
- var panel := KeyboardMouseMappingPanelType.new()
- root.add_child(panel)
- panel.setup(manager)
- panel.open_panel()
- await process_frame
- assert(_has_label_text(panel, "keyboard binds"))
- var scrolls: Array[Node] = panel.find_children(
- "", "ScrollContainer", true, false
- )
- assert(not scrolls.is_empty())
- assert((scrolls.front() as ScrollContainer).follow_focus)
- var first_binding := panel._binding_buttons.get(
- KeyboardMouseMappingManagerType.ROLE_ORDER.front()
- ) as Button
- var last_binding := panel._binding_buttons.get(
- KeyboardMouseMappingManagerType.ROLE_ORDER.back()
- ) as Button
- assert(first_binding.get_node(
- first_binding.focus_neighbor_top
- ) == first_binding)
- assert(last_binding.get_node(
- last_binding.focus_neighbor_bottom
- ) == panel._close_button)
- assert(panel._close_button.get_node(
- panel._close_button.focus_neighbor_top
- ) == last_binding)
- panel._begin_capture(KeyboardMouseMappingManagerType.ROLE_INTERACT)
- var interact_key := InputEventKey.new()
- interact_key.physical_keycode = KEY_F
- interact_key.pressed = true
- panel._input(interact_key)
- assert(_has_physical_key(&"interact", KEY_F))
- assert(
- manager.get_binding_label(
- KeyboardMouseMappingManagerType.ROLE_INTERACT
- ) == "f"
- )
- assert(not panel.is_capturing())
-
- var settings_panel := SettingsPanelScene.instantiate() as SettingsPanel
- root.add_child(settings_panel)
- await process_frame
- var controller_bubble := settings_panel.get_node(
- "%ControllerMapping"
- ) as Button
- var keyboard_bubble := settings_panel.get_node("%KeyboardMapping") as Button
- assert(controller_bubble.text == "controller binds")
- assert(keyboard_bubble.text == "keyboard binds")
-
- assert(manager.reset_mapping())
- assert(not manager.has_custom_mapping())
- assert(not FileAccess.file_exists(
- KeyboardMouseMappingManagerType.MAPPING_PATH
- ))
- assert(_has_physical_key(&"jump", KEY_SPACE))
- assert(_has_mouse_button(&"fish_primary", MOUSE_BUTTON_LEFT))
- assert(_has_physical_key(&"reel_alternate", KEY_QUOTELEFT))
- assert(_event_count(&"reel_alternate", true) == 0)
- assert(_has_physical_key(&"open_chat", KEY_T))
- assert(_event_count(&"jump", true) == joypad_events_before)
-
- var legacy_bindings: Dictionary = manager.get_active_bindings()
- legacy_bindings.erase(str(
- KeyboardMouseMappingManagerType.ROLE_ALTERNATE_REEL
- ))
- legacy_bindings[str(KeyboardMouseMappingManagerType.ROLE_JUMP)] = (
- manager.binding_from_event(rebind_key)
- )
- var legacy_file := FileAccess.open(
- KeyboardMouseMappingManagerType.MAPPING_PATH,
- FileAccess.WRITE,
- )
- assert(legacy_file != null)
- legacy_file.store_string(JSON.stringify({
- "format_version": KeyboardMouseMappingManagerType.LEGACY_FORMAT_VERSION,
- "bindings": legacy_bindings,
- }))
- legacy_file.close()
- var migrated_manager := KeyboardMouseMappingManagerType.new()
- root.add_child(migrated_manager)
- await process_frame
- assert(migrated_manager.has_custom_mapping())
- assert(_has_physical_key(&"jump", KEY_R))
- assert(_has_physical_key(&"reel_alternate", KEY_QUOTELEFT))
- assert(migrated_manager.reset_mapping())
-
- var fishing_spot := FishingSpotScene.instantiate() as FishingSpotType
- root.add_child(fishing_spot)
- await process_frame
- var local_player := PlayerType.new()
- fishing_spot.set("_local_player", local_player)
- fishing_spot.set("_gameplay_input_enabled", true)
- fishing_spot.state = FishingSpotType.FishingState.FIGHTING
- var catch_controller := fishing_spot.get_node("%CatchController")
- var catch_profile := CatchDifficultyProfileType.new()
- catch_profile.barrier_count_min = 0
- catch_profile.barrier_count_max = 0
- catch_controller.start_encounter(catch_profile, 1.0, 1)
- var alternate_echo := InputEventKey.new()
- alternate_echo.physical_keycode = KEY_QUOTELEFT
- alternate_echo.pressed = true
- alternate_echo.echo = true
- assert(alternate_echo.is_action(&"reel_alternate"))
- fishing_spot._unhandled_input(alternate_echo)
- assert(not bool(catch_controller.get("_reel_input_held")))
- var alternate_key_press := InputEventKey.new()
- alternate_key_press.physical_keycode = KEY_QUOTELEFT
- alternate_key_press.pressed = true
- assert(alternate_key_press.is_action(&"reel_alternate"))
- fishing_spot._unhandled_input(alternate_key_press)
- assert(bool(catch_controller.get("_reel_input_held")))
- catch_controller.set_reel_input(false)
- var alternate_press := InputEventAction.new()
- alternate_press.action = &"reel_alternate"
- alternate_press.pressed = true
- fishing_spot._unhandled_input(alternate_press)
- assert(bool(catch_controller.get("_reel_input_held")))
- var alternate_release := InputEventAction.new()
- alternate_release.action = &"reel_alternate"
- alternate_release.pressed = false
- fishing_spot._unhandled_input(alternate_release)
- assert(not bool(catch_controller.get("_reel_input_held")))
- fishing_spot.free()
- local_player.free()
-
- settings_panel.queue_free()
- panel.queue_free()
- migrated_manager.queue_free()
- manager.queue_free()
- print("Keyboard and mouse mapping validation: PASS")
- quit(0)
-
-
-func _event_count(action: StringName, joypad: bool) -> int:
- var result: int = 0
- for event: InputEvent in InputMap.action_get_events(action):
- if joypad == (
- event is InputEventJoypadButton or event is InputEventJoypadMotion
- ):
- result += 1
- return result
-
-
-func _has_physical_key(action: StringName, key: Key) -> bool:
- for event: InputEvent in InputMap.action_get_events(action):
- var key_event := event as InputEventKey
- if key_event != null and key_event.physical_keycode == key:
- return true
- return false
-
-
-func _has_mouse_button(action: StringName, button: MouseButton) -> bool:
- for event: InputEvent in InputMap.action_get_events(action):
- var mouse_event := event as InputEventMouseButton
- if mouse_event != null and mouse_event.button_index == button:
- return true
- return false
-
-
-func _has_label_text(parent: Node, expected: String) -> bool:
- for node: Node in parent.find_children("", "Label", true, false):
- if (node as Label).text == expected:
- return true
- return false
diff --git a/tests/keyboard_mouse_mapping_validation.gd.uid b/tests/keyboard_mouse_mapping_validation.gd.uid
deleted file mode 100644
index 4d21769..0000000
--- a/tests/keyboard_mouse_mapping_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://br6aeelrpa64k
diff --git a/tests/late_join_multiplayer_validation.gd b/tests/late_join_multiplayer_validation.gd
deleted file mode 100644
index 6ea544f..0000000
--- a/tests/late_join_multiplayer_validation.gd
+++ /dev/null
@@ -1,369 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-const TEST_PORT: int = 18194
-const LATE_MESSAGE: String = "late join visibility check"
-const CLEAR_MESSAGE: String = "late join animation clear check"
-const ACTIVE_ACTION: StringName = &"draw"
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var arguments: PackedStringArray = OS.get_cmdline_user_args()
- if arguments.has("host"):
- await _run_host()
- elif arguments.has("first"):
- await _run_first_client()
- elif arguments.has("late"):
- await _run_late_client()
- else:
- push_error("Late-join validation needs host, first, or late mode.")
- quit(1)
-
-
-func _run_host() -> void:
- var main: Node = await _create_initialized_main()
- var session := main.get_node("%NetworkSession") as NetworkSession
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(bool(main.call(
- "_apply_world",
- WorldLayout.STARTER_ISLAND,
- NetworkProtocol.DEFAULT_WORLD_SEED,
- true,
- )))
- assert(session.set_host_world(
- WorldLayout.STARTER_ISLAND,
- NetworkProtocol.DEFAULT_WORLD_SEED,
- ))
- assert(session.start_private_host(TEST_PORT))
- assert(save_manager.initialize_new_game(
- NetworkProtocol.DEFAULT_WORLD_SEED,
- WorldLayout.STARTER_ISLAND,
- ))
- main.call("_enter_gameplay")
- for _frame: int in 4:
- await physics_frame
- assert(session.set_host_open(true))
- assert(await _wait_for_registry_size(session, 3))
- var spawn := main.get_node("%PlayerSpawnService") as PlayerSpawnService
- assert(spawn.get_peer_ids().size() == 3)
- var action_peer_id: int = await _wait_for_remote_action(
- spawn,
- ACTIVE_ACTION,
- )
- assert(action_peer_id > 1)
- assert(await _wait_for_avatar_bool(
- spawn,
- action_peer_id,
- &"_active_item_is_net",
- true,
- ))
- assert(await _wait_for_avatar_sitting(spawn, action_peer_id, true))
- assert(await _wait_for_avatar_action(
- spawn,
- action_peer_id,
- &"",
- ))
- assert(await _wait_for_avatar_sitting(spawn, action_peer_id, false))
- var chat := main.get_node("%NetworkChatService") as NetworkChatService
- assert(await _wait_for_history_message(chat, CLEAR_MESSAGE))
- print("Late-join multiplayer host validation: PASS")
- await _cleanup(main, session)
-
-
-func _run_first_client() -> void:
- var main: Node = await _create_initialized_main()
- var session := await _join(main)
- var profile := main.get_node("%NetworkProfileService") as NetworkProfileService
- var preview: Dictionary = profile.get_persisted_appearance().duplicate(true)
- preview["scale"] = 0.9
- assert(profile.preview_appearance(preview))
- var player := main.get("_player") as Player
- assert(player != null)
- var floor_deadline: int = Time.get_ticks_msec() + 5000
- while Time.get_ticks_msec() < floor_deadline and not player.is_on_floor():
- await physics_frame
- assert(player.is_on_floor())
- player.toggle_sitting()
- assert(player.is_sitting())
- assert(player.bag.add_item(&"crab_net"))
- # Slot zero begins selected with the starter rod. Replacing that slot does
- # not emit a selection change, so use the next slot to exercise the same
- # equipped-item path a player uses in normal gameplay.
- assert(player.hotbar.assign_item(1, &"crab_net"))
- assert(player.hotbar.select_slot(1))
- assert(await _wait_for_local_net(main, player))
- assert(player.begin_animation_action(ACTIVE_ACTION))
- await create_timer(0.5).timeout
- assert(StringName(str(player.get("_animation_action_id"))) == ACTIVE_ACTION)
- var chat := main.get_node("%NetworkChatService") as NetworkChatService
- var deadline: int = Time.get_ticks_msec() + 15000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- if (
- session.get_authenticated_peer_ids().size() == 3
- and _history_contains(chat, LATE_MESSAGE)
- ):
- break
- assert(session.get_authenticated_peer_ids().size() == 3)
- assert(_history_contains(chat, LATE_MESSAGE))
- var spawn := main.get_node("%PlayerSpawnService") as PlayerSpawnService
- assert(spawn.get_peer_ids().size() == 3)
- # Keep the action active until the late peer has joined and announced that
- # it has received the lifecycle snapshot. This exercises client -> host ->
- # existing client/late client replication, including the join-in-progress
- # state carried by the spawn envelope.
- await create_timer(1.0).timeout
- player.end_animation_action()
- player.toggle_sitting()
- assert(not player.is_sitting())
- assert(await _wait_for_history_message(chat, CLEAR_MESSAGE))
- print("Late-join first-client validation: PASS")
- await _cleanup(main, session)
-
-
-func _run_late_client() -> void:
- var main: Node = await _create_initialized_main()
- var session := await _join(main)
- var deadline: int = Time.get_ticks_msec() + 8000
- while (
- Time.get_ticks_msec() < deadline
- and session.get_authenticated_peer_ids().size() < 3
- ):
- await process_frame
- assert(session.get_authenticated_peer_ids().size() == 3)
- var spawn := main.get_node("%PlayerSpawnService") as PlayerSpawnService
- assert(spawn.get_peer_ids().size() == 3)
- var action_peer_id: int = await _wait_for_remote_action(
- spawn,
- ACTIVE_ACTION,
- )
- assert(action_peer_id > 1)
- assert(await _wait_for_avatar_bool(
- spawn,
- action_peer_id,
- &"_active_item_is_net",
- true,
- ))
- assert(await _wait_for_avatar_sitting(spawn, action_peer_id, true))
- var chat := main.get_node("%NetworkChatService") as NetworkChatService
- assert(chat.send_local_message(LATE_MESSAGE))
- assert(await _wait_for_avatar_action(
- spawn,
- action_peer_id,
- &"",
- ))
- assert(await _wait_for_avatar_sitting(spawn, action_peer_id, false))
- assert(chat.send_local_message(CLEAR_MESSAGE))
- await create_timer(0.5).timeout
- print("Late-join late-client validation: PASS")
- await _cleanup(main, session)
-
-
-func _join(main: Node) -> NetworkSession:
- main.call("_on_title_join_game_requested", "127.0.0.1:%d" % TEST_PORT)
- var session := main.get_node("%NetworkSession") as NetworkSession
- var deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
- main.call("_confirm_server_trust")
- if session.is_joined_client() and bool(main.get("_gameplay_started")):
- return session
- assert(false, "Client did not join in time.")
- return session
-
-
-func _wait_for_registry_size(session: NetworkSession, expected: int) -> bool:
- var deadline: int = Time.get_ticks_msec() + 25000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- if session.get_authenticated_peer_ids().size() == expected:
- return true
- return false
-
-
-func _wait_for_local_net(main: Node, player: Player) -> bool:
- var deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < deadline:
- # A joined client can finish its world transition one frame after the
- # hotbar selection signal. Refresh through the production path until the
- # ready fishing state accepts the equipped item.
- main.call("_refresh_active_hotbar_item")
- await process_frame
- if bool(player.get("_active_item_is_net")):
- return true
- var fishing_spot: Node = main.get_node("%FishingSpot")
- print(
- "net equip wait timed out: ",
- {
- "fishing_state": int(fishing_spot.get("state")),
- "selected_slot": player.hotbar.get_selected_slot(),
- "selected_item": String(player.hotbar.get_selected_item_id()),
- "owns_net": player.bag.owns_item(&"crab_net"),
- },
- )
- return false
-
-
-func _history_contains(service: NetworkChatService, body: String) -> bool:
- return service.get_history().any(
- func(message: Dictionary) -> bool:
- return str(message.get("body", "")) == body
- )
-
-
-func _wait_for_history_message(
- service: NetworkChatService,
- body: String,
-) -> bool:
- var deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- if _history_contains(service, body):
- return true
- return false
-
-
-func _wait_for_remote_action(
- spawn_service: PlayerSpawnService,
- action_id: StringName,
-) -> int:
- var deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- for peer_id: int in spawn_service.get_peer_ids():
- var avatar: Player = spawn_service.get_avatar(peer_id)
- if (
- avatar != null
- and _observed_action_id(avatar) == action_id
- ):
- return peer_id
- print(
- "animation wait timed out: ",
- _action_debug_snapshot(spawn_service),
- )
- return 0
-
-
-func _wait_for_avatar_action(
- spawn_service: PlayerSpawnService,
- peer_id: int,
- action_id: StringName,
-) -> bool:
- var deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- var avatar: Player = spawn_service.get_avatar(peer_id)
- if (
- avatar != null
- and _observed_action_id(avatar) == action_id
- ):
- return true
- print(
- "animation clear wait timed out: ",
- _action_debug_snapshot(spawn_service),
- )
- return false
-
-
-func _wait_for_avatar_bool(
- spawn_service: PlayerSpawnService,
- peer_id: int,
- property_name: StringName,
- expected: bool,
-) -> bool:
- var deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- var avatar: Player = spawn_service.get_avatar(peer_id)
- if avatar != null and bool(avatar.get(property_name)) == expected:
- return true
- return false
-
-
-func _wait_for_avatar_sitting(
- spawn_service: PlayerSpawnService,
- peer_id: int,
- expected: bool,
-) -> bool:
- var deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- var avatar: Player = spawn_service.get_avatar(peer_id)
- if avatar != null and avatar.is_sitting() == expected:
- return true
- var avatar: Player = spawn_service.get_avatar(peer_id)
- if avatar != null:
- print(
- "sitting wait timed out: ",
- {
- "peer_id": peer_id,
- "expected": expected,
- "sitting": avatar.is_sitting(),
- "sit_after_landing": bool(avatar.get("_sit_after_landing")),
- "on_floor": avatar.is_on_floor(),
- "position": avatar.global_position,
- "velocity": avatar.velocity,
- "authoritative": bool(avatar.get(
- "_network_authoritative_simulation"
- )),
- "last_input": int(avatar.get(
- "_last_network_input_sequence"
- )),
- },
- )
- return false
-
-
-func _observed_action_id(avatar: Player) -> StringName:
- return StringName(str(avatar.get(
- "_animation_action_id"
- if bool(avatar.get("_network_authoritative_simulation"))
- else "_network_target_animation_action_id"
- )))
-
-
-func _action_debug_snapshot(spawn_service: PlayerSpawnService) -> Dictionary:
- var result: Dictionary = {}
- for peer_id: int in spawn_service.get_peer_ids():
- var avatar: Player = spawn_service.get_avatar(peer_id)
- if avatar == null:
- continue
- result[peer_id] = {
- "authoritative": bool(avatar.get(
- "_network_authoritative_simulation"
- )),
- "source": str(avatar.get("_animation_action_id")),
- "target": str(avatar.get(
- "_network_target_animation_action_id"
- )),
- "last_input": int(avatar.get("_last_network_input_sequence")),
- }
- return result
-
-
-func _create_initialized_main() -> Node:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- return main
-
-
-func _cleanup(main: Node, session: NetworkSession) -> void:
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
diff --git a/tests/late_join_multiplayer_validation.gd.uid b/tests/late_join_multiplayer_validation.gd.uid
deleted file mode 100644
index 5d33548..0000000
--- a/tests/late_join_multiplayer_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dnuwxkoeeykt3
diff --git a/tests/light_performance_profile_validation.gd b/tests/light_performance_profile_validation.gd
deleted file mode 100644
index 6a654f1..0000000
--- a/tests/light_performance_profile_validation.gd
+++ /dev/null
@@ -1,293 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-const RuntimePerformanceProfileType = preload(
- "res://main/runtime_performance_profile.gd"
-)
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- _validate_profile_resolution()
- OS.set_environment(
- RuntimePerformanceProfileType.PROFILE_ENVIRONMENT_VARIABLE,
- "light",
- )
- root.size = Vector2i(640, 480)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- assert(bool(main.call(
- "_apply_world",
- WorldLayout.GENERATED,
- PlayerSaveManager.DEFAULT_WORLD_SEED,
- true,
- )))
- _validate_main_profile(main)
- _validate_minimal_weather(main)
- _stop_audio_players(main)
- main.queue_free()
- for _frame: int in 8:
- await process_frame
- OS.unset_environment(
- RuntimePerformanceProfileType.PROFILE_ENVIRONMENT_VARIABLE
- )
- var normal_main: Node = MainScene.instantiate()
- root.add_child(normal_main)
- for _frame: int in 4:
- await process_frame
- if not bool(normal_main.get("_application_initialized")):
- normal_main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(normal_main.get("_application_initialized")))
- assert(bool(normal_main.call(
- "_apply_world",
- WorldLayout.GENERATED,
- PlayerSaveManager.DEFAULT_WORLD_SEED,
- true,
- )))
- _validate_normal_profile(normal_main)
- _validate_new_game_music_transition(normal_main)
- _stop_audio_players(normal_main)
- normal_main.queue_free()
- for _frame: int in 8:
- await process_frame
- print("Light performance profile validation: PASS")
- quit()
-
-
-func _validate_profile_resolution() -> void:
- var normal := RuntimePerformanceProfileType.from_name(&"normal")
- var light := RuntimePerformanceProfileType.from_name(&"light")
- var legacy := RuntimePerformanceProfileType.from_name(&"", true)
- assert(not normal.is_light())
- assert(is_equal_approx(normal.get_world_render_scale(), 1.0))
- assert(light.is_light())
- assert(is_equal_approx(light.get_world_render_scale(), 0.375))
- assert(legacy.is_light())
-
-
-func _validate_main_profile(main: Node) -> void:
- var profile: RuntimePerformanceProfile = main.get("_performance_profile")
- assert(profile != null and profile.is_light())
- assert(is_equal_approx(root.scaling_3d_scale, 0.375))
- var pixelation: Node = main.get_node("%WorldPixelationPostprocess")
- assert(bool(pixelation.call("is_light_performance_profile")))
- pixelation.call("set_gameplay_active", true)
- var screen_grid := pixelation.get_node("ScreenGrid") as ColorRect
- assert(screen_grid != null and not screen_grid.visible)
-
- var pond := _first_fresh_water_visual(main)
- var ocean := main.get_node(
- "TestWorld/Regions/GeneratedWorldRegion/WaterBodies/OceanWater/VisualWater"
- ) as MeshInstance3D
- assert(not pond.visible)
- assert(ocean.material_override is StandardMaterial3D)
- assert(not ocean.material_override is ShaderMaterial)
- _validate_ocean_fishing_coverage(main, ocean)
- var region := main.get_node(
- "TestWorld/Regions/GeneratedWorldRegion"
- ) as GeneratedWorldRegion
- assert(region != null)
- assert(region.get_node("Decorations").get_child_count() > 0)
-
- var title_background := main.get_node("%TitleBackground") as ColorRect
- var title_material := title_background.material as ShaderMaterial
- assert(title_material != null)
- assert(not bool(title_material.get_shader_parameter(
- "animation_enabled"
- )))
- for backdrop_name: StringName in [
- &"PlayerMenuBackdrop",
- &"ShopBackdrop",
- ]:
- var backdrop := main.get_node("%%%s" % backdrop_name) as ColorRect
- var backdrop_material := backdrop.material as ShaderMaterial
- assert(backdrop_material != null)
- assert(not bool(backdrop_material.get_shader_parameter(
- "animation_enabled"
- )))
- assert(
- backdrop_material.get_shader_parameter(
- "scroll_velocity_pixels"
- ) == Vector2.ZERO
- )
- var game_ui := main.get_node("%GameUI") as GameUI
- var title_screen := game_ui.get_title_screen()
- assert(title_screen != null)
- var player_menu := game_ui.get("_player_menu") as PlayerMenu
- assert(player_menu != null)
- assert(not player_menu.is_cooler_water_effect_enabled())
- assert((main.get_node("%TitleMusic") as AudioStreamPlayer).stream != null)
- assert((main.get_node("%NewGameMusic") as AudioStreamPlayer).stream != null)
- assert((main.get_node("%DuskMusic") as AudioStreamPlayer).stream != null)
- assert(
- (main.get_node("%WavesAudio") as AudioStreamPlayer).stream == null
- )
- assert(
- (main.get_node("RainAmbience") as AudioStreamPlayer).stream == null
- )
-
-
-func _validate_normal_profile(main: Node) -> void:
- var profile: RuntimePerformanceProfile = main.get("_performance_profile")
- assert(profile != null and not profile.is_light())
- assert(is_equal_approx(root.scaling_3d_scale, 1.0))
- var pixelation: Node = main.get_node("%WorldPixelationPostprocess")
- assert(not bool(pixelation.call("is_light_performance_profile")))
- var pond := _first_fresh_water_visual(main)
- var ocean := main.get_node(
- "TestWorld/Regions/GeneratedWorldRegion/WaterBodies/OceanWater/VisualWater"
- ) as MeshInstance3D
- assert(not pond.visible)
- assert(ocean.material_override is ShaderMaterial)
- _validate_ocean_fishing_coverage(main, ocean)
- var region := main.get_node(
- "TestWorld/Regions/GeneratedWorldRegion"
- ) as GeneratedWorldRegion
- assert(region != null)
- assert(region.get_node("Decorations").get_child_count() > 0)
- var game_ui := main.get_node("%GameUI") as GameUI
- var title_screen := game_ui.get_title_screen()
- assert(title_screen != null)
- var player_menu := game_ui.get("_player_menu") as PlayerMenu
- assert(player_menu != null)
- assert(player_menu.is_cooler_water_effect_enabled())
-
- var title_background := main.get_node("%TitleBackground") as ColorRect
- var title_material := title_background.material as ShaderMaterial
- assert(title_material != null)
- assert(bool(title_material.get_shader_parameter("animation_enabled")))
- for backdrop_name: StringName in [
- &"PlayerMenuBackdrop",
- &"ShopBackdrop",
- ]:
- var backdrop := main.get_node("%%%s" % backdrop_name) as ColorRect
- var backdrop_material := backdrop.material as ShaderMaterial
- assert(backdrop_material != null)
- assert(bool(backdrop_material.get_shader_parameter(
- "animation_enabled"
- )))
- assert(
- backdrop_material.get_shader_parameter(
- "scroll_velocity_pixels"
- ) != Vector2.ZERO
- )
- var visuals: WorldTimeVisualController = main.get_node(
- "%WorldTimeVisualController"
- )
- var rain := visuals.get_node("LocalRain") as GPUParticles3D
- assert(rain.amount == WorldTimeVisualController.RAIN_PARTICLE_AMOUNT)
- assert(rain.fixed_fps == 30)
- var clouds := visuals.get_node(
- "LocalStormClouds"
- ) as LocalStormCloudLayer
- assert(clouds.get_patch_count() == 81)
- assert(clouds.get_node_or_null("CloudField") is MultiMeshInstance3D)
- assert(clouds.get_node_or_null("CloudCeiling") == null)
- assert((main.get_node("%TitleMusic") as AudioStreamPlayer).stream != null)
- assert((main.get_node("%NewGameMusic") as AudioStreamPlayer).stream != null)
- assert((main.get_node("%DuskMusic") as AudioStreamPlayer).stream != null)
- assert(
- (main.get_node("%WavesAudio") as AudioStreamPlayer).stream != null
- )
- assert(
- (main.get_node("RainAmbience") as AudioStreamPlayer).stream != null
- )
-
-
-func _validate_new_game_music_transition(main: Node) -> void:
- var title_music := main.get_node("%TitleMusic") as AudioStreamPlayer
- var new_game_music := main.get_node("%NewGameMusic") as AudioStreamPlayer
- assert(title_music.playing)
- main.call("_start_new_game_music")
- assert(not title_music.playing)
- assert(new_game_music.playing)
- main.call("_show_title_music", true)
- assert(not new_game_music.playing)
- assert(title_music.playing)
-
-
-func _first_fresh_water_visual(main: Node) -> MeshInstance3D:
- var root := main.get_node(
- "TestWorld/Regions/GeneratedWorldRegion/WaterBodies/FreshWaterBodies"
- ) as Node3D
- assert(root != null and root.get_child_count() > 0)
- return root.get_child(0).get_node("VisualWater") as MeshInstance3D
-
-
-func _validate_ocean_fishing_coverage(
- main: Node,
- ocean: MeshInstance3D,
-) -> void:
- var ocean_mesh := ocean.mesh as PlaneMesh
- assert(ocean_mesh != null)
- var shape_node := main.get_node(
- "TestWorld/Regions/GeneratedWorldRegion/WaterBodies/OceanWater/"
- + "FishingRegion/Shape"
- ) as CollisionShape3D
- assert(shape_node != null)
- var fishing_shape := shape_node.shape as BoxShape3D
- assert(fishing_shape != null)
- assert(fishing_shape.size.is_equal_approx(Vector3(
- ocean_mesh.size.x,
- 4.0,
- ocean_mesh.size.y,
- )))
-
-
-func _stop_audio_players(root_node: Node) -> void:
- if root_node is AudioStreamPlayer:
- (root_node as AudioStreamPlayer).stop()
- elif root_node is AudioStreamPlayer2D:
- (root_node as AudioStreamPlayer2D).stop()
- elif root_node is AudioStreamPlayer3D:
- (root_node as AudioStreamPlayer3D).stop()
- for child: Node in root_node.get_children():
- _stop_audio_players(child)
-
-
-func _validate_minimal_weather(main: Node) -> void:
- var visuals: WorldTimeVisualController = main.get_node(
- "%WorldTimeVisualController"
- )
- var rain := visuals.get_node("LocalRain") as GPUParticles3D
- assert(rain.amount == WorldTimeVisualController.LIGHT_RAIN_PARTICLE_AMOUNT)
- assert(rain.fixed_fps == WorldTimeVisualController.LIGHT_RAIN_FIXED_FPS)
- assert(rain.amount == 48)
- assert(rain.fixed_fps == 8)
- var clouds := visuals.get_node("LocalStormClouds") as LocalStormCloudLayer
- assert(clouds.get_patch_count() == 1)
- var ceiling := clouds.get_node("CloudCeiling") as MeshInstance3D
- assert(ceiling != null and ceiling.mesh is PlaneMesh)
- assert(ceiling.material_override == null)
- assert((ceiling.mesh as PlaneMesh).material is StandardMaterial3D)
-
- visuals.apply_weather_immediately(WorldWeatherService.Weather.RAINY)
- assert(rain.emitting and rain.amount_ratio > 0.99)
- assert(clouds.visible and clouds.get_storm_amount() > 0.87)
- var cloud_material := (
- (ceiling.mesh as PlaneMesh).material as StandardMaterial3D
- )
- assert(cloud_material.albedo_color.a < 0.65)
- var test_world := main.get_node("TestWorld") as TestWorld
- var runtime_environment := (
- test_world.get_world_environment().environment as Environment
- )
- var sky_material := (
- runtime_environment.sky.sky_material as ShaderMaterial
- )
- assert(is_zero_approx(float(
- sky_material.get_shader_parameter("cloud_coverage")
- )))
- assert(is_zero_approx(float(
- sky_material.get_shader_parameter("cloud_opacity")
- )))
diff --git a/tests/light_performance_profile_validation.gd.uid b/tests/light_performance_profile_validation.gd.uid
deleted file mode 100644
index 3495021..0000000
--- a/tests/light_performance_profile_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c6xqcslinxd1a
diff --git a/tests/logbook_runtime_validation.gd b/tests/logbook_runtime_validation.gd
deleted file mode 100644
index 47e795d..0000000
--- a/tests/logbook_runtime_validation.gd
+++ /dev/null
@@ -1,247 +0,0 @@
-extends SceneTree
-
-const MainScene = preload("res://main/main.tscn")
-const FishCatchType = preload("res://fish/fish_catch.gd")
-const MENU_TRANSITION_SETTLE_SECONDS := 3.0
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var main := MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(save_manager.initialize_new_game())
- main.call("_enter_gameplay")
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_gameplay_started")))
- _validate_save_round_trip(main, save_manager)
- var player := main.get("_player") as Player
- var no_catches: Array[FishCatch] = []
- var no_discoveries: Array[StringName] = []
- assert(player.inventory.replace_all_catches(no_catches, 1))
- assert(player.collection_log.replace_discovered_ids(no_discoveries))
-
- var game_ui := main.get_node("%GameUI") as GameUI
- var player_menu := game_ui.get_node("%PlayerMenu") as PlayerMenu
- var logbook := player_menu.get_node("%CatalogLogbook") as LogbookPage
- var backdrop := main.get_node("%PlayerMenuBackdrop") as ColorRect
- var hotbar := game_ui.get_node("%Hotbar") as Control
- var fishing_spot := main.get("_fishing_spot") as FishingSpot
- assert(logbook.get("_fishing_spot") == fishing_spot)
- assert(logbook.get("_world_time") == main.get("_world_time"))
- assert(logbook.get("_world_weather") == main.get("_world_weather"))
-
- var shortcut := InputEventKey.new()
- shortcut.pressed = true
- shortcut.physical_keycode = KEY_L
- assert(bool(player_menu.call("_handle_direct_page_shortcut", shortcut)))
- await create_timer(MENU_TRANSITION_SETTLE_SECONDS).timeout
- assert(player_menu.visible)
- assert(logbook.visible)
- assert(backdrop.visible)
- assert(not bool(game_ui.get("_player_menu_hotbar_visible")))
- assert(not hotbar.visible)
-
- var entry_buttons: Dictionary = logbook.get("_entry_buttons")
- assert(entry_buttons.size() == 108)
- await _capture_if_requested("-unknown")
- logbook.call("_set_index_open", true, false)
- await process_frame
- assert(bool(logbook.get("_index_open")))
- assert((logbook.get("_index_body") as Control).visible)
- await _capture_if_requested("-index")
- logbook.set_compact_presentation(true)
- await process_frame
- await _capture_if_requested("-index-compact")
- logbook.set_compact_presentation(false)
- await process_frame
- logbook.call("_set_index_open", false, false)
- await process_frame
- logbook.call(
- "_select_category", WaterType.Type.FRESH_WATER
- )
- await create_timer(0.25).timeout
- assert((logbook.get("_entry_buttons") as Dictionary).size() == 108)
- await _capture_if_requested("-fresh")
- logbook.call("_select_category", WaterType.Type.SALT_WATER)
- await create_timer(0.25).timeout
- assert((logbook.get("_entry_buttons") as Dictionary).size() == 202)
- logbook.call("_select_category", WaterType.Type.FRESH_WATER)
- await create_timer(0.25).timeout
- assert((logbook.get("_entry_buttons") as Dictionary).size() == 108)
- player.collection_log.mark_discovered(&"bluegill")
- await process_frame
- logbook.call("_select_entry", &"bluegill", &"bluegill")
- await create_timer(0.2).timeout
- await _capture_if_requested("-known")
- var bluegill := (
- main.get("fish_catalog") as FishPool
- ).get_fish_by_id(&"bluegill")
- var bluegill_available_now := fishing_spot.is_species_available_now(bluegill)
- var index_options := logbook.get("_index_options") as Dictionary
- index_options["availability"] = (
- LogbookCatalog.AvailabilityFilter.AVAILABLE_NOW
- )
- logbook.notify_availability_context_changed()
- await process_frame
- assert(
- (logbook.get("_entry_buttons") as Dictionary).has(&"bluegill")
- == bluegill_available_now
- )
- logbook.call("_reset_index")
- await process_frame
-
- assert(bool(player_menu.call("_handle_direct_page_shortcut", shortcut)))
- await create_timer(MENU_TRANSITION_SETTLE_SECONDS).timeout
- assert(not player_menu.visible)
-
- assert(bool(player_menu.call("_handle_direct_page_shortcut", shortcut)))
- await create_timer(MENU_TRANSITION_SETTLE_SECONDS).timeout
- player_menu.call(
- "_show_section_immediate", PlayerMenu.Section.PROFILE
- )
- await process_frame
- assert(bool(
- player_menu.call("_handle_direct_page_shortcut", shortcut)
- ))
- await create_timer(0.5).timeout
- assert(player_menu.visible)
- assert(logbook.visible)
- assert(
- player_menu.get("_current_section") == PlayerMenu.Section.LOGBOOK
- )
- player_menu.close_menu(PlayerMenu.CloseReason.TEARDOWN, false)
-
- print(
- "Logbook runtime validation: PASS at %dx%d"
- % [root.size.x, root.size.y]
- )
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _validate_save_round_trip(
- main: Node,
- save_manager: PlayerSaveManager,
-) -> void:
- var player := main.get("_player") as Player
- var catalog := main.get("fish_catalog") as FishPool
- assert(catalog != null)
- assert(catalog.candidates.size() == 316)
- var active_species := LogbookCatalog.ordered_species(catalog.candidates)
- assert(active_species.size() == 313)
- # Keep this runtime save fixture within maximum player capacity while the
- # focused catalog validation exercises every active species individually.
- var round_trip_species: Array[FishData] = []
- for fish: FishData in active_species:
- if round_trip_species.size() >= 59:
- break
- round_trip_species.append(fish)
- for fish_id: StringName in [
- &"catfish_blue",
- &"catfish_channel",
- &"catfish_flathead",
- &"catfish_white",
- ]:
- var catfish: FishData = catalog.get_fish_by_id(fish_id)
- if catfish not in round_trip_species:
- round_trip_species.append(catfish)
- assert(round_trip_species.size() == 63)
- player.inventory_layout.restore_backpack_level(
- PlayerInventoryLayout.MAX_BACKPACK_LEVEL
- )
- player.cooler_capacity.restore_level(PlayerCoolerCapacity.MAX_LEVEL)
- assert(
- player.inventory_layout.get_backpack_level()
- == PlayerInventoryLayout.MAX_BACKPACK_LEVEL
- )
- assert(player.cooler_capacity.get_level() == PlayerCoolerCapacity.MAX_LEVEL)
- player.inventory.set_inventory_layout(null)
- for index: int in 4:
- _add_test_catch(player, round_trip_species[index])
- assert(save_manager.save_now())
- var no_catches: Array[FishCatch] = []
- var no_discoveries: Array[StringName] = []
- assert(player.inventory.replace_all_catches(no_catches, 1))
- assert(player.collection_log.replace_discovered_ids(no_discoveries))
- assert(save_manager.load_player_data())
- assert(player.inventory.get_all_catches().size() == 4)
- for index: int in 4:
- var original_fish: FishData = round_trip_species[index]
- assert(player.inventory.get_count(original_fish.id) == 1)
- assert(player.collection_log.has_discovered(original_fish.id))
-
- for index: int in range(4, round_trip_species.size()):
- _add_test_catch(player, round_trip_species[index])
- assert(save_manager.save_now())
- assert(player.inventory.replace_all_catches(no_catches, 1))
- assert(player.collection_log.replace_discovered_ids(no_discoveries))
- assert(save_manager.load_player_data())
- assert(
- player.inventory.get_all_catches().size()
- == round_trip_species.size()
- )
- for fish: FishData in round_trip_species:
- assert(player.inventory.get_count(fish.id) == 1)
- assert(player.collection_log.has_discovered(fish.id))
- for fish_id: StringName in [
- &"catfish_blue",
- &"catfish_channel",
- &"catfish_flathead",
- &"catfish_white",
- ]:
- var fish_catch: FishCatch = (
- player.inventory.get_catches_by_fish_id(fish_id).front()
- )
- player.begin_catch_showcase(fish_catch)
- var catch_sprite := player.find_child(
- "CatchSprite", true, false
- ) as Sprite3D
- assert(catch_sprite != null)
- assert(catch_sprite.texture == fish_catch.fish.display_texture)
- player.end_catch_showcase(Callable(), true)
- player.inventory.set_inventory_layout(player.inventory_layout)
-
-
-func _add_test_catch(player: Player, fish: FishData) -> void:
- var fish_catch := FishCatchType.new()
- fish_catch.fish = fish
- fish_catch.fish_id = fish.id
- fish_catch.weight_lb = fish.get_minimum_weight()
- fish_catch.display_scale = fish.get_display_scale_for_weight(
- fish_catch.weight_lb
- )
- fish_catch.sale_value = fish.get_sale_value_for_weight(
- fish_catch.weight_lb
- )
- fish_catch.ensure_identity()
- player.inventory.add_catch(fish_catch)
- player.collection_log.mark_discovered(fish.id)
-
-
-func _capture_if_requested(suffix: String) -> void:
- if not OS.has_environment("straywild_LOGBOOK_CAPTURE"):
- return
- await process_frame
- await process_frame
- var image: Image = root.get_texture().get_image()
- var path: String = (
- OS.get_environment("straywild_LOGBOOK_CAPTURE")
- + suffix
- + ".png"
- )
- assert(image.save_png(path) == OK)
diff --git a/tests/logbook_runtime_validation.gd.uid b/tests/logbook_runtime_validation.gd.uid
deleted file mode 100644
index 1d23e1d..0000000
--- a/tests/logbook_runtime_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ciii3u308pk4n
diff --git a/tests/logbook_validation.gd b/tests/logbook_validation.gd
deleted file mode 100644
index 431ed2e..0000000
--- a/tests/logbook_validation.gd
+++ /dev/null
@@ -1,1072 +0,0 @@
-extends SceneTree
-
-const FishCatchType = preload("res://fish/fish_catch.gd")
-const FishInventoryType = preload("res://inventory/fish_inventory.gd")
-const FishPoolType = preload("res://fish/fish_pool.gd")
-const CollectionLogType = preload("res://collection/collection_log.gd")
-const FishDataType = preload("res://fish/fish_data.gd")
-const FishQualityType = preload("res://fish/fish_quality.gd")
-const LogbookPortraitType = preload(
- "res://ui/components/logbook_portrait.gd"
-)
-const InventoryNotepadType = preload(
- "res://ui/components/inventory_notepad.gd"
-)
-const LogbookPageScene = preload("res://ui/logbook_page.tscn")
-const CatalogResource: FishPoolType = preload(
- "res://fish/pools/fish_catalog.tres"
-)
-
-var _query_available_id: StringName
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- if OS.has_environment("straywild_TEST_CREATE_ROOT"):
- var data_root := PlayerDataRoot.new()
- root.add_child(data_root)
- var result: Dictionary = data_root.create_unbound_root(
- OS.get_environment("straywild_TEST_CREATE_ROOT")
- )
- assert(bool(result.get("ok", false)))
- data_root.queue_free()
- print("Logbook portable test root: PASS")
- quit()
- return
- _validate_catalog()
- _validate_catalog_query()
- await _validate_page()
- print("Logbook validation: PASS")
- quit()
-
-
-func _validate_catalog() -> void:
- assert(CatalogResource.candidates.size() == 316)
- var ordered := LogbookCatalog.ordered_species(CatalogResource.candidates)
- assert(ordered.size() == 313)
- var previous_number: int = 0
- var catalog_numbers: Dictionary[int, bool] = {}
- for fish: FishDataType in CatalogResource.candidates:
- assert(fish != null)
- assert(fish.catalog_number > 0)
- assert(not catalog_numbers.has(fish.catalog_number))
- catalog_numbers[fish.catalog_number] = true
- assert(not LogbookCatalog.facts_for(fish).is_empty())
- assert(
- LogbookCatalog.facts_for(fish)
- == LogbookCatalog.facts_for(fish).to_lower()
- )
- assert(LogbookCatalog.facts_for(fish) != "unknown")
- for fish: FishDataType in ordered:
- assert(fish.active)
- assert(fish.catalog_number > previous_number)
- assert(LogbookCatalog.catalog_number(fish) == fish.catalog_number)
- previous_number = fish.catalog_number
- assert(
- LogbookCatalog.empty_state(LogbookCatalog.Category.SALT_WATER)
- == "No saltwater catches cataloged yet."
- )
-
-
-func _validate_catalog_query() -> void:
- var collection := CollectionLogType.new()
- var inventory := FishInventoryType.new()
- var fresh_species: Array[FishDataType] = []
- for fish: FishDataType in LogbookCatalog.ordered_species(
- CatalogResource.candidates
- ):
- if LogbookCatalog.category_for(fish) == LogbookCatalog.Category.FRESH_WATER:
- fresh_species.append(fish)
- assert(fresh_species.size() == 108)
- var first := fresh_species[0]
- var second := fresh_species[1]
- var unknown := fresh_species[2]
- for quality: int in FishQualityType.TIER_COUNT:
- collection.record_catch(first.id, quality)
- collection.record_catch(second.id, FishQualityType.Tier.BORING)
- var held := _make_test_catch(first, true)
- assert(inventory.add_catch(held))
-
- var options := _default_query_options()
- options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
- var filtered := LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(filtered.size() == 2)
- assert(first in filtered and second in filtered)
-
- options["show"] = LogbookCatalog.DiscoveryFilter.MISSING
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(filtered.size() == fresh_species.size() - 2)
- options["sort"] = LogbookCatalog.SortMode.CATALOG_NUMBER
- options["descending"] = true
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(filtered.front() == fresh_species.back())
- assert(filtered.back() == unknown)
-
- options["show"] = LogbookCatalog.DiscoveryFilter.INCOMPLETE
- options["descending"] = false
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(filtered == [second])
-
- options = _default_query_options()
- options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
- options["sort"] = LogbookCatalog.SortMode.CATCH_COUNT
- options["descending"] = true
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(filtered == [first, second])
-
- options = _default_query_options()
- options["search"] = unknown.display_name
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(filtered.is_empty())
- options["search"] = str(unknown.catalog_number)
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(filtered == [unknown])
-
- options = _default_query_options()
- options["holding"] = LogbookCatalog.HoldingFilter.FAVORITED
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(filtered == [first])
- options["holding"] = LogbookCatalog.HoldingFilter.IN_COOLER
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(filtered == [first])
-
- options = _default_query_options()
- options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
- options["group"] = String(first.collection_group)
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(first in filtered)
- for fish: FishDataType in filtered:
- assert(fish.collection_group == first.collection_group)
-
- options = _default_query_options()
- options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
- options["rarity"] = (
- int(first.rarity) + LogbookCatalog.RarityFilter.COMMON
- )
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(first in filtered)
- for fish: FishDataType in filtered:
- assert(fish.rarity == first.rarity)
-
- options = _default_query_options()
- options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
- options["method"] = (
- int(first.collection_method) + LogbookCatalog.MethodFilter.FISHING
- )
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(first in filtered)
- for fish: FishDataType in filtered:
- assert(fish.collection_method == first.collection_method)
-
- var first_season := -1
- for season: int in 4:
- if first.is_available_in_season(season):
- first_season = season
- break
- assert(first_season >= 0)
- options = _default_query_options()
- options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
- options["season"] = (
- first_season + LogbookCatalog.SeasonFilter.SPRING
- )
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(first in filtered)
- for fish: FishDataType in filtered:
- assert(fish.is_available_in_season(first_season))
-
- var first_is_available_by_day := (
- first.availability == null or first.availability.allow_day
- )
- options = _default_query_options()
- options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
- options["time"] = (
- LogbookCatalog.TimeFilter.DAY
- if first_is_available_by_day
- else LogbookCatalog.TimeFilter.NIGHT
- )
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(first in filtered)
-
- options = _default_query_options()
- options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT
- options["sort"] = LogbookCatalog.SortMode.QUALITY_PROGRESS
- options["descending"] = true
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(filtered == [first, second])
- for sort_mode: LogbookCatalog.SortMode in [
- LogbookCatalog.SortMode.NAME,
- LogbookCatalog.SortMode.RARITY,
- LogbookCatalog.SortMode.VALUE,
- LogbookCatalog.SortMode.WEIGHT,
- ]:
- options = _default_query_options()
- options["sort"] = sort_mode
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(filtered[0] in [first, second])
- assert(filtered[1] in [first, second])
- assert(filtered[0] != filtered[1])
- assert(filtered[2] == unknown)
-
- _query_available_id = second.id
- options = _default_query_options()
- options["availability"] = (
- LogbookCatalog.AvailabilityFilter.AVAILABLE_NOW
- )
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- Callable(self, "_query_species_available_now"),
- )
- assert(filtered == [second])
-
- _query_available_id = unknown.id
- options["show"] = LogbookCatalog.DiscoveryFilter.MISSING
- options["rarity"] = (
- int(unknown.rarity) + LogbookCatalog.RarityFilter.COMMON
- )
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- Callable(self, "_query_species_available_now"),
- )
- assert(filtered == [unknown])
-
- options = _default_query_options()
- options["show"] = LogbookCatalog.DiscoveryFilter.MISSING
- options["group"] = String(unknown.collection_group)
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(unknown in filtered)
- assert(
- String(unknown.collection_group)
- in LogbookCatalog.available_groups(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- )
- )
-
- var unknown_is_available_by_day := (
- unknown.availability == null or unknown.availability.allow_day
- )
- options = _default_query_options()
- options["show"] = LogbookCatalog.DiscoveryFilter.MISSING
- options["time"] = (
- LogbookCatalog.TimeFilter.DAY
- if unknown_is_available_by_day
- else LogbookCatalog.TimeFilter.NIGHT
- )
- filtered = LogbookCatalog.filtered_species(
- CatalogResource.candidates,
- LogbookCatalog.Category.FRESH_WATER,
- collection,
- inventory,
- options,
- )
- assert(unknown in filtered)
- inventory.free()
- collection.free()
-
-
-func _default_query_options() -> Dictionary:
- return {
- "search": "",
- "show": LogbookCatalog.DiscoveryFilter.ALL,
- "sort": LogbookCatalog.SortMode.CATALOG_NUMBER,
- "descending": false,
- "availability": LogbookCatalog.AvailabilityFilter.ANY,
- "season": LogbookCatalog.SeasonFilter.ANY,
- "time": LogbookCatalog.TimeFilter.ANY,
- "rarity": LogbookCatalog.RarityFilter.ANY,
- "method": LogbookCatalog.MethodFilter.ANY,
- "holding": LogbookCatalog.HoldingFilter.ANY,
- "group": "",
- }
-
-
-func _make_test_catch(fish: FishDataType, favorited: bool) -> FishCatchType:
- var fish_catch := FishCatchType.new()
- fish_catch.fish = fish
- fish_catch.fish_id = fish.id
- fish_catch.weight_lb = fish.get_minimum_weight()
- fish_catch.display_scale = fish.get_display_scale_for_weight(
- fish_catch.weight_lb
- )
- fish_catch.quality = FishQualityType.Tier.BORING
- fish_catch.sale_value = fish.get_sale_value_for_weight(fish_catch.weight_lb)
- fish_catch.is_favorited = favorited
- fish_catch.ensure_identity()
- return fish_catch
-
-
-func _query_species_available_now(fish: FishDataType) -> bool:
- return fish != null and fish.id == _query_available_id
-
-
-func _validate_page() -> void:
- var collection := CollectionLogType.new()
- var inventory := FishInventoryType.new()
- root.add_child(collection)
- root.add_child(inventory)
- var page := LogbookPageScene.instantiate() as LogbookPage
- root.add_child(page)
- page.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT)
- page.size = Vector2(1280.0, 720.0)
- await process_frame
- page.setup(collection, inventory, CatalogResource)
- page.activate()
- await process_frame
- var outer_margin := page.get("_outer_margin") as MarginContainer
- var book := page.get("_book") as Control
- assert(outer_margin != null)
- assert(book != null)
- page.set_compact_presentation(true)
- await process_frame
- assert(outer_margin.get_theme_constant("margin_left") == 160)
- assert(outer_margin.get_theme_constant("margin_right") == 160)
- assert(outer_margin.get_theme_constant("margin_bottom") == 106)
- assert(is_equal_approx(book.offset_top, 65.0))
- page.call("_set_index_open", true, false)
- await process_frame
- _validate_index_content_bounds(page)
- page.call("_set_index_open", false, false)
- page.set_compact_presentation(false)
- await process_frame
- assert(outer_margin.get_theme_constant("margin_left") == 52)
- assert(outer_margin.get_theme_constant("margin_right") == 52)
- assert(outer_margin.get_theme_constant("margin_bottom") == 18)
- assert(is_equal_approx(book.offset_top, 50.0))
- assert(
- page.get("_category") == LogbookCatalog.Category.FRESH_WATER
- )
- assert((page.get("_catalog_grid") as GridContainer).columns == 4)
- var catalog_scroll := page.get("_catalog_scroll") as ScrollContainer
- assert(catalog_scroll != null and catalog_scroll.follow_focus)
- catalog_scroll.scroll_vertical = 37
- await create_timer(0.2).timeout
- assert(catalog_scroll.scroll_vertical == 37)
- page.call(
- "_set_controller_zone",
- LogbookPage.ControllerZone.ENTRIES,
- )
- for _frame: int in 2:
- await process_frame
- var initial_entries: Array = (
- page.get("_entry_buttons") as Dictionary
- ).values()
- assert(initial_entries.size() > 4)
- var final_entry := initial_entries.back() as Button
- final_entry.grab_focus()
- for _frame: int in 3:
- await process_frame
- assert(catalog_scroll.scroll_vertical > 0)
- page.reset_controller_zone()
- for _frame: int in 2:
- await process_frame
- var category_tabs: Array = page.get("_category_tabs") as Array
- var category_tab_categories: Array = (
- page.get("_category_tab_categories") as Array
- )
- assert(category_tab_categories == [
- LogbookCatalog.Category.FRESH_WATER,
- LogbookCatalog.Category.SALT_WATER,
- LogbookCatalog.Category.SHELLFISH,
- LogbookCatalog.Category.OTHER,
- ])
- assert(category_tabs.size() == category_tab_categories.size())
- assert(
- LogbookCatalog.category_label(LogbookCatalog.Category.OTHER)
- == "Insects"
- )
- for tab_node: Variant in category_tabs:
- var category_tab := tab_node as Button
- assert(category_tab.size == LogbookPage.LOGBOOK_TAB_SIZE)
- var text_width: float = UtilityPageStyle.TuffyFont.get_string_size(
- category_tab.text,
- HORIZONTAL_ALIGNMENT_LEFT,
- -1.0,
- OrganizerTab.FONT_SIZE,
- ).x
- assert(
- text_width + LogbookPage.LOGBOOK_TAB_TEXT_SIDE_INSET * 2.0
- <= category_tab.size.x
- )
- assert(
- (category_tabs[0] as Button).get_parent().position.x
- == LogbookPage.LOGBOOK_TAB_LEFT_INSET
- )
- var initial_detail_body := page.get("_detail_body") as VBoxContainer
- assert(not initial_detail_body.get_parent() is ScrollContainer)
- var index_tab := page.get("_index_tab") as Button
- var index_body := page.get("_index_body") as VBoxContainer
- assert(index_tab != null and index_body != null)
- assert(index_tab.size == LogbookPage.INDEX_TAB_SIZE)
- assert(
- not index_tab.get_global_rect().intersects(
- (category_tabs.back() as Button).get_global_rect()
- )
- )
- page.call("_set_index_open", true, false)
- await process_frame
- assert(index_body.visible)
- assert(not initial_detail_body.visible)
- assert((page.get("_index_option_buttons") as Dictionary).size() == 10)
- _validate_index_content_bounds(page)
- page.call(
- "_cycle_index_option",
- &"show",
- 1,
- )
- await process_frame
- assert((page.get("_entry_buttons") as Dictionary).is_empty())
- assert(
- (page.get("_empty_state") as Label).text
- == "No entries match this index."
- )
- page.call("_reset_index")
- await process_frame
- assert((page.get("_entry_buttons") as Dictionary).size() == 108)
- page.call("_set_index_open", false, false)
- await process_frame
- assert(not index_body.visible)
- assert(initial_detail_body.visible)
- assert(
- page.call("_entry_label_text", "flathead catfish")
- == "flathead\ncatfish"
- )
-
- var shared_material: Material
- var silhouette_count: int = 0
- var target_silhouette_area: float = (
- LogbookPage.CATALOG_PORTRAIT_SIZE.x
- * LogbookPage.CATALOG_PORTRAIT_SIZE.y
- * LogbookPortraitType.UNIFORM_FOOTPRINT_AREA_RATIO
- )
- for category: LogbookCatalog.Category in [
- LogbookCatalog.Category.FRESH_WATER,
- LogbookCatalog.Category.SALT_WATER,
- ]:
- page.call("_select_category", category)
- await create_timer(0.25).timeout
- var entries: Dictionary = page.get("_entry_buttons")
- assert(
- entries.size()
- == (108 if category == LogbookCatalog.Category.FRESH_WATER else 202)
- )
- for fish: FishDataType in LogbookCatalog.ordered_species(
- CatalogResource.candidates
- ):
- if LogbookCatalog.category_for(fish) != category:
- continue
- var unknown_key := StringName(
- "unknown_%d" % LogbookCatalog.catalog_number(fish)
- )
- var entry := entries.get(unknown_key) as Button
- assert(entry != null)
- assert(entry.text.is_empty())
- assert(entry.tooltip_text.is_empty())
- assert(entry.icon == null)
- assert(entry.accessibility_name == "Unknown catalog entry")
- assert(_has_visible_unknown_name(entry))
- var portrait: LogbookPortraitType = _find_portrait(entry)
- assert(portrait != null)
- assert(portrait.source_texture == fish.display_texture)
- assert(
- portrait.custom_minimum_size.x
- <= LogbookPage.CATALOG_PORTRAIT_SIZE.x
- and portrait.custom_minimum_size.y
- <= LogbookPage.CATALOG_PORTRAIT_SIZE.y
- )
- assert(portrait.get_parent() is CenterContainer)
- assert(
- (portrait.get_parent() as CenterContainer).custom_minimum_size
- == LogbookPage.CATALOG_PORTRAIT_SIZE
- )
- assert(
- portrait.expand_mode
- == TextureRect.EXPAND_IGNORE_SIZE
- )
- assert(
- portrait.stretch_mode
- == TextureRect.STRETCH_KEEP_ASPECT_CENTERED
- )
- assert(portrait.material is ShaderMaterial)
- if shared_material == null:
- shared_material = portrait.material
- else:
- assert(portrait.material == shared_material)
- assert(_texture_has_transparency(portrait.texture))
- var silhouette_area: float = (
- portrait.custom_minimum_size.x
- * portrait.custom_minimum_size.y
- )
- assert(silhouette_area > 0.0)
- assert(silhouette_area <= target_silhouette_area * 1.05)
- assert(
- silhouette_area >= target_silhouette_area * 0.9
- or is_equal_approx(
- portrait.custom_minimum_size.x,
- LogbookPage.CATALOG_PORTRAIT_SIZE.x,
- )
- or is_equal_approx(
- portrait.custom_minimum_size.y,
- LogbookPage.CATALOG_PORTRAIT_SIZE.y,
- )
- )
- silhouette_count += 1
- for candidate: FishDataType in CatalogResource.candidates:
- assert(not entry.text.contains(candidate.display_name))
- assert(
- not entry.tooltip_text.contains(
- candidate.display_name
- )
- )
- assert(
- not entry.accessibility_name.contains(
- candidate.display_name
- )
- )
- assert(silhouette_count == 310)
-
- page.call("_select_category", LogbookCatalog.Category.SHELLFISH)
- await create_timer(0.25).timeout
- assert((page.get("_entry_buttons") as Dictionary).size() == 2)
- assert(
- (page.get("_entry_buttons") as Dictionary).has(&"unknown_3906")
- )
- assert(
- (page.get("_entry_buttons") as Dictionary).has(&"unknown_6406")
- )
- page.call("_select_category", LogbookCatalog.Category.OTHER)
- await create_timer(0.25).timeout
- assert((page.get("_entry_buttons") as Dictionary).size() == 1)
- assert(
- (page.get("_entry_buttons") as Dictionary).has(&"unknown_8001")
- )
- page.call("_select_category", LogbookCatalog.Category.FRESH_WATER)
- await create_timer(0.25).timeout
-
- var bluegill = CatalogResource.get_fish_by_id(&"bluegill")
- var bluegill_unknown := StringName(
- "unknown_%d" % bluegill.catalog_number
- )
- page.call("_select_entry", bluegill_unknown, StringName())
- assert(
- (page.get("_entry_buttons") as Dictionary)
- .get(bluegill_unknown) != null
- )
- collection.mark_discovered(&"bluegill")
- collection.mark_quality_discovered(
- &"bluegill",
- FishQualityType.Tier.BORING,
- )
- await process_frame
- var entries: Dictionary = page.get("_entry_buttons")
- var known := entries.get(&"bluegill") as Button
- assert(known != null)
- assert(known.custom_minimum_size.x == known.custom_minimum_size.y)
- _validate_entry_bubble_styles(known)
- assert(known.text.is_empty())
- assert(known.icon == null)
- assert(_has_visible_name(known, bluegill.display_name))
- var known_portrait: LogbookPortraitType = _find_portrait(known)
- assert(known_portrait != null)
- assert(known_portrait.source_texture == bluegill.display_texture)
- assert(known_portrait.material == null)
- assert(known_portrait.get_parent() is CenterContainer)
- assert(
- (known_portrait.get_parent() as CenterContainer).custom_minimum_size
- == LogbookPage.CATALOG_PORTRAIT_SIZE
- )
- assert(known.button_pressed)
- _validate_handwritten_logbook_font(page)
-
- var fish_catch := FishCatchType.new()
- fish_catch.fish = bluegill
- fish_catch.fish_id = bluegill.id
- fish_catch.weight_lb = bluegill.get_minimum_weight()
- fish_catch.display_scale = bluegill.get_display_scale_for_weight(
- fish_catch.weight_lb
- )
- fish_catch.sale_value = bluegill.get_sale_value_for_weight(
- fish_catch.weight_lb
- )
- fish_catch.ensure_identity()
- inventory.add_catch(fish_catch)
- collection.record_catch(fish_catch.fish_id, fish_catch.quality)
- page.call("_select_entry", &"bluegill", &"bluegill")
- await process_frame
- assert(not _detail_text(page).contains("number owned"))
- assert(_detail_text(page).contains("number caught\n1"))
- assert(_detail_text(page).contains("body of water\nfresh water"))
- assert(
- _detail_text(page).contains(
- "catalog number\n#%03d" % bluegill.catalog_number
- )
- )
- assert(_detail_text(page).contains("fish facts\n"))
- assert(_detail_text(page).contains(LogbookCatalog.facts_for(bluegill)))
- _validate_detail_field_fonts(page)
- _validate_handwritten_numeric_scale(page)
- _validate_detail_content_bounds(page)
- var detail_buttons: Array = page.get("_detail_buttons") as Array
- assert(detail_buttons.size() == 4)
- var portrait_detail := detail_buttons[0] as Button
- var facts_detail := detail_buttons[1] as Button
- var quality_detail := detail_buttons[2] as Button
- var stats_detail := detail_buttons[3] as Button
- assert(
- quality_detail.custom_minimum_size
- == LogbookPage.DETAIL_QUALITY_SECTION_SIZE
- )
- assert(
- stats_detail.custom_minimum_size
- == LogbookPage.DETAIL_STATS_SECTION_SIZE
- )
- assert(
- quality_detail.size_flags_horizontal
- == Control.SIZE_SHRINK_CENTER
- )
- assert(
- stats_detail.size_flags_horizontal
- == Control.SIZE_SHRINK_CENTER
- )
- assert(
- stats_detail.size_flags_vertical & Control.SIZE_EXPAND == 0
- )
- assert(
- portrait_detail.get_node(portrait_detail.focus_neighbor_right)
- == facts_detail
- )
- assert(
- portrait_detail.get_node(portrait_detail.focus_neighbor_bottom)
- == quality_detail
- )
- assert(
- facts_detail.get_node(facts_detail.focus_neighbor_left)
- == portrait_detail
- )
- assert(
- facts_detail.get_node(facts_detail.focus_neighbor_bottom)
- == quality_detail
- )
- assert(
- quality_detail.get_node(quality_detail.focus_neighbor_bottom)
- == stats_detail
- )
- assert(
- stats_detail.get_node(stats_detail.focus_neighbor_top)
- == quality_detail
- )
- var portrait_button := page.get("_detail_portrait_button") as Button
- assert(portrait_button != null)
- portrait_button.pressed.emit()
- await process_frame
- var portrait_overlay := page.get("_portrait_overlay") as Control
- assert(portrait_overlay.visible)
- var overlay_artwork := (
- page.get("_portrait_overlay_artwork") as LogbookPortraitType
- )
- assert(overlay_artwork.source_texture == bluegill.display_texture)
- assert(
- overlay_artwork.custom_minimum_size.x
- <= LogbookPage.PORTRAIT_VIEW_MAX_SIZE.x
- and overlay_artwork.custom_minimum_size.y
- <= LogbookPage.PORTRAIT_VIEW_MAX_SIZE.y
- )
- var overlay_card := _find_overlay_card(portrait_overlay)
- assert(overlay_card != null)
- assert(overlay_card.size == LogbookPage.DETAIL_OVERLAY_CARD_SIZE)
- assert(is_equal_approx(overlay_card.size.aspect(), 4.0 / 3.0))
- (page.get("_portrait_overlay_backdrop") as Button).pressed.emit()
- await process_frame
- assert(not portrait_overlay.visible)
- detail_buttons = page.get("_detail_buttons") as Array
- assert(detail_buttons.size() == 4)
- facts_detail = detail_buttons[1] as Button
- facts_detail.pressed.emit()
- await process_frame
- var overlay_text := page.get("_portrait_overlay_text") as Label
- assert(overlay_text.visible)
- assert(
- overlay_text.get_theme_font_size("font_size")
- == LogbookPage.DETAIL_OVERLAY_TEXT_FONT_SIZE
- )
- assert(
- overlay_text.horizontal_alignment
- == HORIZONTAL_ALIGNMENT_LEFT
- )
- assert(
- overlay_text.get_theme_font("font")
- == UtilityPageStyle.TuffyFont
- )
- (page.get("_portrait_overlay_backdrop") as Button).pressed.emit()
- await process_frame
-
- quality_detail.pressed.emit()
- await process_frame
- var quality_view := page.get("_portrait_overlay_quality_view") as Control
- assert(quality_view.visible)
- var quality_text: String = _descendant_label_text(quality_view)
- for quality: int in FishQualityType.TIER_COUNT:
- assert(quality_text.contains(FishQualityType.display_name(quality)))
- assert(quality_text.contains("●"))
- assert(quality_text.contains("○"))
- _validate_overlay_fonts(quality_view)
- (page.get("_portrait_overlay_backdrop") as Button).pressed.emit()
- await process_frame
-
- stats_detail.pressed.emit()
- await process_frame
- var stats_view := page.get("_portrait_overlay_stats_view") as Control
- assert(stats_view.visible)
- var stats_text: String = _descendant_label_text(stats_view)
- assert(stats_text.contains("catalog number"))
- assert(stats_text.contains("number caught"))
- assert(stats_text.contains("1"))
- assert(stats_text.contains("seasons"))
- assert(not stats_text.contains("number owned"))
- _validate_overlay_fonts(stats_view)
- (page.get("_portrait_overlay_backdrop") as Button).pressed.emit()
- await process_frame
-
- var live_index_options := page.get("_index_options") as Dictionary
- live_index_options["holding"] = LogbookCatalog.HoldingFilter.IN_COOLER
- page.call("_refresh_catalog")
- await process_frame
- assert(
- (page.get("_entry_buttons") as Dictionary).keys()
- == [&"bluegill"]
- )
- inventory.remove_catch_by_id(fish_catch.catch_id)
- await process_frame
- assert((page.get("_entry_buttons") as Dictionary).is_empty())
- page.call("_reset_index")
- await process_frame
- assert(not _detail_text(page).contains("number owned"))
- page.queue_free()
- collection.queue_free()
- inventory.queue_free()
-
-
-func _detail_text(page: LogbookPage) -> String:
- var values: PackedStringArray = []
- var detail_body := page.get("_detail_body") as VBoxContainer
- for node: Node in detail_body.find_children("*", "Label", true, false):
- values.append((node as Label).text)
- return "\n".join(values)
-
-
-func _validate_index_content_bounds(page: LogbookPage) -> void:
- var body := page.get("_index_body") as VBoxContainer
- assert(body != null and body.visible)
- var right_page := body.get_parent() as Control
- assert(right_page != null)
- var page_rect := right_page.get_global_rect()
- var body_rect := body.get_global_rect()
- var protected_rect := Rect2(
- page_rect.position + Vector2(
- LogbookPage.INDEX_CONTENT_LEFT_INSET,
- LogbookPage.INDEX_CONTENT_TOP_INSET,
- ),
- page_rect.size - Vector2(
- LogbookPage.INDEX_CONTENT_LEFT_INSET
- + LogbookPage.INDEX_CONTENT_RIGHT_INSET,
- LogbookPage.INDEX_CONTENT_TOP_INSET
- + LogbookPage.INDEX_CONTENT_BOTTOM_INSET,
- ),
- )
- assert(
- body_rect.position.x
- >= page_rect.position.x + LogbookPage.INDEX_CONTENT_LEFT_INSET - 1.0
- )
- assert(
- body_rect.end.x
- <= page_rect.end.x - LogbookPage.INDEX_CONTENT_RIGHT_INSET + 1.0
- )
- assert(
- body_rect.position.y >= page_rect.position.y,
- "Index starts above the authored page: body=%s page=%s"
- % [body_rect, page_rect],
- )
- assert(
- body_rect.end.y <= page_rect.end.y,
- "Index extends below the authored page: body=%s page=%s"
- % [body_rect, page_rect],
- )
- var controls: Array = page.get("_index_controls") as Array
- for value: Variant in controls:
- var control := value as Control
- if control != null and control.visible:
- assert(
- protected_rect.grow(2.0).encloses(control.get_global_rect()),
- "Index control crossed the authored right-page boundary: %s"
- % control.name,
- )
-
-
-func _descendant_label_text(parent: Node) -> String:
- var values: PackedStringArray = []
- for node: Node in parent.find_children("*", "Label", true, false):
- values.append((node as Label).text)
- return "\n".join(values)
-
-
-func _validate_overlay_fonts(parent: Node) -> void:
- for node: Node in parent.find_children("*", "Label", true, false):
- assert(
- (node as Label).get_theme_font("font")
- == UtilityPageStyle.TuffyFont
- )
-
-
-func _validate_detail_content_bounds(page: LogbookPage) -> void:
- var detail_body := page.get("_detail_body") as VBoxContainer
- var page_rect: Rect2 = (detail_body.get_parent() as Control).get_global_rect()
- for node: Node in detail_body.find_children("*", "Label", true, false):
- var label := node as Label
- var label_rect: Rect2 = label.get_global_rect()
- assert(
- label_rect.position.y >= page_rect.position.y - 0.5,
- "Label starts above logbook page: %s %s vs %s"
- % [label.text, label_rect, page_rect],
- )
- assert(
- label_rect.end.y <= page_rect.end.y + 0.5,
- "Label ends below logbook page: %s %s vs %s"
- % [label.text, label_rect, page_rect],
- )
-
-
-func _find_overlay_card(parent: Node) -> PanelContainer:
- for node: Node in parent.find_children(
- "*", "PanelContainer", true, false
- ):
- var panel := node as PanelContainer
- if panel.custom_minimum_size == LogbookPage.DETAIL_OVERLAY_CARD_SIZE:
- return panel
- return null
-
-
-func _find_portrait(entry: Button) -> LogbookPortraitType:
- var pending: Array[Node] = [entry]
- while not pending.is_empty():
- var node: Node = pending.pop_back()
- if node is LogbookPortraitType:
- return node as LogbookPortraitType
- pending.append_array(node.get_children())
- return null
-
-
-func _has_visible_unknown_name(entry: Button) -> bool:
- return _has_visible_name(entry, "???")
-
-
-func _has_visible_name(entry: Button, expected_name: String) -> bool:
- var pending: Array[Node] = [entry]
- while not pending.is_empty():
- var node: Node = pending.pop_back()
- if node is Label and (node as Label).text == expected_name:
- return true
- pending.append_array(node.get_children())
- return false
-
-
-func _validate_handwritten_logbook_font(page: LogbookPage) -> void:
- var detail_body := page.get("_detail_body") as VBoxContainer
- for node: Node in detail_body.find_children("*", "Label", true, false):
- var label := node as Label
- if label.text.begins_with("quality collection"):
- continue
- if label.text in [
- "fish facts",
- "catalog number",
- "rarity",
- "body of water",
- "time of day",
- "seasons",
- "weight range",
- "value range",
- "number caught",
- ]:
- continue
- assert(
- label.get_theme_font("font")
- == InventoryNotepadType.HANDWRITTEN_FONT
- )
-
-
-func _validate_detail_field_fonts(page: LogbookPage) -> void:
- var detail_body := page.get("_detail_body") as VBoxContainer
- var field_names: Array[String] = [
- "fish facts",
- "catalog number",
- "rarity",
- "body of water",
- "time of day",
- "seasons",
- "weight range",
- "value range",
- "number caught",
- ]
- for node: Node in detail_body.find_children("*", "Label", true, false):
- var label := node as Label
- if label.text.begins_with("quality collection"):
- assert(label.get_theme_font("font") == UtilityPageStyle.TuffyFont)
- continue
- if label.text not in field_names:
- continue
- assert(label.get_theme_font("font") == UtilityPageStyle.TuffyFont)
- if label.text != "fish facts":
- assert(
- label.horizontal_alignment
- == HORIZONTAL_ALIGNMENT_CENTER
- )
-
-
-func _validate_handwritten_numeric_scale(page: LogbookPage) -> void:
- var detail_body := page.get("_detail_body") as VBoxContainer
- var text_value: Label
- var numeric_value: Label
- for node: Node in detail_body.find_children("*", "Label", true, false):
- var label := node as Label
- if label.text == "common":
- text_value = label
- elif label.text.begins_with("#"):
- numeric_value = label
- assert(text_value != null)
- assert(numeric_value != null)
- assert(
- numeric_value.get_theme_font_size("font_size")
- < text_value.get_theme_font_size("font_size")
- )
-
-
-func _validate_entry_bubble_styles(entry: Button) -> void:
- var normal := entry.get_theme_stylebox("normal") as StyleBoxFlat
- var hover := entry.get_theme_stylebox("hover") as StyleBoxFlat
- var pressed := entry.get_theme_stylebox("pressed") as StyleBoxFlat
- assert(normal != null)
- assert(hover != null)
- assert(pressed != null)
- assert(is_zero_approx(normal.bg_color.a))
- assert(hover.bg_color.a > 0.0)
- assert(pressed.bg_color.a > 0.0)
- assert(hover.border_width_left == 0)
- assert(pressed.border_width_left == 0)
- assert(hover.corner_radius_top_left == 51)
-
-
-func _texture_has_transparency(texture: Texture2D) -> bool:
- var image: Image = texture.get_image()
- return image != null and image.detect_alpha() != Image.ALPHA_NONE
diff --git a/tests/logbook_validation.gd.uid b/tests/logbook_validation.gd.uid
deleted file mode 100644
index 1f35fa1..0000000
--- a/tests/logbook_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c8pt0ibr37rmw
diff --git a/tests/manual/terrain_chunk_generator/terrain_chunk_compatibility_report.gd b/tests/manual/terrain_chunk_generator/terrain_chunk_compatibility_report.gd
deleted file mode 100644
index 0d35543..0000000
--- a/tests/manual/terrain_chunk_generator/terrain_chunk_compatibility_report.gd
+++ /dev/null
@@ -1,122 +0,0 @@
-extends SceneTree
-
-const CATALOG: TerrainChunkCatalog = preload(
- "res://world/generation/chunks/terrain_chunk_catalog.tres"
-)
-const BIOME_CATALOG: TerrainBiomeCatalog = preload(
- "res://world/generation/biomes/terrain_biome_catalog.tres"
-)
-
-
-func _initialize() -> void:
- call_deferred(&"_run")
-
-
-func _run() -> void:
- var generator := _make_generator()
- root.add_child(generator)
- if not generator._prepare_catalog():
- generator.free()
- quit(1)
- return
- var variants: Array[TerrainChunkVariant] = []
- variants.assign(generator._solver_variants)
-
- print(
- "Terrain chunk rotations: %d authored, %d distinct constraints"
- % [generator._variants.size(), variants.size()]
- )
- for variant: TerrainChunkVariant in variants:
- print("\n", variant.stable_key())
- var ocean_edges := variant.rotated_edge_mask(
- variant.definition.ocean_facing_edges
- )
- for edge_value: int in TerrainChunkTopology.Edge.values():
- var edge := edge_value as TerrainChunkTopology.Edge
- var matches := PackedStringArray()
- for other: TerrainChunkVariant in variants:
- var opposite := TerrainChunkTopology.opposite_edge(edge)
- if generator._edges_are_compatible(
- variant,
- edge,
- other,
- opposite,
- ):
- matches.append(
- "%s.%s"
- % [
- other.stable_key(),
- TerrainChunkTopology.edge_name(opposite),
- ]
- )
- print(
- " %s%s [%d points]: %s"
- % [
- TerrainChunkTopology.edge_name(edge),
- (
- " [ocean]"
- if (ocean_edges & (1 << edge_value)) != 0
- else ""
- ),
- variant.profile(edge).points.size(),
- ", ".join(matches) if not matches.is_empty() else "NONE",
- ]
- )
-
- if generator.generate():
- var summary := generator._build_summary()
- print(
- (
- "\nSeeded diagnostic layout %s "
- + "(%d backtracks, %d repeated edges):"
- )
- % [
- str(summary["layout_fingerprint"]).substr(0, 12),
- int(summary["backtracks"]),
- int(summary["adjacent_repeat_edges"]),
- ]
- )
- var keys := generator.placement_keys()
- for row: int in generator.grid_size.y:
- var cells := PackedStringArray()
- for column: int in generator.grid_size.x:
- cells.append(keys[row * generator.grid_size.x + column])
- print(" ", " ".join(cells))
- var biomes := TerrainBiomeAssigner.assign(
- BIOME_CATALOG,
- generator.placement_records(),
- generator.generation_seed,
- )
- print("\nBiome layout:")
- for row: int in generator.grid_size.y:
- var cells := PackedStringArray()
- for column: int in generator.grid_size.x:
- var biome_id: StringName = biomes.get(
- Vector2i(column, row),
- &"",
- )
- cells.append(String(biome_id).trim_prefix("biome_"))
- print(" ", " ".join(cells))
- generator.free()
- quit(0)
-
-
-func _make_generator() -> TerrainChunkGenerator:
- var generator := TerrainChunkGenerator.new()
- generator.catalog = CATALOG
- generator.grid_size = Vector2i(9, 9)
- generator.generation_seed = 13001
- generator.generate_on_ready = false
- generator.build_collision = false
- generator.force_center_chunk_id = &"chunk_spawn"
- generator.required_chunk_ids = PackedStringArray(
- [
- "chunk_spawn",
- "chunk_0001",
- "chunk_0002",
- "chunk_0003",
- "chunk_0004",
- ]
- )
- generator.grass_sand_smoothing_enabled = true
- return generator
diff --git a/tests/manual/terrain_chunk_generator/terrain_chunk_compatibility_report.gd.uid b/tests/manual/terrain_chunk_generator/terrain_chunk_compatibility_report.gd.uid
deleted file mode 100644
index 1da131f..0000000
--- a/tests/manual/terrain_chunk_generator/terrain_chunk_compatibility_report.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://di4yaor4f3s4e
diff --git a/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.gd b/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.gd
deleted file mode 100644
index 657347f..0000000
--- a/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.gd
+++ /dev/null
@@ -1,37 +0,0 @@
-extends Node3D
-
-@onready var _generator: TerrainChunkGenerator = $TerrainChunkGenerator
-@onready var _status: Label = $Instructions/Status
-
-
-func _unhandled_input(event: InputEvent) -> void:
- if event.is_action_pressed(&"ui_cancel"):
- get_viewport().set_input_as_handled()
- get_tree().quit()
- return
- var key_event := event as InputEventKey
- if (
- key_event != null
- and key_event.pressed
- and not key_event.echo
- and key_event.physical_keycode == KEY_R
- ):
- get_viewport().set_input_as_handled()
- _generator.generation_seed += 1
- _generator.generate()
-
-
-func _on_generation_completed(summary: Dictionary) -> void:
- _status.text = (
- "Terrain generator diagnostic • seed %d • %d chunks • "
- + "%d rotations / %d constraints • %d backtracks • "
- + "%d repeated edges\nlayout %s • R: regenerate • Esc/B: close"
- ) % [
- int(summary["seed"]),
- int(summary["chunk_count"]),
- int(summary["variant_count"]),
- int(summary["solver_variant_count"]),
- int(summary["backtracks"]),
- int(summary["adjacent_repeat_edges"]),
- str(summary["layout_fingerprint"]).substr(0, 12),
- ]
diff --git a/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.gd.uid b/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.gd.uid
deleted file mode 100644
index 4713f51..0000000
--- a/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://0dmjswsm038h
diff --git a/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.tscn b/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.tscn
deleted file mode 100644
index 5c1e046..0000000
--- a/tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.tscn
+++ /dev/null
@@ -1,48 +0,0 @@
-[gd_scene load_steps=7 format=3]
-
-[ext_resource type="Script" path="res://tests/manual/terrain_chunk_generator/terrain_chunk_generator_test.gd" id="1_test"]
-[ext_resource type="Script" path="res://world/generation/terrain_chunk_generator.gd" id="2_generator"]
-[ext_resource type="Resource" path="res://world/generation/chunks/terrain_chunk_catalog.tres" id="3_catalog"]
-[ext_resource type="PackedScene" path="res://player/player.tscn" id="4_player"]
-[ext_resource type="Environment" path="res://world/environment/straywild_environment.tres" id="5_environment"]
-
-[node name="TerrainChunkGeneratorTest" type="Node3D"]
-script = ExtResource("1_test")
-
-[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
-environment = ExtResource("5_environment")
-
-[node name="Sun" type="DirectionalLight3D" parent="."]
-rotation_degrees = Vector3(-54, -32, 0)
-light_color = Color(0.88, 0.94, 0.96, 1)
-light_energy = 1.0
-shadow_enabled = true
-
-[node name="TerrainChunkGenerator" type="Node3D" parent="."]
-script = ExtResource("2_generator")
-catalog = ExtResource("3_catalog")
-grid_size = Vector2i(15, 13)
-generation_seed = 13001
-build_collision = true
-show_chunk_labels = false
-force_center_chunk_id = &"chunk_spawn"
-required_chunk_ids = PackedStringArray("chunk_spawn", "chunk_0001", "chunk_0002", "chunk_0003", "chunk_0004")
-grass_sand_smoothing_enabled = true
-elevated_cliff_feature_enabled = true
-
-[node name="Player" parent="." instance=ExtResource("4_player")]
-position = Vector3(0, 0.3, 0)
-
-[node name="Instructions" type="CanvasLayer" parent="."]
-layer = 20
-
-[node name="Status" type="Label" parent="Instructions"]
-offset_left = 18.0
-offset_top = 18.0
-offset_right = 930.0
-offset_bottom = 74.0
-text = "Terrain generator diagnostic\nR: regenerate • Esc/B: close"
-theme_override_colors/font_color = Color(0.76, 0.88, 0.9, 1)
-theme_override_font_sizes/font_size = 18
-
-[connection signal="generation_completed" from="TerrainChunkGenerator" to="." method="_on_generation_completed"]
diff --git a/tests/movement_multiplayer_validation.gd b/tests/movement_multiplayer_validation.gd
deleted file mode 100644
index 05a668d..0000000
--- a/tests/movement_multiplayer_validation.gd
+++ /dev/null
@@ -1,908 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-const PlayerScene: PackedScene = preload("res://player/player.tscn")
-const TEST_PORT: int = 18171
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- await _validate_latency_smoothing()
- var arguments: PackedStringArray = OS.get_cmdline_user_args()
- if arguments.has("unit"):
- print("Movement latency smoothing validation: PASS")
- quit()
- return
- if arguments.has("host"):
- await _run_host()
- return
- if arguments.has("client"):
- await _run_client()
- return
- push_error("Movement multiplayer validation needs host or client mode.")
- quit(1)
-
-
-func _validate_latency_smoothing() -> void:
- var avatar := PlayerScene.instantiate() as Player
- root.add_child(avatar)
- await process_frame
- avatar.set_process(false)
- avatar.set_physics_process(false)
- _validate_compact_input_encoding()
- _validate_compact_snapshot_encoding()
- _validate_compact_animation_encoding()
- _validate_animation_action_ordering(avatar)
- _validate_transit_estimation()
- await _validate_remote_snapshot_smoothing(avatar)
- _validate_remote_locomotion_playback_recovery(avatar)
- _validate_reliable_jump_intent(avatar)
- _validate_airborne_sitting(avatar)
- _validate_stale_input_expiry(avatar)
- avatar.queue_free()
- await process_frame
-
-
-func _validate_compact_input_encoding() -> void:
- var input: Dictionary = _movement_input(
- 12,
- true,
- true,
- true,
- &"strike",
- 4,
- true,
- )
- var encoded: Array = NetworkSession._encode_movement_input(input)
- assert(encoded.size() == NetworkSession.MOVEMENT_INPUT_FIELD_COUNT)
- assert(var_to_bytes(encoded).size() < var_to_bytes(input).size())
- assert(NetworkSession._decode_movement_input(encoded) == input)
- assert(NetworkSession._decode_movement_input(encoded.slice(0, 3)).is_empty())
- var unknown_flags: Array = encoded.duplicate()
- unknown_flags[3] = 1 << 12
- assert(NetworkSession._decode_movement_input(unknown_flags).is_empty())
-
-
-func _validate_compact_snapshot_encoding() -> void:
- var moving_snapshot: Dictionary = _network_snapshot(
- Vector3.ZERO,
- Vector3(4.5, 0.0, 0.0),
- 1,
- )
- assert(NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE == 8)
- assert(NetworkSession.OWNER_SNAPSHOT_DIVISOR == 3)
- var encoded_snapshots: Array = []
- for _peer: int in NetworkSession.MOVEMENT_SNAPSHOT_BATCH_SIZE:
- encoded_snapshots.append(
- NetworkSession._encode_movement_snapshot(moving_snapshot)
- )
- assert(var_to_bytes(encoded_snapshots).size() < 1200)
- var expected_snapshot: Dictionary = moving_snapshot.duplicate(true)
- expected_snapshot.erase("animation_state")
- assert(
- NetworkSession._decode_movement_snapshot(encoded_snapshots[0])
- == expected_snapshot
- )
- assert(
- NetworkSession._decode_movement_snapshot(
- encoded_snapshots[0].slice(0, 4)
- ).is_empty()
- )
- var invalid_snapshot_flags: Array = encoded_snapshots[0].duplicate()
- invalid_snapshot_flags[5] = 1 << 12
- assert(
- NetworkSession._decode_movement_snapshot(
- invalid_snapshot_flags
- ).is_empty()
- )
-
-
-func _validate_compact_animation_encoding() -> void:
- var paused_state := NetworkPlayerAnimationProtocol.make_state(
- NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE,
- true,
- &"strike",
- 7,
- 0.5,
- true,
- )
- var encoded: Array = NetworkSession._encode_movement_animation(
- 2,
- paused_state,
- )
- assert(encoded.size() == NetworkSession.MOVEMENT_ANIMATION_FIELD_COUNT)
- var decoded: Dictionary = NetworkSession._decode_movement_animation(encoded)
- assert(int(decoded.get("peer_id", 0)) == 2)
- assert(Dictionary(decoded.get("state", {})) == paused_state)
- var advanced_state: Dictionary = paused_state.duplicate(true)
- advanced_state["action"]["elapsed"] = 0.75
- assert(
- NetworkSession._movement_animation_signature(advanced_state)
- == NetworkSession._movement_animation_signature(paused_state)
- )
- var action: Dictionary = paused_state["action"]
- var encoded_action: Array = (
- NetworkSession._encode_movement_animation_action(action, true)
- )
- assert(
- encoded_action.size()
- == NetworkSession.MOVEMENT_ANIMATION_ACTION_FIELD_COUNT
- )
- var decoded_action: Dictionary = (
- NetworkSession._decode_movement_animation_action(encoded_action)
- )
- assert(Dictionary(decoded_action.get("action", {})) == action)
- assert(bool(decoded_action.get("sitting", false)))
- assert(
- NetworkSession._decode_movement_animation_action(
- encoded_action.slice(0, 2)
- ).is_empty()
- )
-
-
-func _validate_airborne_sitting(avatar: Player) -> void:
- avatar.reset_network_movement_state()
- avatar.set_local_control(true)
- avatar.set("_sit_after_landing", true)
- assert(avatar.get_network_sitting_intent())
- assert(not avatar.get_network_sitting_state())
- assert(not bool(avatar.make_network_snapshot(2)["sitting"]))
- assert(bool(avatar.capture_network_input(1)["sitting"]))
- avatar.call("_queue_local_network_jump_intent")
- var jumping_input: Dictionary = avatar.capture_network_input(2)
- assert(bool(jumping_input["jump"]))
- assert(not bool(jumping_input["sitting"]))
- avatar.call("_cancel_sitting_for_jump")
- assert(not bool(avatar.get("_sit_after_landing")))
- assert(bool(avatar.get("_sitting_intent_pending")))
- avatar.reset_network_movement_state()
-
- # Even a malformed or stale reconciliation that marks an airborne avatar as
- # seated must not bypass gravity and freeze it in place.
- avatar.set("_sit_after_landing", false)
- avatar.call("_set_sitting", true)
- avatar.velocity = Vector3(0.0, 4.0, 0.0)
- avatar.call("_simulate_movement_physics", 0.1)
- assert(not avatar.is_sitting())
- assert(bool(avatar.get("_sit_after_landing")))
- assert(avatar.velocity.y < 4.0)
- avatar.reset_network_movement_state()
-
-
-func _validate_animation_action_ordering(avatar: Player) -> void:
- var draw := NetworkPlayerAnimationProtocol.make_action_state(
- &"draw", 5, 0.2
- )
- avatar.apply_authoritative_network_animation_action(draw)
- assert(StringName(str(avatar.get("_animation_action_id"))) == &"draw")
- var cleared := NetworkPlayerAnimationProtocol.make_action_state(
- &"", 6, 0.0
- )
- avatar.apply_authoritative_network_animation_action(cleared)
- assert(StringName(str(avatar.get("_animation_action_id"))).is_empty())
- avatar.apply_authoritative_network_animation_action(draw)
- assert(StringName(str(avatar.get("_animation_action_id"))).is_empty())
- avatar.apply_authoritative_network_animation_action(
- NetworkPlayerAnimationProtocol.make_action_state(&"strike", 6, 0.0)
- )
- assert(StringName(str(avatar.get("_animation_action_id"))).is_empty())
- var strike := NetworkPlayerAnimationProtocol.make_action_state(
- &"strike", 7, 0.2
- )
- avatar.apply_authoritative_network_animation_action(strike)
- avatar.apply_authoritative_network_animation_action(
- NetworkPlayerAnimationProtocol.make_action_state(
- &"strike", 8, 0.4, true
- )
- )
- avatar.apply_authoritative_network_animation_action(strike)
- assert(bool(avatar.get("_animation_action_paused")))
- avatar.apply_authoritative_network_animation_action(
- NetworkPlayerAnimationProtocol.make_action_state(&"", 9, 0.0)
- )
-
- avatar.configure_network_remote(false)
- avatar.apply_network_animation_state(
- NetworkPlayerAnimationProtocol.make_state(
- NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE,
- true,
- &"strike",
- 9,
- 0.4,
- true,
- )
- )
- avatar.apply_network_animation_state(
- NetworkPlayerAnimationProtocol.make_state(
- NetworkPlayerAnimationProtocol.LOCOMOTION_WALKING,
- true,
- &"draw",
- 8,
- 0.1,
- )
- )
- assert(
- StringName(str(avatar.get("_network_target_animation_action_id")))
- == &"strike"
- )
- avatar.apply_network_animation_state(
- NetworkPlayerAnimationProtocol.make_state(
- NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE,
- true,
- &"",
- 10,
- )
- )
-
-
-func _validate_transit_estimation() -> void:
- assert(is_equal_approx(
- Player.resolve_local_prediction_transit_seconds(
- 10,
- 16,
- 1.0 / 30.0,
- 0.075,
- ),
- 0.075,
- ))
- # Six outstanding 30 Hz inputs span about 200 ms round trip. The fallback
- # must use only the approximately 100 ms one-way half of that gap.
- assert(is_equal_approx(
- Player.resolve_local_prediction_transit_seconds(
- 10,
- 16,
- 1.0 / 30.0,
- -1.0,
- ),
- 0.1,
- ))
- assert(is_equal_approx(
- Player.resolve_local_prediction_transit_seconds(
- 1,
- 100,
- 1.0 / 30.0,
- -1.0,
- ),
- Player.LOCAL_PREDICTION_EXTRAPOLATION_LIMIT_SECONDS,
- ))
-
-
-func _validate_remote_snapshot_smoothing(avatar: Player) -> void:
- avatar.configure_network_remote(false)
- var moving_snapshot: Dictionary = _network_snapshot(
- Vector3.ZERO,
- Vector3(4.5, 0.0, 0.0),
- 1,
- )
- avatar.push_network_snapshot(moving_snapshot, 0.1)
- assert(is_equal_approx(avatar.global_position.x, 0.45))
- assert(is_equal_approx(
- float(avatar.get("_network_target_position").x),
- 0.45,
- ))
- avatar.call("_update_network_interpolation", 0.12)
- var before_delayed_snapshot: Vector3 = avatar.global_position
- var delayed_snapshot: Dictionary = _network_snapshot(
- Vector3(0.54, 0.0, 0.0),
- Vector3(4.5, 0.0, 0.0),
- 2,
- )
- avatar.push_network_snapshot(delayed_snapshot, 0.1)
- # Snapshot receipt updates the target without teleporting a presented
- # remote avatar, even after a jittered packet interval.
- assert(avatar.global_position == before_delayed_snapshot)
- assert(float(avatar.get("_network_snapshot_jitter")) > 0.0)
- avatar.call("_update_network_interpolation", 1.0 / 60.0)
- assert(
- avatar.global_position.distance_to(before_delayed_snapshot) < 0.12
- )
- # Simulate a burst of dropped snapshots. Extrapolation must stop at the
- # tight limit instead of allowing the remote avatar to run indefinitely.
- for _step: int in 20:
- avatar.call("_update_network_interpolation", 1.0 / 30.0)
- assert(is_equal_approx(
- float(avatar.get("_network_snapshot_age")),
- Player.NETWORK_EXTRAPOLATION_LIMIT_SECONDS,
- ))
- var maximum_extrapolated_x: float = (
- 0.54
- + 4.5 * (0.1 + Player.NETWORK_EXTRAPOLATION_LIMIT_SECONDS)
- )
- assert(avatar.global_position.x <= maximum_extrapolated_x + 0.1)
-
- # Reconciliation only applies after both simulations report a grounded body.
- # Give the unit avatar a real floor so this exercises the production path.
- var floor := StaticBody3D.new()
- floor.collision_layer = 1
- floor.collision_mask = 0
- floor.position = Vector3(0.0, -0.1, 0.0)
- var floor_shape := CollisionShape3D.new()
- var floor_box := BoxShape3D.new()
- floor_box.size = Vector3(100.0, 0.2, 100.0)
- floor_shape.shape = floor_box
- floor.add_child(floor_shape)
- root.add_child(floor)
- await physics_frame
- avatar.set_local_control(true)
- avatar.global_position = Vector3(0.8, 0.0, 0.0)
- avatar.velocity = Vector3.DOWN
- avatar.move_and_slide()
- assert(avatar.is_on_floor())
- avatar.apply_local_prediction_correction(
- moving_snapshot,
- 2,
- 1.0 / 30.0,
- 0.1,
- 0.1,
- )
- # Ordinary host/client disagreement is expected while a packet is in flight.
- # It must never tug the locally controlled body or camera around.
- assert(is_equal_approx(avatar.global_position.x, 0.8))
- assert(
- int(avatar.get("_local_prediction_soft_corrections")) == 0
- )
- assert(
- int(avatar.get("_local_prediction_hard_corrections")) == 0
- )
-
- # A larger but still plausible mismatch must persist across multiple audits
- # before a small correction is allowed. Preserve both visible character and
- # camera positions while the collision body catches up.
- avatar.global_position = Vector3(3.0, 0.0, 0.0)
- var visual_position: Vector3 = avatar.get_node("Visuals").global_position
- var camera_position: Vector3 = avatar.get_node("CameraYaw").global_position
- var drift_snapshot: Dictionary = _network_snapshot(
- Vector3.ZERO,
- Vector3.ZERO,
- 20,
- )
- for _audit: int in 3:
- avatar.apply_local_prediction_correction(
- drift_snapshot,
- 20,
- 1.0 / 30.0,
- 0.0,
- 0.1,
- )
- assert(is_equal_approx(avatar.global_position.x, 3.0))
- avatar.apply_local_prediction_correction(
- drift_snapshot,
- 20,
- 1.0 / 30.0,
- 0.0,
- 0.1,
- )
- assert(avatar.global_position.x < 3.0)
- assert(avatar.global_position.x >= 2.85 - 0.001)
- assert(avatar.get_node("Visuals").global_position == visual_position)
- assert(avatar.get_node("CameraYaw").global_position == camera_position)
- assert(
- int(avatar.get("_local_prediction_soft_corrections")) == 1
- )
-
- # Host and client jump timing naturally differs. An airborne authoritative
- # snapshot must not drag the local capsule sideways or down before landing.
- avatar.global_position = Vector3(3.0, 0.0, 0.0)
- avatar.velocity = Vector3(0.0, 1.0, 0.0)
- var airborne_snapshot: Dictionary = drift_snapshot.duplicate(true)
- airborne_snapshot["grounded"] = false
- for _audit: int in 6:
- avatar.apply_local_prediction_correction(
- airborne_snapshot,
- 20,
- 1.0 / 30.0,
- 0.0,
- 0.1,
- )
- assert(avatar.global_position.is_equal_approx(Vector3(3.0, 0.0, 0.0)))
- assert(int(avatar.get("_local_prediction_soft_corrections")) == 1)
- avatar.velocity = Vector3.ZERO
-
- # The local body may reach its apex after the host has already landed. The
- # local airborne state still owns presentation and must not be pulled toward
- # the host until it has landed too.
- avatar.global_position = Vector3(3.0, 2.0, 0.0)
- avatar.velocity = Vector3.ZERO
- avatar.move_and_slide()
- assert(not avatar.is_on_floor())
- for _audit: int in 6:
- avatar.apply_local_prediction_correction(
- drift_snapshot,
- 20,
- 1.0 / 30.0,
- 0.0,
- 0.1,
- )
- assert(avatar.global_position.is_equal_approx(Vector3(3.0, 2.0, 0.0)))
- assert(int(avatar.get("_local_prediction_soft_corrections")) == 1)
-
- # Genuine divergence still recovers immediately, as do the separate reliable
- # teleport and water-recovery paths used by gameplay transitions.
- avatar.global_position = Vector3(10.0, 0.0, 0.0)
- avatar.apply_local_prediction_correction(
- drift_snapshot,
- 20,
- 1.0 / 30.0,
- 0.0,
- 0.1,
- )
- assert(avatar.global_position == Vector3.ZERO)
- assert(
- int(avatar.get("_local_prediction_hard_corrections")) == 1
- )
-
- # Soft reconciliation must respect the same terrain collision as ordinary
- # player movement instead of phasing the capsule through a solid obstacle.
- var blocker := StaticBody3D.new()
- blocker.collision_layer = 1
- blocker.collision_mask = 0
- blocker.position = Vector3(1.0, 1.0, 0.0)
- var blocker_shape := CollisionShape3D.new()
- var blocker_box := BoxShape3D.new()
- blocker_box.size = Vector3(0.2, 4.0, 4.0)
- blocker_shape.shape = blocker_box
- blocker.add_child(blocker_shape)
- root.add_child(blocker)
- await physics_frame
- avatar.global_position = Vector3.ZERO
- avatar.velocity = Vector3.ZERO
- avatar.call("_clear_local_reconciliation_offsets")
- var blocked_visual_position: Vector3 = (
- avatar.get_node("Visuals") as Node3D
- ).global_position
- var blocked_camera_position: Vector3 = (
- avatar.get_node("CameraYaw") as Node3D
- ).global_position
- assert(bool(avatar.call(
- "_apply_camera_safe_local_correction",
- Vector3(2.0, 0.0, 0.0),
- )))
- assert(avatar.global_position.x > 0.0 and avatar.global_position.x < 0.6)
- assert(
- (avatar.get_node("Visuals") as Node3D).global_position
- == blocked_visual_position
- )
- assert(
- (avatar.get_node("CameraYaw") as Node3D).global_position
- == blocked_camera_position
- )
- blocker.queue_free()
- floor.queue_free()
- await physics_frame
-
-
-func _validate_remote_locomotion_playback_recovery(avatar: Player) -> void:
- avatar.configure_network_remote(false)
- avatar.push_network_snapshot(
- _network_snapshot(Vector3.ZERO, Vector3(4.5, 0.0, 0.0), 21)
- )
- avatar.call("_update_character_animation")
- var animation_player := avatar.get_node(
- "Visuals/CharacterRig/AnimationPlayer"
- ) as AnimationPlayer
- assert(animation_player.assigned_animation == &"running")
- assert(animation_player.is_playing())
-
- # Reproduce issue #111: locomotion replication still says RUNNING (and the
- # dust therefore still emits), but the rig playback has fallen idle. The
- # local presentation pass must repair that state without another packet.
- animation_player.stop()
- assert(not animation_player.is_playing())
- assert(
- int(avatar.get("_network_target_locomotion_state"))
- == Player.LocomotionState.RUNNING
- )
- avatar.call("_update_character_animation")
- assert(animation_player.assigned_animation == &"running")
- assert(animation_player.is_playing())
-
-
-func _validate_reliable_jump_intent(avatar: Player) -> void:
- avatar.set_local_control(true)
- avatar.call("_queue_local_network_jump_intent")
- var first: Dictionary = avatar.capture_network_input(20)
- var repeated: Dictionary = avatar.capture_network_input(21)
- assert(bool(first["jump"]))
- assert(bool(repeated["jump"]))
- assert(int(avatar.get("_local_network_jump_intent_sequence")) == 20)
- avatar.apply_local_prediction_correction(
- _network_snapshot(avatar.global_position, Vector3.ZERO, 20),
- 20,
- 1.0 / 30.0,
- 0.0,
- 0.1,
- )
- assert(not bool(avatar.capture_network_input(22)["jump"]))
-
- avatar.configure_network_remote(true)
- var first_host_jump: Dictionary = _movement_input(30, false, false)
- first_host_jump["jump"] = true
- avatar.apply_authoritative_network_input(first_host_jump)
- assert(bool(avatar.get("_network_jump_pending")))
- avatar.set("_network_jump_pending", false)
- var repeated_host_jump: Dictionary = _movement_input(31, false, false)
- repeated_host_jump["jump"] = true
- avatar.apply_authoritative_network_input(repeated_host_jump)
- assert(not bool(avatar.get("_network_jump_pending")))
- avatar.apply_authoritative_network_input(
- _movement_input(32, false, false)
- )
- var next_host_jump: Dictionary = _movement_input(33, false, false)
- next_host_jump["jump"] = true
- avatar.apply_authoritative_network_input(next_host_jump)
- assert(bool(avatar.get("_network_jump_pending")))
- avatar.reset_network_movement_state()
- assert(not bool(avatar.get("_network_jump_pending")))
- assert(not bool(avatar.get("_local_network_jump_intent_pending")))
- assert(int(avatar.get("_last_network_input_sequence")) == 0)
-
-
-func _validate_stale_input_expiry(avatar: Player) -> void:
- avatar.configure_network_remote(true)
- var high_latency_timeout := (
- Player.resolve_network_input_stale_timeout_seconds(400)
- )
- assert(high_latency_timeout > Player.NETWORK_INPUT_STALE_TIMEOUT_SECONDS)
- avatar.apply_authoritative_network_input(
- _movement_input(40, true),
- high_latency_timeout,
- )
- assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
- avatar.call(
- "_update_network_input_freshness",
- Player.NETWORK_INPUT_STALE_TIMEOUT_SECONDS + 0.01,
- )
- assert(not bool(avatar.get("_network_input_stale")))
- avatar.call(
- "_update_network_input_freshness",
- high_latency_timeout,
- )
- assert((avatar.get("_network_axis") as Vector2) == Vector2.ZERO)
- assert(not bool(avatar.get("_network_sprint")))
- assert(bool(avatar.get("_network_input_stale")))
- avatar.apply_authoritative_network_input(_movement_input(41, false))
- assert(not bool(avatar.get("_network_input_stale")))
- assert((avatar.get("_network_axis") as Vector2).length_squared() > 0.0)
-
-
-func _network_snapshot(
- position: Vector3,
- velocity: Vector3,
- acknowledged_input: int,
-) -> Dictionary:
- return {
- "peer_id": 2,
- "acknowledged_input": acknowledged_input,
- "position": [position.x, position.y, position.z],
- "velocity": [velocity.x, velocity.y, velocity.z],
- "visual_yaw": 0.0,
- "grounded": true,
- "animation_state": NetworkPlayerAnimationProtocol.make_state(
- NetworkPlayerAnimationProtocol.LOCOMOTION_RUNNING,
- true,
- ),
- "sitting": false,
- "casting": false,
- }
-
-
-func _run_host() -> void:
- var main: Node = await _create_initialized_main()
- var session := main.get_node("%NetworkSession") as NetworkSession
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(bool(main.call(
- "_apply_world",
- WorldLayout.STARTER_ISLAND,
- NetworkProtocol.DEFAULT_WORLD_SEED,
- true,
- )))
- assert(bool(main.call(
- "_prepare_host_world",
- WorldLayout.STARTER_ISLAND,
- NetworkProtocol.DEFAULT_WORLD_SEED,
- )))
- assert(save_manager.initialize_new_game(
- NetworkProtocol.DEFAULT_WORLD_SEED,
- WorldLayout.STARTER_ISLAND,
- ))
- assert(session.start_private_host(TEST_PORT))
- main.call("_enter_gameplay")
- for _frame: int in 4:
- await physics_frame
- assert(session.set_host_open(true))
- var remote_peer_id: int = await _wait_for_remote_peer(session)
- assert(remote_peer_id > 1)
- # Authentication completes before the client has necessarily created and
- # begun observing the host avatar. Give its gameplay scene a bounded moment
- # to settle before emitting the one-shot locomotion/action sequence.
- await create_timer(3.0).timeout
- var player := main.get("_player") as Player
- var world := main.get_node("TestWorld") as TestWorld
- player.configure_network_remote(true)
- await _reset_locomotion_test_position(player, session, world)
- var input_sequence: int = 1
- input_sequence = await _sustain_locomotion_input(
- player, input_sequence, 1.5, true
- )
- await _reset_locomotion_test_position(player, session, world)
- input_sequence = await _sustain_locomotion_input(
- player, input_sequence, 1.5, false
- )
- await _reset_locomotion_test_position(player, session, world)
- input_sequence = await _sustain_locomotion_input(
- player, input_sequence, 1.5, false, true, true
- )
- input_sequence = await _sustain_locomotion_input(
- player, input_sequence, 1.5, false, false, true
- )
- player.apply_authoritative_network_input(
- _movement_input(input_sequence, false, false, true, &"draw", 1)
- )
- input_sequence += 1
- await create_timer(1.0).timeout
- player.apply_authoritative_network_input(
- _movement_input(
- input_sequence, false, false, true, &"strike", 2, true
- )
- )
- input_sequence += 1
- await create_timer(2.0).timeout
- player.apply_authoritative_network_input(
- _movement_input(input_sequence, false, false, false, &"", 3)
- )
- var disconnect_deadline: int = Time.get_ticks_msec() + 15000
- while (
- Time.get_ticks_msec() < disconnect_deadline
- and session.is_authenticated_peer(remote_peer_id)
- ):
- await process_frame
- assert(not session.is_authenticated_peer(remote_peer_id))
- print("Movement multiplayer host validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _run_client() -> void:
- var main: Node = await _create_initialized_main()
- main.call(
- "_on_title_join_game_requested",
- "127.0.0.1:%d" % TEST_PORT,
- )
- var session := main.get_node("%NetworkSession") as NetworkSession
- var join_deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < join_deadline:
- await process_frame
- if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
- main.call("_confirm_server_trust")
- if session.is_joined_client() and bool(main.get("_gameplay_started")):
- break
- assert(session.is_joined_client())
- var spawn_service := main.get_node(
- "%PlayerSpawnService"
- ) as PlayerSpawnService
- var host_avatar: Player = await _wait_for_avatar(spawn_service, 1)
- assert(host_avatar != null)
- var animation_player := host_avatar.get_node(
- "Visuals/CharacterRig/AnimationPlayer"
- ) as AnimationPlayer
- var sprint_dust := host_avatar.get_node("%SprintDust") as SprintDustTrail
- var saw_running: bool = false
- var saw_walking: bool = false
- var saw_sneaking: bool = false
- var saw_idle_sneak: bool = false
- var saw_net_draw: bool = false
- var saw_net_strike: bool = false
- var saw_net_strike_paused: bool = false
- var saw_animation_advance: bool = false
- var saw_dust: bool = false
- var observed_animations: Dictionary = {}
- var observed_locomotion_states: Dictionary = {}
- var maximum_sneak_speed: float = 0.0
- var previous_animation: StringName = &""
- var previous_animation_position: float = -1.0
- var observation_deadline: int = Time.get_ticks_msec() + 22000
- while Time.get_ticks_msec() < observation_deadline:
- await process_frame
- var current_animation: StringName = animation_player.current_animation
- observed_animations[current_animation] = true
- var target_locomotion_state: int = int(host_avatar.get(
- "_network_target_locomotion_state"
- ))
- observed_locomotion_states[target_locomotion_state] = true
- if target_locomotion_state == Player.LocomotionState.SNEAKING:
- maximum_sneak_speed = maxf(
- maximum_sneak_speed,
- Vector2(
- host_avatar.velocity.x, host_avatar.velocity.z
- ).length(),
- )
- saw_running = saw_running or current_animation.begins_with("running")
- saw_walking = saw_walking or current_animation.begins_with("walking")
- saw_sneaking = saw_sneaking or current_animation == &"sneaking"
- saw_idle_sneak = saw_idle_sneak or current_animation == &"idle_sneak"
- saw_net_draw = saw_net_draw or current_animation == &"draw"
- saw_net_strike = (
- saw_net_strike
- or current_animation == &"strike"
- or animation_player.assigned_animation == &"strike"
- )
- saw_net_strike_paused = (
- saw_net_strike_paused
- or bool(host_avatar.get(
- "_network_target_animation_action_paused"
- ))
- )
- var current_position: float = (
- animation_player.current_animation_position
- )
- if (
- current_animation == previous_animation
- and previous_animation_position >= 0.0
- and absf(current_position - previous_animation_position) > 0.001
- ):
- saw_animation_advance = true
- previous_animation = current_animation
- previous_animation_position = current_position
- saw_dust = saw_dust or sprint_dust.get_active_puff_count() > 0
- if (
- saw_running
- and saw_walking
- and saw_sneaking
- and saw_idle_sneak
- and saw_net_draw
- and saw_net_strike
- and saw_net_strike_paused
- and saw_animation_advance
- and saw_dust
- ):
- break
- var missing_observations: Array[String] = []
- if not saw_running:
- missing_observations.append("running")
- if not saw_walking:
- missing_observations.append("walking")
- if not saw_sneaking:
- missing_observations.append("sneaking")
- if not saw_idle_sneak:
- missing_observations.append("idle_sneak")
- if not saw_net_draw:
- missing_observations.append("net_draw")
- if not saw_net_strike:
- missing_observations.append("net_strike")
- if not saw_net_strike_paused:
- missing_observations.append("net_strike_paused")
- if not saw_animation_advance:
- missing_observations.append("animation_advance")
- if not saw_dust:
- missing_observations.append("sprint_dust")
- if not missing_observations.is_empty():
- push_error(
- (
- "Missing movement observations %s; observed animations: %s; "
- + "locomotion states: %s; maximum sneak speed: %.3f"
- ) % [
- missing_observations,
- observed_animations.keys(),
- observed_locomotion_states.keys(),
- maximum_sneak_speed,
- ]
- )
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit(1)
- return
- print("Movement multiplayer client validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _movement_input(
- sequence: int,
- sprinting: bool,
- moving: bool = true,
- sneaking: bool = false,
- action_id: StringName = &"",
- action_sequence: int = 0,
- action_paused: bool = false,
-) -> Dictionary:
- return {
- "sequence": sequence,
- "axis": [0.0, -1.0] if moving else [0.0, 0.0],
- "camera_yaw": 0.0,
- "jump": false,
- "sprint": sprinting,
- "sneak": sneaking,
- "slow_walk": false,
- "sitting": false,
- "casting": false,
- "animation_action": NetworkPlayerAnimationProtocol.make_action_state(
- action_id,
- action_sequence,
- 0.0,
- action_paused,
- ),
- }
-
-
-func _sustain_locomotion_input(
- player: Player,
- starting_sequence: int,
- duration_seconds: float,
- sprinting: bool,
- moving: bool = true,
- sneaking: bool = false,
-) -> int:
- var sequence: int = starting_sequence
- var deadline: int = (
- Time.get_ticks_msec() + roundi(duration_seconds * 1000.0)
- )
- while Time.get_ticks_msec() < deadline:
- player.apply_authoritative_network_input(
- _movement_input(sequence, sprinting, moving, sneaking)
- )
- sequence += 1
- await create_timer(0.1).timeout
- return sequence
-
-
-func _reset_locomotion_test_position(
- player: Player,
- session: NetworkSession,
- world: TestWorld,
-) -> void:
- player.velocity = Vector3.ZERO
- player.global_transform = world.get_player_spawn_transform()
- session.publish_authoritative_teleport(1)
- for _frame: int in 4:
- await physics_frame
-
-
-func _create_initialized_main() -> Node:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- return main
-
-
-func _wait_for_remote_peer(session: NetworkSession) -> int:
- var deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- for peer_id: int in session.get_authenticated_peer_ids():
- if peer_id != session.get_local_peer_id():
- return peer_id
- return 0
-
-
-func _wait_for_avatar(
- spawn_service: PlayerSpawnService,
- peer_id: int,
-) -> Player:
- var deadline: int = Time.get_ticks_msec() + 8000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- var avatar := spawn_service.get_avatar(peer_id) as Player
- if avatar != null:
- return avatar
- return null
diff --git a/tests/movement_multiplayer_validation.gd.uid b/tests/movement_multiplayer_validation.gd.uid
deleted file mode 100644
index 4d8c726..0000000
--- a/tests/movement_multiplayer_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dlpu5lwb2pges
diff --git a/tests/net_attachment_validation.gd b/tests/net_attachment_validation.gd
deleted file mode 100644
index 6b53c08..0000000
--- a/tests/net_attachment_validation.gd
+++ /dev/null
@@ -1,163 +0,0 @@
-extends SceneTree
-
-const NetAttachmentScene: PackedScene = preload(
- "res://player/net_attachment.tscn"
-)
-const FishingRodAttachmentScene: PackedScene = preload(
- "res://player/fishing_rod_attachment.tscn"
-)
-
-var failures: Array[String] = []
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var host_skeleton := Skeleton3D.new()
- host_skeleton.name = "HostSkeleton"
- host_skeleton.add_bone("rod_socket")
- host_skeleton.add_bone("rod_socket.001")
- host_skeleton.set_bone_parent(1, 0)
- host_skeleton.set_bone_rest(
- 1,
- Transform3D(
- Basis(Quaternion(Vector3.UP, Vector3.RIGHT)),
- Vector3(0.0, 0.25, 0.0),
- ),
- )
- root.add_child(host_skeleton)
-
- var attachment := NetAttachmentScene.instantiate() as BoneAttachment3D
- host_skeleton.add_child(attachment)
- await process_frame
- await process_frame
-
- _check(
- attachment.bone_name == "rod_socket",
- "Attachment moved off rod_socket.",
- )
- var catching_net := attachment.get_node_or_null("CatchingNet") as Node3D
- _check(catching_net != null, "CatchingNet presentation root is missing.")
- if catching_net == null:
- _finish(host_skeleton)
- return
- _check(not catching_net.visible, "Net must start hidden.")
- _check(
- catching_net.position.is_zero_approx(),
- "Net direction alignment moved its hand anchor.",
- )
- _check(
- catching_net.basis.y.is_equal_approx(Vector3.UP),
- "Net working axis no longer follows rod_socket.",
- )
- _check(
- catching_net.basis.z.is_equal_approx(Vector3.RIGHT),
- "Net rear face did not follow the socket direction guide.",
- )
-
- var model := catching_net.get_node_or_null("ModelMount/NetModel") as Node3D
- _check(model != null, "Imported net model is missing.")
- if model == null:
- _finish(host_skeleton)
- return
- var skeleton := model.find_child("Skeleton3D", true, false) as Skeleton3D
- _check(skeleton != null, "Imported net Skeleton3D is missing.")
- if skeleton == null:
- _finish(host_skeleton)
- return
- _check(
- skeleton.find_bone("net_root") >= 0,
- "Imported net_root bone is missing.",
- )
- _check(
- skeleton.find_bone("net_rim") >= 0,
- "Imported net_rim bone is missing.",
- )
- _check(
- skeleton.find_bone("net_mid") >= 0,
- "Imported net_mid bone is missing.",
- )
- _check(
- skeleton.find_bone("net_tip") >= 0,
- "Imported net_tip bone is missing.",
- )
-
- var spring := skeleton.get_node_or_null("NetSpring") as SpringBoneSimulator3D
- _check(spring != null, "Net spring simulator was not created.")
- if spring != null:
- _check(spring.get_setting_count() == 1, "Net spring settings changed.")
- _check(
- spring.get_root_bone_name(0) == &"net_mid",
- "Net spring root must remain net_mid.",
- )
- _check(
- spring.get_end_bone_name(0) == &"net_tip",
- "Net spring end must remain net_tip.",
- )
- _check(not spring.active, "Hidden net spring must be suspended.")
- catching_net.visible = true
- await process_frame
- await process_frame
- _check(spring.active, "Visible net spring must be active.")
- _check(spring.get_joint_count(0) == 2, "Net spring chain is incomplete.")
- _check(
- not attachment.get_impact_world_position().is_equal_approx(
- catching_net.global_position
- ),
- "Net impact position did not follow the authored rim bone.",
- )
- catching_net.visible = false
- await process_frame
- _check(not spring.active, "Hidden net spring did not suspend.")
-
- for child: Node in model.find_children("*", "MeshInstance3D", true, false):
- var mesh_instance := child as MeshInstance3D
- if mesh_instance.mesh == null:
- continue
- for surface_index: int in mesh_instance.mesh.get_surface_count():
- var material := mesh_instance.mesh.surface_get_material(surface_index)
- if material is BaseMaterial3D:
- _check(
- (material as BaseMaterial3D).texture_filter
- == BaseMaterial3D.TEXTURE_FILTER_NEAREST,
- "Net material does not use nearest texture sampling.",
- )
-
- var rod_attachment := (
- FishingRodAttachmentScene.instantiate() as BoneAttachment3D
- )
- host_skeleton.add_child(rod_attachment)
- await process_frame
- var fishing_rod := rod_attachment.get_node("FishingRod") as Node3D
- _check(
- fishing_rod.position.is_zero_approx(),
- "Rod direction alignment moved its hand anchor.",
- )
- _check(
- fishing_rod.basis.y.is_equal_approx(Vector3.UP),
- "Rod working axis no longer follows rod_socket.",
- )
- _check(
- fishing_rod.basis.z.is_equal_approx(Vector3.RIGHT),
- "Rod rear face did not follow the socket direction guide.",
- )
-
- _finish(host_skeleton)
-
-
-func _finish(host_skeleton: Skeleton3D) -> void:
- host_skeleton.queue_free()
- if failures.is_empty():
- print("Net attachment validation: PASS")
- quit(0)
- return
- for failure: String in failures:
- printerr("Net attachment validation: ", failure)
- quit(1)
-
-
-func _check(condition: bool, message: String) -> void:
- if not condition:
- failures.append(message)
diff --git a/tests/net_attachment_validation.gd.uid b/tests/net_attachment_validation.gd.uid
deleted file mode 100644
index 5f968d1..0000000
--- a/tests/net_attachment_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://clnr8ofu1fqw5
diff --git a/tests/network_chat_protocol_validation.gd b/tests/network_chat_protocol_validation.gd
deleted file mode 100644
index 9368189..0000000
--- a/tests/network_chat_protocol_validation.gd
+++ /dev/null
@@ -1,55 +0,0 @@
-extends SceneTree
-
-const EDGE_PADDING: String = " \t\n ⠀"
-
-
-func _initialize() -> void:
- call_deferred("_validate_protocol")
-
-
-func _validate_protocol() -> void:
- _validate_type_and_empty_handling()
- _validate_message_cleanup()
- _validate_protocol_limits()
- print("Network chat protocol validation: PASS")
- quit()
-
-
-func _validate_type_and_empty_handling() -> void:
- assert(NetworkChatProtocol.sanitize_body(null) == "")
- assert(NetworkChatProtocol.sanitize_body(42) == "")
- assert(NetworkChatProtocol.sanitize_body(EDGE_PADDING) == "")
- assert(NetworkChatProtocol.sanitize_body("\u0001") == "")
-
-
-func _validate_message_cleanup() -> void:
- assert(NetworkChatProtocol.sanitize_body(" hello ") == "hello")
- assert(NetworkChatProtocol.sanitize_body("a\tb\nc") == "a b c")
- assert(NetworkChatProtocol.sanitize_body("word word") == "word word")
- assert(NetworkChatProtocol.sanitize_body(" !!! ") == "!!!")
- assert(
- NetworkChatProtocol.sanitize_body(
- EDGE_PADDING + "visible" + EDGE_PADDING
- ) == "visible"
- )
-
-
-func _validate_protocol_limits() -> void:
- var maximum_body: String = "a".repeat(
- NetworkChatProtocol.MAX_VISIBLE_CHARACTERS
- )
- assert(NetworkChatProtocol.sanitize_body(maximum_body) == maximum_body)
- assert(
- NetworkChatProtocol.sanitize_body(maximum_body + "a") == ""
- )
- var maximum_utf8_body: String = "🐟".repeat(
- NetworkChatProtocol.MAX_VISIBLE_CHARACTERS
- )
- assert(
- maximum_utf8_body.to_utf8_buffer().size()
- == NetworkChatProtocol.MAX_UTF8_BYTES
- )
- assert(
- NetworkChatProtocol.sanitize_body(maximum_utf8_body)
- == maximum_utf8_body
- )
diff --git a/tests/network_chat_protocol_validation.gd.uid b/tests/network_chat_protocol_validation.gd.uid
deleted file mode 100644
index 5e98b42..0000000
--- a/tests/network_chat_protocol_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bj7nq2k6q5c7f
diff --git a/tests/network_player_animation_protocol_validation.gd b/tests/network_player_animation_protocol_validation.gd
deleted file mode 100644
index 66a3ec3..0000000
--- a/tests/network_player_animation_protocol_validation.gd
+++ /dev/null
@@ -1,65 +0,0 @@
-extends SceneTree
-
-
-func _initialize() -> void:
- var idle := NetworkPlayerAnimationProtocol.make_state(
- NetworkPlayerAnimationProtocol.LOCOMOTION_IDLE,
- true,
- )
- assert(NetworkPlayerAnimationProtocol.validate_state(idle))
- var emote := NetworkPlayerAnimationProtocol.make_state(
- NetworkPlayerAnimationProtocol.LOCOMOTION_RUNNING,
- false,
- &"wave_hello",
- 17,
- 1.25,
- )
- assert(NetworkPlayerAnimationProtocol.validate_state(emote))
- var paused_action := NetworkPlayerAnimationProtocol.make_action_state(
- &"strike",
- 18,
- 0.72,
- true,
- )
- assert(NetworkPlayerAnimationProtocol.validate_action_state(paused_action))
- assert(bool(paused_action["paused"]))
- var future_locomotion := NetworkPlayerAnimationProtocol.make_state(
- &"swimming_fast",
- false,
- )
- assert(NetworkPlayerAnimationProtocol.validate_state(future_locomotion))
- var sneaking := NetworkPlayerAnimationProtocol.make_state(
- NetworkPlayerAnimationProtocol.LOCOMOTION_SNEAKING,
- true,
- )
- assert(NetworkPlayerAnimationProtocol.validate_state(sneaking))
- var extra_future_field: Dictionary = emote.duplicate(true)
- extra_future_field["future_layer"] = {"id": "umbrella"}
- assert(NetworkPlayerAnimationProtocol.validate_state(extra_future_field))
-
- var invalid_action: Dictionary = emote.duplicate(true)
- invalid_action["action"] = {
- "id": "../unsafe",
- "sequence": 17,
- "elapsed": 1.25,
- }
- assert(not NetworkPlayerAnimationProtocol.validate_state(invalid_action))
- var invalid_sequence: Dictionary = emote.duplicate(true)
- invalid_sequence["action"] = {
- "id": "wave_hello",
- "sequence": -1,
- "elapsed": 1.25,
- }
- assert(not NetworkPlayerAnimationProtocol.validate_state(invalid_sequence))
- var invalid_elapsed: Dictionary = emote.duplicate(true)
- invalid_elapsed["action"] = {
- "id": "wave_hello",
- "sequence": 17,
- "elapsed": INF,
- }
- assert(not NetworkPlayerAnimationProtocol.validate_state(invalid_elapsed))
- var invalid_paused: Dictionary = emote.duplicate(true)
- invalid_paused["action"]["paused"] = "yes"
- assert(not NetworkPlayerAnimationProtocol.validate_state(invalid_paused))
- print("Network player animation protocol validation: PASS")
- quit()
diff --git a/tests/network_player_animation_protocol_validation.gd.uid b/tests/network_player_animation_protocol_validation.gd.uid
deleted file mode 100644
index 017146d..0000000
--- a/tests/network_player_animation_protocol_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cjgelp62dkykn
diff --git a/tests/on_screen_keyboard_validation.gd b/tests/on_screen_keyboard_validation.gd
deleted file mode 100644
index a685c65..0000000
--- a/tests/on_screen_keyboard_validation.gd
+++ /dev/null
@@ -1,360 +0,0 @@
-extends SceneTree
-
-const KeyboardType = preload("res://ui/on_screen_keyboard.gd")
-const ControllerMappingManagerType = preload(
- "res://settings/controller_mapping_manager.gd"
-)
-const SettingsManagerType = preload(
- "res://settings/player_settings_manager.gd"
-)
-
-
-class ControllerInputProbe:
- extends Node
-
- var pressed_buttons: Array[int] = []
-
-
- func _input(event: InputEvent) -> void:
- var button := event as InputEventJoypadButton
- if button != null and button.pressed:
- pressed_buttons.append(button.button_index)
-
-
-class NativeKeyboardProbe:
- extends OnScreenKeyboard
- var shown_controls: Array[Control] = []
- var hide_count: int = 0
-
-
- func _uses_native_virtual_keyboard() -> bool:
- return true
-
-
- func _show_native_keyboard_for(control: Control) -> void:
- shown_controls.append(control)
-
-
- func _hide_native_keyboard() -> void:
- hide_count += 1
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- _validate_default_and_persistence()
- await _validate_keyboard_entry()
- await _validate_automapped_face_buttons()
- await _validate_native_keyboard()
- print("On-screen keyboard validation: PASS")
- quit()
-
-
-func _validate_default_and_persistence() -> void:
- var defaults := PlayerSettings.new()
- assert(not defaults.on_screen_keyboard_enabled)
- assert(not defaults.swap_hotbar_camera_scroll)
- assert(
- KeyboardType.should_enable_for_controller(false, false, "Linux")
- )
- assert(
- KeyboardType.should_enable_for_controller(false, true, "Linux")
- )
- assert(
- not KeyboardType.should_enable_for_controller(false, true, "Android")
- )
- assert(
- KeyboardType.should_enable_for_controller(true, true, "Android")
- )
- assert(
- KeyboardType.should_use_native_virtual_keyboard(
- false, true, "Android"
- )
- )
- assert(
- not KeyboardType.should_use_native_virtual_keyboard(
- true, true, "Android"
- )
- )
- assert(
- not KeyboardType.should_use_native_virtual_keyboard(
- false, true, "Linux"
- )
- )
- var manager := SettingsManagerType.new()
- root.add_child(manager)
- assert(manager.load_settings())
- var edited: PlayerSettings = manager.current_settings.copy()
- edited.on_screen_keyboard_enabled = true
- edited.swap_hotbar_camera_scroll = true
- assert(manager.apply_settings(edited))
- var reloaded := SettingsManagerType.new()
- root.add_child(reloaded)
- assert(reloaded.load_settings())
- assert(reloaded.current_settings.on_screen_keyboard_enabled)
- assert(reloaded.current_settings.swap_hotbar_camera_scroll)
- manager.queue_free()
- reloaded.queue_free()
-
-
-func _validate_keyboard_entry() -> void:
- var host := Control.new()
- root.add_child(host)
- var edit := LineEdit.new()
- edit.text = "seed"
- edit.caret_column = 0
- edit.focus_mode = Control.FOCUS_ALL
- host.add_child(edit)
- var keyboard := KeyboardType.new()
- host.add_child(keyboard)
- await process_frame
- keyboard.set_enabled(false)
- assert(keyboard.is_enabled())
- var activate_event := InputEventJoypadButton.new()
- activate_event.button_index = JOY_BUTTON_A
- activate_event.pressed = true
- if keyboard.is_enabled():
- edit.grab_focus()
- keyboard.call("_input", activate_event)
- assert(keyboard.is_open())
- keyboard.call("_close_keyboard", true)
- assert(not keyboard.is_open())
- keyboard.set_enabled(true)
- edit.grab_focus()
- keyboard.call("_input", activate_event)
- assert(keyboard.is_open())
- assert(edit.caret_column == edit.text.length())
- assert(keyboard.get("_buffer_caret") == edit.text.length())
- assert(
- str(
- (keyboard.get("_preview") as RichTextLabel).get_parsed_text()
- ).begins_with("seed")
- )
- var left_page_event := InputEventJoypadButton.new()
- left_page_event.button_index = JOY_BUTTON_LEFT_SHOULDER
- left_page_event.pressed = true
- keyboard.call("_input", left_page_event)
- assert(keyboard.is_open())
- assert(keyboard.get("_page") == KeyboardType.Page.SYMBOLS)
- var right_page_event := InputEventJoypadButton.new()
- right_page_event.button_index = JOY_BUTTON_RIGHT_SHOULDER
- right_page_event.pressed = true
- keyboard.call("_input", right_page_event)
- assert(keyboard.is_open())
- assert(keyboard.get("_page") == KeyboardType.Page.LOWER)
- for _frame: int in 2:
- await process_frame
- var focused_key := root.gui_get_focus_owner() as Button
- assert(focused_key != null and focused_key.text == "q")
- var second_key := keyboard.get("_key_buttons")[1] as Button
- second_key.grab_focus()
- var outside_button := Button.new()
- host.add_child(outside_button)
- outside_button.grab_focus()
- assert(root.gui_get_focus_owner() == outside_button)
- keyboard.call("_process", 0.0)
- assert(root.gui_get_focus_owner() == second_key)
- var conflicting_cancel_binding := InputEventJoypadButton.new()
- conflicting_cancel_binding.button_index = JOY_BUTTON_A
- InputMap.action_add_event(&"ui_cancel", conflicting_cancel_binding)
- keyboard.call("_input", activate_event)
- InputMap.action_erase_event(&"ui_cancel", conflicting_cancel_binding)
- assert(keyboard.is_open())
- assert(edit.text == "seedw")
- var left_trigger := InputEventJoypadMotion.new()
- left_trigger.axis = JOY_AXIS_TRIGGER_LEFT
- left_trigger.axis_value = 1.0
- keyboard.call("_input", left_trigger)
- assert(keyboard.get("_buffer_caret") == edit.text.length() - 1)
- var right_trigger := InputEventJoypadMotion.new()
- right_trigger.axis = JOY_AXIS_TRIGGER_RIGHT
- right_trigger.axis_value = 1.0
- keyboard.call("_input", right_trigger)
- assert(keyboard.get("_buffer_caret") == edit.text.length())
- var preview := keyboard.get("_preview") as RichTextLabel
- var preview_height_before_blink: float = preview.get_content_height()
- var preview_text_before_blink: String = preview.get_parsed_text()
- keyboard.call("_process", KeyboardType.CARET_BLINK_INTERVAL)
- assert(
- not bool(keyboard.get("_caret_visible"))
- )
- assert(preview.get_parsed_text() == preview_text_before_blink)
- assert(is_equal_approx(preview.get_content_height(), preview_height_before_blink))
- keyboard.call("_type_character", "a")
- keyboard.call("_type_space")
- keyboard.call("_set_page", KeyboardType.Page.UPPER)
- keyboard.call("_type_character", "B")
- assert(edit.text == "seedwa B")
- keyboard.call("_move_caret", -1)
- keyboard.call("_type_character", "C")
- assert(edit.text == "seedwa CB")
- var input_probe := ControllerInputProbe.new()
- root.add_child(input_probe)
- root.move_child(input_probe, 0)
- var backspace_event := InputEventJoypadButton.new()
- backspace_event.button_index = JOY_BUTTON_X
- backspace_event.pressed = true
- Input.parse_input_event(backspace_event)
- await process_frame
- assert(edit.text == "seedwa B")
- assert(input_probe.pressed_buttons.is_empty())
- var close_event := InputEventJoypadButton.new()
- close_event.button_index = JOY_BUTTON_B
- close_event.pressed = true
- keyboard.call("_input", close_event)
- assert(not keyboard.is_open())
- assert(root.gui_get_focus_owner() == edit)
- input_probe.queue_free()
- edit.grab_focus()
- keyboard.call("_input", activate_event)
- assert(keyboard.is_open())
- var submitted: Array[String] = []
- edit.text_submitted.connect(func(value: String) -> void:
- submitted.append(value)
- )
- keyboard.call("_submit")
- assert(not keyboard.is_open())
- assert(submitted == ["seedwa B"])
- var letter_body := TextEdit.new()
- letter_body.text = "letter"
- letter_body.focus_mode = Control.FOCUS_ALL
- host.add_child(letter_body)
- letter_body.text_changed.connect(func() -> void:
- outside_button.call_deferred("grab_focus")
- )
- letter_body.grab_focus()
- keyboard.call("_input", activate_event)
- assert(keyboard.is_open())
- for _frame: int in 2:
- await process_frame
- var letter_key := keyboard.get("_key_buttons")[2] as Button
- letter_key.grab_focus()
- keyboard.call("_input", activate_event)
- for _frame: int in 2:
- await process_frame
- assert(letter_body.text == "lettere")
- assert(root.gui_get_focus_owner() == letter_key)
- keyboard.call("_input", close_event)
- assert(not keyboard.is_open())
- host.queue_free()
-
-
-func _validate_automapped_face_buttons() -> void:
- var manager := ControllerMappingManagerType.new()
- root.add_child(manager)
- await process_frame
- var swapped_bindings: Dictionary = (
- ControllerMappingManagerType.default_bindings()
- )
- swapped_bindings[str(ControllerMappingManagerType.ROLE_A)] = {
- "kind": "button",
- "button": int(JOY_BUTTON_B),
- }
- swapped_bindings[str(ControllerMappingManagerType.ROLE_B)] = {
- "kind": "button",
- "button": int(JOY_BUTTON_A),
- }
- manager._active_profile_key = "keyboard-mapping-test"
- manager._profiles[manager._active_profile_key] = {
- "controller_name": "keyboard mapping test",
- "bindings": swapped_bindings,
- }
- var host := Control.new()
- root.add_child(host)
- var edit := LineEdit.new()
- edit.focus_mode = Control.FOCUS_ALL
- host.add_child(edit)
- var keyboard := KeyboardType.new()
- host.add_child(keyboard)
- keyboard.setup_controller_mapping(manager)
- keyboard.set_enabled(true)
- await process_frame
- var mapped_accept := InputEventJoypadButton.new()
- mapped_accept.device = manager.get_active_device_id()
- mapped_accept.button_index = JOY_BUTTON_B
- mapped_accept.pressed = true
- edit.grab_focus()
- keyboard.call("_input", mapped_accept)
- assert(keyboard.is_open())
- for _frame: int in 2:
- await process_frame
- keyboard.call("_input", mapped_accept)
- assert(keyboard.is_open())
- assert(edit.text == "q")
- var mapped_cancel := InputEventJoypadButton.new()
- mapped_cancel.device = manager.get_active_device_id()
- mapped_cancel.button_index = JOY_BUTTON_A
- mapped_cancel.pressed = true
- keyboard.call("_input", mapped_cancel)
- assert(not keyboard.is_open())
- host.queue_free()
- manager.queue_free()
-
-
-func _validate_native_keyboard() -> void:
- var host := Control.new()
- root.add_child(host)
- var edit := LineEdit.new()
- edit.focus_mode = Control.FOCUS_ALL
- edit.virtual_keyboard_enabled = true
- host.add_child(edit)
- var outside_button := Button.new()
- host.add_child(outside_button)
- var keyboard := NativeKeyboardProbe.new()
- host.add_child(keyboard)
- await process_frame
- edit.grab_focus()
- assert(keyboard.is_open())
- var focus_exit_count: int = 0
- edit.focus_exited.connect(func() -> void:
- focus_exit_count += 1
- )
- assert(keyboard.request_for_control(edit))
- assert(edit.has_focus())
- assert(focus_exit_count == 0)
- assert(not edit.virtual_keyboard_enabled)
- assert(not edit.virtual_keyboard_show_on_focus)
- assert(keyboard.shown_controls == [edit])
- var character_event := InputEventKey.new()
- character_event.keycode = KEY_A
- character_event.unicode = 97
- character_event.pressed = true
- Input.parse_input_event(character_event)
- await process_frame
- assert(edit.has_focus())
- assert(edit.text == "a")
- outside_button.grab_focus()
- await process_frame
- assert(outside_button.has_focus())
- assert(keyboard.is_open())
- var backspace_event := InputEventKey.new()
- backspace_event.keycode = KEY_BACKSPACE
- backspace_event.pressed = true
- Input.parse_input_event(backspace_event)
- await process_frame
- assert(edit.has_focus())
- assert(edit.text.is_empty())
- var close_event := InputEventJoypadButton.new()
- close_event.button_index = JOY_BUTTON_B
- close_event.pressed = true
- keyboard.call("_input", close_event)
- assert(not edit.has_focus())
- assert(not keyboard.is_open())
- assert(edit.virtual_keyboard_enabled)
- assert(edit.virtual_keyboard_show_on_focus)
- assert(keyboard.hide_count == 1)
- assert(keyboard.request_for_control(edit))
- await process_frame
- assert(edit.has_focus())
- assert(keyboard.is_open())
- assert(keyboard.shown_controls == [edit, edit])
- var escape_event := InputEventKey.new()
- escape_event.keycode = KEY_ESCAPE
- escape_event.pressed = true
- keyboard.call("_input", escape_event)
- assert(not edit.has_focus())
- assert(not keyboard.is_open())
- assert(keyboard.hide_count == 2)
- host.queue_free()
diff --git a/tests/on_screen_keyboard_validation.gd.uid b/tests/on_screen_keyboard_validation.gd.uid
deleted file mode 100644
index 973bdca..0000000
--- a/tests/on_screen_keyboard_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bgmt6joqdk37q
diff --git a/tests/operator_multiplayer_validation.gd b/tests/operator_multiplayer_validation.gd
deleted file mode 100644
index 05f8ee9..0000000
--- a/tests/operator_multiplayer_validation.gd
+++ /dev/null
@@ -1,266 +0,0 @@
-extends SceneTree
-
-const MainScene = preload("res://main/main.tscn")
-const TEST_PORT: int = 18140
-const FIRST_BANNED_FINGERPRINT: String = (
- "1111111111111111111111111111111111111111111111111111111111111111"
-)
-const SECOND_BANNED_FINGERPRINT: String = (
- "2222222222222222222222222222222222222222222222222222222222222222"
-)
-
-var _moderation_results: Array[Dictionary] = []
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var arguments: PackedStringArray = OS.get_cmdline_user_args()
- if arguments.has("host"):
- await _run_host()
- return
- if arguments.has("client"):
- await _run_client()
- return
- push_error("Operator multiplayer validation needs host or client mode.")
- quit(1)
-
-
-func _run_host() -> void:
- var main: Node = await _create_initialized_main()
- var session := main.get_node("%NetworkSession") as NetworkSession
- var service := (
- main.get_node("%NetworkPlayerListService")
- as NetworkPlayerListService
- )
- var bans := main.get_node("%HostBanStore") as HostBanStore
- assert(bool(main.call(
- "_apply_world", WorldLayout.STARTER_ISLAND, 1, true
- )))
- assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
- assert(session.start_private_host(TEST_PORT))
- assert(session.set_host_open(true))
-
- var remote_peer_id: int = await _wait_for_remote_peer(session)
- assert(remote_peer_id > 1)
- var remote_record: PeerRegistry.PeerRecord = session.get_peer_record(
- remote_peer_id
- )
- assert(remote_record != null and remote_record.identity_authenticated)
- var host_fingerprint: String = session.get_host_identity_fingerprint()
- assert(bans.ban(
- host_fingerprint, FIRST_BANNED_FINGERPRINT, "First Banned Player"
- ))
-
- var entry: PlayerListEntry = _entry_for_peer(service, remote_peer_id)
- assert(entry != null and entry.can_manage_operator)
- assert(service.set_operator(
- remote_peer_id,
- remote_record.identity_fingerprint,
- true,
- entry.revision,
- ))
- assert(session.is_peer_operator(remote_peer_id))
- var players_page := main.find_child(
- "PlayersPage", true, false
- ) as PlayersPage
- assert(players_page != null)
- players_page.call("_refresh")
- assert(_has_button_text(players_page, "deop"))
- assert(_has_button_tooltip(players_page, "scrub art"))
-
- var unban_deadline: int = Time.get_ticks_msec() + 12000
- while (
- Time.get_ticks_msec() < unban_deadline
- and bans.is_banned(host_fingerprint, FIRST_BANNED_FINGERPRINT)
- ):
- await process_frame
- assert(not bans.is_banned(
- host_fingerprint, FIRST_BANNED_FINGERPRINT
- ))
- assert(bans.ban(
- host_fingerprint, SECOND_BANNED_FINGERPRINT, "Second Banned Player"
- ))
-
- entry = _entry_for_peer(service, remote_peer_id)
- assert(entry != null and entry.is_operator)
- assert(service.set_operator(
- remote_peer_id,
- remote_record.identity_fingerprint,
- false,
- entry.revision,
- ))
- assert(not session.is_peer_operator(remote_peer_id))
- await create_timer(2.0).timeout
- assert(session.kick_authenticated_peer(
- remote_peer_id,
- remote_record.identity_fingerprint,
- ))
-
- var disconnect_deadline: int = Time.get_ticks_msec() + 8000
- while (
- Time.get_ticks_msec() < disconnect_deadline
- and session.is_authenticated_peer(remote_peer_id)
- ):
- await process_frame
- assert(not session.is_authenticated_peer(remote_peer_id))
- assert(bans.is_banned(host_fingerprint, SECOND_BANNED_FINGERPRINT))
- print("Operator multiplayer host validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _run_client() -> void:
- var main: Node = await _create_initialized_main()
- main.call(
- "_on_title_join_game_requested", "127.0.0.1:%d" % TEST_PORT
- )
- var session := main.get_node("%NetworkSession") as NetworkSession
- var service := (
- main.get_node("%NetworkPlayerListService")
- as NetworkPlayerListService
- )
- service.moderation_finished.connect(
- func(success: bool, message: String) -> void:
- _moderation_results.append({
- "success": success,
- "message": message,
- })
- )
- var join_deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < join_deadline:
- await process_frame
- if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
- main.call("_confirm_server_trust")
- if session.is_joined_client():
- break
- assert(session.is_joined_client())
-
- var operator_deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < operator_deadline and not session.is_local_operator():
- await process_frame
- assert(session.is_local_operator())
- assert(service.is_local_moderator())
- var players_page := main.find_child(
- "PlayersPage", true, false
- ) as PlayersPage
- assert(players_page != null)
- players_page.call("_refresh")
- var tabs := players_page.get("_tabs") as HBoxContainer
- assert((tabs.get_child(3) as Button).visible)
- players_page.call("_select_tab", 3)
- assert(int(players_page.get("_current_tab")) == 3)
- var local_entry: PlayerListEntry = _entry_for_peer(
- service, session.get_local_peer_id()
- )
- assert(local_entry != null and local_entry.is_operator)
-
- var ban_deadline: int = Time.get_ticks_msec() + 10000
- while (
- Time.get_ticks_msec() < ban_deadline
- and not _has_ban(service.get_bans(), FIRST_BANNED_FINGERPRINT)
- ):
- await process_frame
- assert(_has_ban(service.get_bans(), FIRST_BANNED_FINGERPRINT))
- assert(service.unban(FIRST_BANNED_FINGERPRINT))
- var result_deadline: int = Time.get_ticks_msec() + 8000
- while Time.get_ticks_msec() < result_deadline and _moderation_results.is_empty():
- await process_frame
- assert(not _moderation_results.is_empty())
- assert(bool(_moderation_results.back().get("success", false)))
-
- var deop_deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < deop_deadline and session.is_local_operator():
- await process_frame
- assert(not session.is_local_operator())
- assert(not service.is_local_moderator())
- assert(not (tabs.get_child(3) as Button).visible)
- assert(int(players_page.get("_current_tab")) == 0)
- service.request_unban.rpc_id(1, SECOND_BANNED_FINGERPRINT)
- var disconnect_deadline: int = Time.get_ticks_msec() + 10000
- while (
- Time.get_ticks_msec() < disconnect_deadline
- and session.state != NetworkSession.State.SERVER_LOST
- ):
- await process_frame
- assert(session.state == NetworkSession.State.SERVER_LOST)
- assert(not bool(main.get("_gameplay_started")))
- var game_ui := main.get_node("%GameUI") as GameUI
- var title_screen := game_ui.get_title_screen()
- assert(title_screen.visible)
- assert(not title_screen.is_awaiting_start_input())
- assert((title_screen.get_node("%Center") as Control).visible)
- assert((title_screen.get_node("%ButtonCenter") as Control).visible)
- assert(not (title_screen.get_node("%JoinGamePage") as Control).visible)
- var feedback := title_screen.get_node("%FeedbackLabel") as RichTextLabel
- assert(feedback.visible)
- assert("removed by the host" in feedback.text.to_lower())
- print("Operator multiplayer client validation: PASS")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _create_initialized_main() -> Node:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- return main
-
-
-func _wait_for_remote_peer(session: NetworkSession) -> int:
- var deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- for peer_id: int in session.get_authenticated_peer_ids():
- if peer_id != session.get_local_peer_id():
- return peer_id
- return 0
-
-
-func _entry_for_peer(
- service: NetworkPlayerListService,
- peer_id: int,
-) -> PlayerListEntry:
- for entry: PlayerListEntry in service.get_entries():
- if entry.peer_id == peer_id:
- return entry
- return null
-
-
-func _has_ban(records: Array[Dictionary], fingerprint: String) -> bool:
- for record: Dictionary in records:
- if str(record.get("target_fingerprint", "")) == fingerprint:
- return true
- return false
-
-
-func _has_button_text(root_node: Node, text: String) -> bool:
- for node: Node in root_node.find_children("*", "Button", true, false):
- var button := node as Button
- if button != null and button.text == text:
- return true
- return false
-
-
-func _has_button_tooltip(root_node: Node, tooltip: String) -> bool:
- for node: Node in root_node.find_children("*", "Button", true, false):
- var button := node as Button
- if button != null and button.tooltip_text == tooltip:
- return true
- return false
diff --git a/tests/operator_multiplayer_validation.gd.uid b/tests/operator_multiplayer_validation.gd.uid
deleted file mode 100644
index 2951ea6..0000000
--- a/tests/operator_multiplayer_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cv17xsf20ue5t
diff --git a/tests/player_experience_ui_validation.gd b/tests/player_experience_ui_validation.gd
deleted file mode 100644
index 31ed497..0000000
--- a/tests/player_experience_ui_validation.gd
+++ /dev/null
@@ -1,51 +0,0 @@
-extends SceneTree
-
-const GameUIScene: PackedScene = preload("res://ui/game_ui.tscn")
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var game_ui := GameUIScene.instantiate() as GameUI
- root.add_child(game_ui)
- await process_frame
- game_ui.set("_gameplay_ui_enabled", true)
- var panel := game_ui.get_node("%ExperienceProgressPanel") as PanelContainer
- var bubble := game_ui.get_node("%ExperienceBubble") as PanelContainer
- var bubble_label := game_ui.get_node("%ExperienceBubbleLabel") as Label
- var award_label := game_ui.get_node("%ExperienceAwardLabel") as Label
- assert(panel != null and bubble != null)
- assert(not panel.visible and not bubble.visible)
- game_ui.call("_on_showcase_changed", "bluegill", "common", 1.0, 0, true)
- await process_frame
- game_ui.call(
- "_on_experience_awarded",
- 50,
- 0,
- 50,
- 1,
- 1,
- )
- await process_frame
- assert(not panel.visible and not bubble.visible)
- game_ui.call("_on_showcase_changed", "", "", 0.0, 0, false)
- await process_frame
- await process_frame
- assert(panel.visible and bubble.visible)
- assert(award_label.text == "+50 xp")
- assert(bubble_label.text == "+50 xp!")
- await create_timer(1.6).timeout
- var progress := game_ui.get_node("%ExperienceProgress") as ProgressBar
- var level_label := game_ui.get_node("%ExperienceLevelLabel") as Label
- assert(is_equal_approx(progress.value, 50.0))
- assert(level_label.text == "level 1")
- await create_timer(1.2).timeout
- assert(not panel.visible and not bubble.visible)
- game_ui.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- print("Player experience UI validation: PASS")
- quit()
diff --git a/tests/player_experience_ui_validation.gd.uid b/tests/player_experience_ui_validation.gd.uid
deleted file mode 100644
index b2f976b..0000000
--- a/tests/player_experience_ui_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c03tvq1ydhdqj
diff --git a/tests/player_experience_validation.gd b/tests/player_experience_validation.gd
deleted file mode 100644
index a76a3dd..0000000
--- a/tests/player_experience_validation.gd
+++ /dev/null
@@ -1,188 +0,0 @@
-extends SceneTree
-
-const FishCatchType = preload("res://fish/fish_catch.gd")
-const FishExperienceType = preload("res://fish/fish_experience.gd")
-const FishQualityType = preload("res://fish/fish_quality.gd")
-const PlayerExperienceType = preload(
- "res://progression/player_experience.gd"
-)
-const Catalog: FishPool = preload("res://fish/pools/fish_catalog.tres")
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- _validate_level_curve()
- _validate_catch_awards()
- _validate_player_experience_state()
- _validate_save_migration()
- print("Player experience validation: PASS")
- quit()
-
-
-func _validate_level_curve() -> void:
- assert(PlayerExperienceType.experience_required_for_next_level(1) == 100)
- assert(PlayerExperienceType.experience_required_for_next_level(2) == 130)
- assert(PlayerExperienceType.experience_required_for_next_level(3) == 170)
- assert(PlayerExperienceType.total_experience_for_level(1) == 0)
- assert(PlayerExperienceType.total_experience_for_level(2) == 100)
- assert(PlayerExperienceType.total_experience_for_level(3) == 230)
- assert(PlayerExperienceType.total_experience_for_level(4) == 400)
- assert(PlayerExperienceType.level_for_total_experience(99) == 1)
- assert(PlayerExperienceType.level_for_total_experience(100) == 2)
- assert(PlayerExperienceType.level_for_total_experience(399) == 3)
- assert(PlayerExperienceType.level_for_total_experience(400) == 4)
- assert(
- is_equal_approx(
- PlayerExperienceType.progress_for_total_experience(50),
- 0.5,
- )
- )
-
-
-func _validate_catch_awards() -> void:
- var minimum_boring: FishCatch = _make_catch(
- FishQualityType.Tier.BORING,
- false,
- )
- assert(
- FishExperienceType.calculate_catch_experience(
- minimum_boring,
- false,
- 0,
- ) == 50
- )
- var maximum_shiny: FishCatch = _make_catch(
- FishQualityType.Tier.SHINY,
- true,
- )
- var boring_mask: int = FishQualityType.bit_for(
- FishQualityType.Tier.BORING
- )
- assert(
- FishExperienceType.calculate_catch_experience(
- maximum_shiny,
- true,
- boring_mask,
- ) == 46
- )
- var almost_mastered: int = (
- FishQualityType.ALL_TIERS_MASK
- & ~FishQualityType.bit_for(FishQualityType.Tier.SHINY)
- )
- assert(
- FishExperienceType.calculate_catch_experience(
- maximum_shiny,
- true,
- almost_mastered,
- ) == 146
- )
- assert(
- FishExperienceType.calculate_catch_experience(
- maximum_shiny,
- true,
- FishQualityType.ALL_TIERS_MASK,
- ) == 31
- )
-
-
-func _validate_player_experience_state() -> void:
- var experience := PlayerExperienceType.new()
- root.add_child(experience)
- var awards: Array[Array] = []
- experience.experience_awarded.connect(
- func(
- amount: int,
- previous_total: int,
- new_total: int,
- previous_level: int,
- new_level: int,
- ) -> void:
- awards.append([
- amount,
- previous_total,
- new_total,
- previous_level,
- new_level,
- ])
- )
- assert(experience.get_level() == 1)
- assert(experience.award_experience(125))
- assert(experience.get_total_experience() == 125)
- assert(experience.get_level() == 2)
- assert(experience.get_experience_in_level() == 25)
- assert(awards.size() == 1)
- assert(awards[0] == [125, 0, 125, 1, 2])
- assert(experience.restore_total_experience(400))
- assert(experience.get_level() == 4)
- assert(not experience.restore_total_experience(-1))
- experience.queue_free()
-
-
-func _validate_save_migration() -> void:
- var manager := PlayerSaveManager.new()
- root.add_child(manager)
- var version_five: Dictionary = {
- "save_version": 5,
- "wallet": {"balance": 0},
- "collection": {
- "discovered_fish_ids": [],
- "discovered_quality_masks": {},
- },
- "inventory": {"next_catch_sequence": 1, "catches": []},
- "bag": {"items": []},
- "hotbar": {"selected_slot": 0, "slots": []},
- "upgrades": {"reel_speed_level": 0, "barrier_power_level": 0},
- "cooler": {"capacity_level": 0},
- "art": {"unlock_mask": 0},
- }
- var migrated: Dictionary = manager.call(
- "_migrate_save",
- version_five,
- 5,
- )
- assert(int(migrated.get("save_version", -1)) == 11)
- var experience_data: Dictionary = migrated.get("experience", {})
- assert(int(experience_data.get("total_experience", -1)) == 0)
- var world_data: Dictionary = migrated.get("world", {})
- assert(is_equal_approx(float(world_data.get("time_hours", -1.0)), 8.0))
- assert(
- int(world_data.get("seed", 0))
- == PlayerSaveManager.DEFAULT_WORLD_SEED
- )
- assert(
- PlayerJobService.validate_save_data(migrated.get("jobs", {}))
- )
- assert(not PlayerHomeState.sanitize_save_data(
- migrated.get("home", {})
- ).is_empty())
- manager.queue_free()
-
-
-func _make_catch(quality: int, maximum_weight: bool) -> FishCatch:
- var fish: FishData = Catalog.get_fish_by_id(&"bluegill")
- assert(fish != null)
- var fish_catch := FishCatchType.new()
- fish_catch.fish = fish
- fish_catch.fish_id = fish.id
- fish_catch.catch_id = StringName("bluegill:xp-%d-%s" % [
- quality,
- "max" if maximum_weight else "min",
- ])
- fish_catch.catch_sequence = 1
- fish_catch.weight_lb = (
- fish.get_maximum_weight()
- if maximum_weight
- else fish.get_minimum_weight()
- )
- fish_catch.display_scale = fish.get_display_scale_for_weight(
- fish_catch.weight_lb
- )
- fish_catch.quality = quality
- fish_catch.sale_value = FishQualityType.apply_sale_value(
- fish.get_sale_value_for_weight(fish_catch.weight_lb),
- quality,
- )
- return fish_catch
diff --git a/tests/player_experience_validation.gd.uid b/tests/player_experience_validation.gd.uid
deleted file mode 100644
index 1afe50c..0000000
--- a/tests/player_experience_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://djkr2iaa5taks
diff --git a/tests/player_gathering_animation_validation.gd b/tests/player_gathering_animation_validation.gd
deleted file mode 100644
index 42f97ca..0000000
--- a/tests/player_gathering_animation_validation.gd
+++ /dev/null
@@ -1,140 +0,0 @@
-extends SceneTree
-
-
-const PlayerScene: PackedScene = preload("res://player/player.tscn")
-const CrabBrown: FishData = preload(
- "res://fish/species/crab_brown/crab_brown.tres"
-)
-const ClamManila: FishData = preload(
- "res://fish/species/clam_manila/clam_manila.tres"
-)
-const BeetleStagCommon: FishData = preload(
- "res://fish/species/beetle_stag_common/beetle_stag_common.tres"
-)
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var player := PlayerScene.instantiate() as Player
- root.add_child(player)
- await process_frame
- assert(is_equal_approx(
- Player.get_catch_texture_resolution_scale(CrabBrown.display_texture),
- 20.0,
- ))
- assert(is_equal_approx(
- Player.get_catch_texture_resolution_scale(ClamManila.display_texture),
- 20.0,
- ))
- assert(is_equal_approx(
- Player.get_catch_texture_resolution_scale(BeetleStagCommon.display_texture),
- 20.0,
- ))
- player.set_process(false)
- player.set_physics_process(false)
- var animation_player := player.get_node(
- "Visuals/CharacterRig/AnimationPlayer"
- ) as AnimationPlayer
- assert(animation_player != null)
- for required_animation: StringName in [
- &"draw",
- &"idle_sneak",
- &"sneaking",
- &"strike",
- ]:
- assert(animation_player.has_animation(required_animation))
-
- Input.action_press(&"sneak")
- player.velocity = Vector3.ZERO
- player.call("_update_character_animation")
- assert(animation_player.current_animation == &"idle_sneak")
-
- player.velocity = Vector3(player.sneak_speed, 0.0, 0.0)
- player.call("_update_character_animation")
- assert(animation_player.current_animation == &"sneaking")
-
- player.velocity = Vector3.ZERO
- assert(player.begin_net_draw_visual())
- assert(animation_player.current_animation == &"draw")
- player.call("_on_character_animation_finished", &"draw")
- assert(StringName(player.get("_animation_action_id")) == &"draw")
- assert(animation_player.current_animation == &"draw")
-
- assert(player.play_net_strike_visual())
- assert(animation_player.current_animation == &"strike")
- player.call("_on_character_animation_finished", &"strike")
- assert(StringName(player.get("_animation_action_id")) == &"strike")
- assert(bool(player.get("_animation_action_paused")))
- assert(player.is_net_strike_held())
- assert(player.release_net_strike_hold())
- assert(StringName(player.get("_animation_action_id")).is_empty())
- assert(animation_player.current_animation == &"idle_sneak")
-
- Input.action_release(&"sneak")
- assert(player.begin_net_draw_visual())
- assert(animation_player.current_animation == &"casting")
- player.cancel_net_action_visual()
- assert(StringName(player.get("_animation_action_id")).is_empty())
- assert(animation_player.current_animation == &"idle")
- Input.action_press(&"sneak")
- var visuals := player.get_node("Visuals") as Node3D
-
- assert(player.play_net_strike_visual())
- player.resolve_net_strike_visual(true)
- player.net_success_contact_pause_duration = 0.05
- player.showcase_turn_duration = 0.05
- player.showcase_camera_transition_duration = 0.05
- player.net_showcase_camera_yaw_offset = 0.0
- var crab_catch := FishCatch.new()
- crab_catch.fish = CrabBrown
- crab_catch.fish_id = CrabBrown.id
- crab_catch.weight_lb = CrabBrown.get_minimum_weight()
- crab_catch.display_scale = CrabBrown.get_display_scale_for_weight(
- crab_catch.weight_lb
- )
- crab_catch.sale_value = CrabBrown.get_sale_value_for_weight(
- crab_catch.weight_lb
- )
- crab_catch.ensure_identity()
- player.begin_catch_showcase(crab_catch)
- assert(not bool(player.get("_showcase_animation_active")))
- assert(player.get("_pending_net_showcase_catch") == crab_catch)
- player.call("_on_character_animation_finished", &"strike")
- assert(StringName(player.get("_animation_action_id")) == &"strike")
- assert(bool(player.get("_animation_action_paused")))
- assert(not bool(player.get("_showcase_animation_active")))
- var contact_pause := player.get("_net_strike_showcase_pause_tween") as Tween
- assert(contact_pause != null and contact_pause.is_valid())
- await contact_pause.finished
- assert(StringName(player.get("_animation_action_id")).is_empty())
- assert(bool(player.get("_showcase_animation_active")))
- var stored_showcase_rotation: Vector3 = player.get(
- "_showcase_visual_rotation"
- )
- await create_timer(0.1).timeout
- assert(
- absf(wrapf(
- visuals.rotation.y - stored_showcase_rotation.y,
- -PI,
- PI,
- )) > 3.0
- )
- var catch_display := player.get("_catch_display") as Node3D
- assert(catch_display != null and catch_display.visible)
- player.end_catch_showcase(Callable(), true, true)
- assert(
- absf(wrapf(
- visuals.rotation.y - stored_showcase_rotation.y,
- -PI,
- PI,
- )) < 0.01
- )
-
- Input.action_release(&"sneak")
- player.queue_free()
- await process_frame
- print("Player gathering animation validation: PASS")
- quit()
diff --git a/tests/player_gathering_animation_validation.gd.uid b/tests/player_gathering_animation_validation.gd.uid
deleted file mode 100644
index 276cd27..0000000
--- a/tests/player_gathering_animation_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cba1j1dfxk5t4
diff --git a/tests/profile_multiplayer_validation.gd b/tests/profile_multiplayer_validation.gd
deleted file mode 100644
index 4d2bd70..0000000
--- a/tests/profile_multiplayer_validation.gd
+++ /dev/null
@@ -1,311 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-const AnimaleseVoiceType = preload("res://ui/animalese_voice.gd")
-const TEST_PORT: int = 18144
-const HOST_HISTORY_MESSAGE: String = "silent history replay"
-const HOST_VOICE_MESSAGE: String = "remote voice playback"
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- _validate_voice_distance_attenuation()
- var arguments: PackedStringArray = OS.get_cmdline_user_args()
- if arguments.has("host"):
- await _run_host()
- return
- if arguments.has("client"):
- await _run_client()
- return
- push_error("Profile multiplayer validation needs host or client mode.")
- quit(1)
-
-
-func _validate_voice_distance_attenuation() -> void:
- assert(is_zero_approx(
- ChatUI.animalese_volume_offset_db_for_distance(0.0)
- ))
- assert(is_zero_approx(
- ChatUI.animalese_volume_offset_db_for_distance(4.0)
- ))
- var middle_distance_volume := (
- ChatUI.animalese_volume_offset_db_for_distance(14.0)
- )
- assert(middle_distance_volume < 0.0)
- assert(middle_distance_volume > -80.0)
- assert(is_equal_approx(
- ChatUI.animalese_volume_offset_db_for_distance(24.0),
- -80.0,
- ))
-
-
-func _run_host() -> void:
- var main: Node = await _create_initialized_main()
- var session := main.get_node("%NetworkSession") as NetworkSession
- var profile := main.get_node(
- "%NetworkProfilePreferences"
- ) as NetworkProfilePreferences
- assert(profile.set_profile_identity(
- profile.display_name,
- "deep",
- profile.speech_speed_id,
- profile.call_id,
- "kim",
- ))
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(bool(main.call(
- "_apply_world", WorldLayout.STARTER_ISLAND, 1, true
- )))
- assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
- assert(session.start_private_host(TEST_PORT))
- var local_avatar := main.get_node("%PlayerSpawnService").get_avatar(
- 1
- ) as Player
- assert(local_avatar != null)
- assert(local_avatar.get_animalese_voice_id() == "deep")
- assert(local_avatar.get_animalese_sample_set_id() == "kim")
- assert(save_manager.initialize_new_game())
- main.call("_enter_gameplay")
- for _frame: int in 4:
- await physics_frame
- assert(session.set_host_open(true))
- var chat_service := main.get_node(
- "%NetworkChatService"
- ) as NetworkChatService
- assert(chat_service.send_local_message(
- HOST_HISTORY_MESSAGE,
- profile.voice_id,
- profile.sample_set_id,
- ))
- var voice_messages: Array[Dictionary] = []
- chat_service.message_received.connect(func(message: Dictionary) -> void:
- if str(message.get("body", "")) == "voice profile sync":
- voice_messages.append(message)
- )
- var remote_peer_id: int = await _wait_for_remote_peer(session)
- assert(remote_peer_id > 1)
- var record := session.get_peer_record(remote_peer_id)
- assert(record != null)
- assert(
- NetworkProtocol.APPEARANCE_PREVIEW_CAPABILITY
- in record.capability_flags
- )
- var original: Dictionary = record.appearance_snapshot.duplicate(true)
- var changed: Dictionary = _changed_appearance(original)
- assert(changed != original)
- await _wait_for_remote_appearance(session, remote_peer_id, changed)
- await _wait_for_remote_appearance(session, remote_peer_id, original)
- await _wait_for_remote_appearance(session, remote_peer_id, changed)
- var avatar := main.get_node("%PlayerSpawnService").get_avatar(
- remote_peer_id
- ) as Player
- assert(avatar != null)
- assert(avatar.appearance_snapshot == changed)
- assert(record.display_name != "NetworkProfileService")
- assert(chat_service.send_local_message(
- HOST_VOICE_MESSAGE,
- profile.voice_id,
- profile.sample_set_id,
- ))
- var voice_message_deadline: int = Time.get_ticks_msec() + 8000
- while (
- Time.get_ticks_msec() < voice_message_deadline
- and voice_messages.is_empty()
- ):
- await process_frame
- assert(not voice_messages.is_empty())
- assert(str(voice_messages[0].get("voice_id", "")) == "deep")
- assert(str(voice_messages[0].get("sample_set_id", "")) == "kim")
- var disconnect_deadline: int = Time.get_ticks_msec() + 8000
- while (
- Time.get_ticks_msec() < disconnect_deadline
- and session.is_authenticated_peer(remote_peer_id)
- ):
- await process_frame
- assert(not session.is_authenticated_peer(remote_peer_id))
- print("Profile multiplayer host validation: PASS")
- await _session_cleanup(main, session)
-
-
-func _run_client() -> void:
- var main: Node = await _create_initialized_main()
- var chat_ui := main.get_node("%GameUI").get_node("%ChatUI") as ChatUI
- var animalese := chat_ui.get("_animalese_voice") as AnimaleseVoiceType
- assert(animalese != null)
- var remote_voice_samples: Array[String] = []
- animalese.character_sample_played.connect(
- func(sample_set_id: String, _character: String) -> void:
- remote_voice_samples.append(sample_set_id)
- )
- main.call("_on_title_join_game_requested", "127.0.0.1:%d" % TEST_PORT)
- var session := main.get_node("%NetworkSession") as NetworkSession
- var join_deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < join_deadline:
- await process_frame
- if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
- main.call("_confirm_server_trust")
- if session.is_joined_client() and bool(main.get("_gameplay_started")):
- break
- assert(session.is_joined_client())
- assert(session.supports_server_capability(
- NetworkProtocol.APPEARANCE_PREVIEW_CAPABILITY
- ))
- var chat_service := main.get_node(
- "%NetworkChatService"
- ) as NetworkChatService
- # Player-hosted rooms deliberately send no prior scrollback to joining or
- # reconnecting clients. Live messages sent after authentication still work.
- await create_timer(0.5).timeout
- assert(not chat_service.get_history().any(
- func(message: Dictionary) -> bool:
- return str(message.get("body", "")) == HOST_HISTORY_MESSAGE
- ))
- assert(remote_voice_samples.is_empty())
- var service := main.get_node(
- "%NetworkProfileService"
- ) as NetworkProfileService
- var original: Dictionary = service.get_persisted_appearance()
- var changed: Dictionary = _changed_appearance(original)
- assert(service.preview_appearance(changed))
- await create_timer(0.75).timeout
- service.restore_persisted_appearance()
- await create_timer(0.75).timeout
- assert(service.preview_appearance(changed))
- await create_timer(0.75).timeout
- var apply_results: Array = []
- service.apply_finished.connect(func(accepted: bool, message: String) -> void:
- apply_results.append([accepted, message])
- )
- assert(service.apply_profile(
- service.get_persisted_name(),
- changed,
- true,
- "deep",
- service.get_persisted_speech_speed_id(),
- service.get_persisted_call_id(),
- "kim",
- ))
- var apply_deadline: int = Time.get_ticks_msec() + 8000
- while Time.get_ticks_msec() < apply_deadline and apply_results.is_empty():
- await process_frame
- assert(not apply_results.is_empty())
- assert(bool(apply_results[0][0]))
- assert(service.get_persisted_appearance() == changed)
- assert(service.get_persisted_voice_id() == "deep")
- assert(service.get_persisted_sample_set_id() == "kim")
- assert(Dictionary(session.get("_local_appearance_snapshot")) == changed)
- var player := main.get_node("%PlayerSpawnService").get_local_player() as Player
- assert(player.get_animalese_voice_id() == "deep")
- assert(player.get_animalese_sample_set_id() == "kim")
- var host_voice_messages: Array[Dictionary] = []
- for message: Dictionary in chat_service.get_history():
- if str(message.get("body", "")) == HOST_VOICE_MESSAGE:
- host_voice_messages.append(message)
- var host_voice_deadline: int = Time.get_ticks_msec() + 8000
- while (
- Time.get_ticks_msec() < host_voice_deadline
- and (
- host_voice_messages.is_empty()
- or remote_voice_samples.is_empty()
- )
- ):
- await process_frame
- for message: Dictionary in chat_service.get_history():
- if (
- str(message.get("body", "")) == HOST_VOICE_MESSAGE
- and message not in host_voice_messages
- ):
- host_voice_messages.append(message)
- assert(not host_voice_messages.is_empty())
- assert(str(host_voice_messages[0].get("voice_id", "")) == "deep")
- assert(str(host_voice_messages[0].get("sample_set_id", "")) == "kim")
- assert(remote_voice_samples.has("kim"))
- var local_confirmations: Array[Dictionary] = []
- chat_service.local_message_confirmed.connect(
- func(message: Dictionary) -> void:
- if str(message.get("body", "")) == "voice profile sync":
- local_confirmations.append(message)
- )
- assert(chat_service.send_local_message(
- "voice profile sync",
- service.get_persisted_voice_id(),
- service.get_persisted_sample_set_id(),
- ))
- var confirmation_deadline: int = Time.get_ticks_msec() + 8000
- while (
- Time.get_ticks_msec() < confirmation_deadline
- and local_confirmations.is_empty()
- ):
- await process_frame
- assert(not local_confirmations.is_empty())
- assert(str(local_confirmations[0].get("voice_id", "")) == "deep")
- assert(str(local_confirmations[0].get("sample_set_id", "")) == "kim")
- print("Profile multiplayer client validation: PASS")
- await _session_cleanup(main, session)
-
-
-func _changed_appearance(original: Dictionary) -> Dictionary:
- var changed := original.duplicate(true)
- for category_id: String in CharacterCustomizationCatalog.CATEGORY_IDS:
- if category_id in CharacterCustomizationCatalog.FUR_STYLE_IDS:
- continue
- var options: Array = CharacterCustomizationCatalog.options_for(category_id)
- for option: Dictionary in options:
- var option_id: Variant = option.get("id")
- if option_id != changed.get(category_id):
- changed[category_id] = option_id
- if CharacterCustomizationCatalog.validate_snapshot(changed):
- return changed
- changed[category_id] = original.get(category_id)
- assert(false, "No alternate appearance option is available.")
- return changed
-
-
-func _wait_for_remote_appearance(
- session: NetworkSession,
- peer_id: int,
- expected: Dictionary,
-) -> void:
- var deadline: int = Time.get_ticks_msec() + 8000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- var record := session.get_peer_record(peer_id)
- if record != null and record.appearance_snapshot == expected:
- return
- assert(false, "Timed out waiting for a remote appearance update.")
-
-
-func _create_initialized_main() -> Node:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- return main
-
-
-func _wait_for_remote_peer(session: NetworkSession) -> int:
- var deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- for peer_id: int in session.get_authenticated_peer_ids():
- if peer_id != session.get_local_peer_id():
- return peer_id
- return 0
-
-
-func _session_cleanup(main: Node, session: NetworkSession) -> void:
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
diff --git a/tests/profile_multiplayer_validation.gd.uid b/tests/profile_multiplayer_validation.gd.uid
deleted file mode 100644
index d561da1..0000000
--- a/tests/profile_multiplayer_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://qvk8y0fd7o2j
diff --git a/tests/progression_archive_validation.gd b/tests/progression_archive_validation.gd
deleted file mode 100644
index 30f0df6..0000000
--- a/tests/progression_archive_validation.gd
+++ /dev/null
@@ -1,377 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
-
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- var player := main.get("_player") as Player
- var data_root := main.get("_data_root") as PlayerDataRoot
- var home_state := main.get("_home_state") as PlayerHomeState
- assert(
- save_manager != null
- and player != null
- and data_root != null
- and home_state != null
- )
- assert(save_manager.initialize_new_game(864209))
- save_manager.set_autosave_enabled(true)
- assert(player.wallet.restore_balance(4321))
- assert(player.bag.add_item(&"coffee"))
- assert(home_state.add_owned_decor(&"basic_cube"))
- assert(not home_state.place_decor(
- &"basic_cube",
- Vector3.ZERO,
- 0.0,
- PlayerHomeState.PlacementMode.GRID,
- ).is_empty())
- assert(save_manager.save_now())
-
- var save_path: String = str(save_manager.get("_save_path"))
- assert(save_path.ends_with("player/player_save.nfsave"))
- assert(FileAccess.file_exists(save_path))
- assert(not FileAccess.file_exists(save_path.get_base_dir().path_join(
- "player_save.json"
- )))
- _assert_opaque(save_path)
- var decoded: Dictionary = ProgressionSaveCodec.read_local_save(save_path)
- assert(bool(decoded.get("ok", false)))
- assert(int((decoded["data"] as Dictionary)["save_version"]) == 11)
- assert(not PlayerHomeState.sanitize_save_data(
- (decoded["data"] as Dictionary).get("home", {})
- ).is_empty())
- assert(
- ((decoded["data"] as Dictionary)["home"] as Dictionary)
- .get("placements", []).size() == 1
- )
- var legacy_container_path: String = data_root.progression_backup_directory().path_join(
- "netfishing-container.nfsave"
- )
- _assert_legacy_progression_container(
- legacy_container_path,
- decoded["data"] as Dictionary,
- )
- _assert_legacy_data_manifest(data_root.root_path.get_base_dir())
- _assert_legacy_identity_backup(main, data_root)
-
- var archive_path: String = data_root.progression_backup_directory().path_join(
- "progression-test.nfsave"
- )
- var exported: Dictionary = save_manager.export_progression_archive(
- archive_path
- )
- assert(bool(exported.get("ok", false)))
- assert(FileAccess.file_exists(archive_path))
- _assert_opaque(archive_path)
- var inspected: Dictionary = save_manager.inspect_progression_archive(
- archive_path
- )
- assert(bool(inspected.get("ok", false)))
- assert(int(inspected.get("wallet_balance", -1)) == 4321)
- assert(int(inspected.get("world_seed", -1)) == 864209)
-
- assert(player.wallet.restore_balance(7))
- assert(save_manager.save_now())
- var imported: Dictionary = save_manager.import_progression_archive(
- archive_path
- )
- assert(bool(imported.get("ok", false)))
- assert(save_manager.load_player_data())
- assert(player.wallet.get_balance() == 4321)
- assert(save_manager.get_world_seed() == 864209)
- assert(home_state.get_owned_count(&"basic_cube") == 1)
- assert(home_state.get_placed_count(&"basic_cube") == 1)
-
- var legacy_data: Dictionary = (
- ProgressionSaveCodec.read_local_save(save_path)["data"]
- )
- assert(save_manager.delete_progression_save())
- var legacy_path: String = save_path.get_base_dir().path_join(
- "player_save.json"
- )
- var legacy_file := FileAccess.open(legacy_path, FileAccess.WRITE)
- assert(legacy_file != null)
- legacy_file.store_string(JSON.stringify(legacy_data, "\t"))
- legacy_file.close()
- assert(save_manager.load_player_data())
- assert(FileAccess.file_exists(save_path))
- assert(not FileAccess.file_exists(legacy_path))
- _assert_opaque(save_path)
-
- save_manager.set_autosave_enabled(false)
- var save_slots := main.get("_save_slots") as PlayerSaveSlotCatalog
- assert(save_slots != null)
- assert(save_slots.configure(data_root, save_manager))
- var adopted_slots: Array[Dictionary] = save_slots.list_slots()
- assert(adopted_slots.size() == 1)
- var adopted_slot_id: String = str(adopted_slots.front().get("slot_id", ""))
- assert(bool(adopted_slots.front().get("legacy", false)))
- assert(bool(adopted_slots.front().get("has_save", false)))
-
- var duplicated: Dictionary = save_slots.duplicate_slot(
- adopted_slot_id,
- "another island",
- WorldLayout.GENERATED,
- 97531,
- )
- assert(bool(duplicated.get("ok", false)))
- var duplicated_slot_id: String = str(duplicated.get("slot_id", ""))
- var duplicate_summary: Dictionary = save_slots.get_slot(duplicated_slot_id)
- assert(int(duplicate_summary.get("wallet_balance", -1)) == 4321)
- assert(int(duplicate_summary.get("world_seed", -1)) == 97531)
- assert(save_slots.activate_slot(duplicated_slot_id))
- assert(save_manager.load_player_data())
- assert(player.wallet.get_balance() == 4321)
- assert(save_manager.get_world_seed() == 97531)
- assert(save_slots.rename_slot(duplicated_slot_id, "renamed island"))
-
- var slot_archive: String = data_root.progression_backup_directory().path_join(
- "save-slot-export.nfsave"
- )
- assert(bool(save_slots.export_slot(
- duplicated_slot_id,
- slot_archive,
- ).get("ok", false)))
- var imported_slot: Dictionary = save_slots.import_slot(
- slot_archive,
- "imported island",
- )
- assert(bool(imported_slot.get("ok", false)))
- var imported_slot_id: String = str(imported_slot.get("slot_id", ""))
- assert(not save_slots.get_slot(imported_slot_id).is_empty())
- assert(save_slots.delete_slot(duplicated_slot_id))
- assert(save_slots.get_slot(duplicated_slot_id).is_empty())
- assert(save_slots.list_slots().size() == 2)
-
- var settings_panels: Array[Node] = main.find_children(
- "*", "SettingsPanel", true, false
- )
- assert(settings_panels.size() == 2)
- for settings_panel: SettingsPanel in settings_panels:
- assert(settings_panel.get_node_or_null("%ExportProgression") == null)
- assert(settings_panel.get_node_or_null("%ImportProgression") == null)
- var settings_panel: SettingsPanel = settings_panels.front() as SettingsPanel
- settings_panel.set(
- "_presentation_mode", SettingsPanel.PresentationMode.TITLE_EMBEDDED
- )
- var unchanged_root: String = data_root.root_path
- settings_panel.call("_choose_data_folder")
- var data_folder_dialog := (
- settings_panel.get("_data_folder_dialog") as FileDialog
- )
- assert(data_folder_dialog != null)
- assert(data_folder_dialog.current_dir == unchanged_root)
- data_folder_dialog.hide()
- settings_panel.call("_change_data_folder", unchanged_root + "/")
- await process_frame
- assert(data_root.root_path == unchanged_root)
- assert(
- (settings_panel.get_node("%SettingsFeedback") as Label).text
- == "this data folder is already active."
- )
- var save_slots_page := main.find_child(
- "SaveSlotsPage", true, false
- ) as SaveSlotsPage
- assert(save_slots_page != null)
- assert(save_slots_page.get_node("%ExportSlotButton") is Button)
- assert(save_slots_page.get_node("%ImportSlotButton") is Button)
-
- var unchanged_migration: Dictionary = (
- PortableDataMigration.migrate_active_to(
- data_root, unchanged_root + "/"
- )
- )
- assert(bool(unchanged_migration.get("ok", false)))
- assert(not bool(unchanged_migration.get("changed", true)))
- assert(data_root.root_path == unchanged_root)
-
- var migrated_root: String = data_root.root_path.get_base_dir().path_join(
- "progression-migrated-data"
- )
- settings_panel.call("_change_data_folder", migrated_root)
- for _frame: int in 4:
- await process_frame
- assert(data_root.root_path == migrated_root)
- assert(
- (settings_panel.get_node("%SettingsFeedback") as Label).text
- == "data folder changed."
- )
- var migrated_save: String = migrated_root.path_join(
- "player/player_save.nfsave"
- )
- assert(FileAccess.file_exists(migrated_save))
- assert(bool(
- ProgressionSaveCodec.read_local_save(migrated_save).get("ok", false)
- ))
- assert(FileAccess.file_exists(migrated_root.path_join(
- "player/save_slots.json"
- )))
- var migrated_imported_slot: String = migrated_root.path_join(
- "player/saves/%s.nfsave" % imported_slot_id
- )
- assert(FileAccess.file_exists(migrated_imported_slot))
- assert(bool(
- ProgressionSaveCodec.read_local_save(migrated_imported_slot).get(
- "ok", false
- )
- ))
- var live_slots: Array[Dictionary] = save_slots.list_slots()
- assert(live_slots.size() == 2)
- var live_created: Dictionary = save_slots.create_empty_slot(
- "live root switch"
- )
- assert(bool(live_created.get("ok", false)))
- var live_slot_id: String = str(live_created.get("slot_id", ""))
- assert(save_slots.rename_slot(live_slot_id, "live root ready"))
- assert(
- str(save_slots.get_slot(live_slot_id).get("display_name", ""))
- == "live root ready"
- )
-
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- print("Progression archive validation: PASS")
- quit()
-
-
-func _assert_legacy_progression_container(
- path: String,
- payload: Dictionary,
-) -> void:
- var payload_json: String = JSON.stringify(payload)
- var envelope: Dictionary = {
- "magic": ProgressionSaveCodec.LEGACY_MAGIC,
- "format_version": ProgressionSaveCodec.FORMAT_VERSION,
- "kind": ProgressionSaveCodec.LOCAL_KIND,
- "game_version": "0.18.1-alpha",
- "created_at_unix": 1,
- "payload_sha256": PortableFileGuard.hash_bytes(
- payload_json.to_utf8_buffer()
- ),
- "payload_json": payload_json,
- }
- var file: FileAccess = FileAccess.open_encrypted_with_pass(
- path,
- FileAccess.WRITE,
- ProgressionSaveCodec.CONTAINER_PASSPHRASE,
- )
- assert(file != null)
- file.store_string(JSON.stringify(envelope))
- file.close()
- var decoded: Dictionary = ProgressionSaveCodec.read_local_save(path)
- assert(bool(decoded.get("ok", false)))
- assert(bool(decoded.get("legacy_container", false)))
-
-
-func _assert_legacy_data_manifest(parent: String) -> void:
- var legacy_root: String = parent.path_join("netfishing-manifest-root")
- assert(DirAccess.make_dir_recursive_absolute(legacy_root) == OK)
- var legacy_id: String = "0123456789abcdef0123456789abcdef"
- var legacy_manifest: Dictionary = {
- "format_version": PlayerDataRoot.MANIFEST_VERSION,
- "layout_version": PlayerDataRoot.LAYOUT_VERSION,
- "application": PlayerDataRoot.LEGACY_APPLICATION_ID,
- "root_id": legacy_id,
- "created_at_unix": 1,
- "last_opened_at_unix": 1,
- }
- var legacy_path: String = legacy_root.path_join(
- PlayerDataRoot.LEGACY_MANIFEST_FILENAME
- )
- var file := FileAccess.open(legacy_path, FileAccess.WRITE)
- assert(file != null)
- file.store_string(JSON.stringify(legacy_manifest, "\t"))
- file.close()
- var legacy_data_root := PlayerDataRoot.new()
- root.add_child(legacy_data_root)
- assert(legacy_data_root.use_existing_root(legacy_root))
- assert(legacy_data_root.root_id == legacy_id)
- assert(FileAccess.file_exists(legacy_path))
- var canonical_path: String = legacy_root.path_join(
- PlayerDataRoot.MANIFEST_FILENAME
- )
- assert(FileAccess.file_exists(canonical_path))
- var parser := JSON.new()
- assert(parser.parse(FileAccess.get_file_as_string(canonical_path)) == OK)
- assert((parser.data as Dictionary).get("application") == PlayerDataRoot.APPLICATION_ID)
- legacy_data_root.queue_free()
-
-
-func _assert_legacy_identity_backup(main: Node, data_root: PlayerDataRoot) -> void:
- var player_identity := main.get("_player_identity") as PlayerIdentityStore
- var identity_backups := main.get("_identity_backups") as IdentityBackupService
- assert(player_identity != null and player_identity.is_ready())
- assert(identity_backups != null)
- var material: Dictionary = player_identity.export_identity_material()
- var fingerprint: String = str(material.get("fingerprint", ""))
- var private_key := CryptoKey.new()
- assert(private_key.load_from_string(str(material.get("private_pem", ""))) == OK)
- var canonical: PackedByteArray = NetworkIdentityCrypto.canonical_bytes_for_prefix(
- NetworkIdentityCrypto.LEGACY_DOMAIN_PREFIX,
- "identity_backup_self_test",
- ["player", fingerprint],
- )
- var context := HashingContext.new()
- assert(context.start(HashingContext.HASH_SHA256) == OK)
- context.update(canonical)
- var signature: PackedByteArray = Crypto.new().sign(
- HashingContext.HASH_SHA256,
- context.finish(),
- private_key,
- )
- var envelope: Dictionary = {
- "magic": IdentityBackupService.LEGACY_MAGIC,
- "format_version": IdentityBackupService.FORMAT_VERSION,
- "identity_type": "player",
- "algorithm": NetworkIdentityCrypto.ALGORITHM,
- "private_pem": material.get("private_pem", ""),
- "public_pem": material.get("public_pem", ""),
- "fingerprint": fingerprint,
- "created_at_unix": 1,
- "source_device_id": data_root.device_id,
- "self_signature": Marshalls.raw_to_base64(signature),
- }
- var backup_path: String = data_root.identity_backup_directory().path_join(
- "netfishing-player.nfidentity"
- )
- var passphrase := "legacy-test-passphrase"
- var file: FileAccess = FileAccess.open_encrypted_with_pass(
- backup_path,
- FileAccess.WRITE,
- passphrase,
- )
- assert(file != null)
- file.store_string(JSON.stringify(envelope))
- file.close()
- var inspected: Dictionary = identity_backups.inspect_backup(
- backup_path,
- passphrase,
- "player",
- )
- assert(bool(inspected.get("ok", false)))
- assert(str(inspected.get("fingerprint", "")) == fingerprint)
-
-
-func _assert_opaque(path: String) -> void:
- var bytes: PackedByteArray = PortableFileGuard.read_bytes(path)
- assert(not bytes.is_empty())
- var encoded_hex: String = bytes.hex_encode()
- assert("save_version".to_utf8_buffer().hex_encode() not in encoded_hex)
- assert("wallet".to_utf8_buffer().hex_encode() not in encoded_hex)
diff --git a/tests/progression_archive_validation.gd.uid b/tests/progression_archive_validation.gd.uid
deleted file mode 100644
index 59699fd..0000000
--- a/tests/progression_archive_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ccl47ftc1cu5l
diff --git a/tests/rod_catalog_validation.gd b/tests/rod_catalog_validation.gd
deleted file mode 100644
index 3b43b3f..0000000
--- a/tests/rod_catalog_validation.gd
+++ /dev/null
@@ -1,109 +0,0 @@
-extends SceneTree
-
-const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
-const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
-const ItemCatalogType = preload("res://items/item_catalog.gd")
-const ItemDataType = preload("res://items/item_data.gd")
-const OwnedItemType = preload("res://items/owned_item.gd")
-const PlayerBagType = preload("res://inventory/player_bag.gd")
-const PlayerWalletType = preload("res://economy/player_wallet.gd")
-
-const ItemCatalogResource: ItemCatalogType = preload(
- "res://items/catalog/item_catalog.tres"
-)
-const RodAttachmentScene = preload(
- "res://player/fishing_rod_attachment.tscn"
-)
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- _validate_catalog_activation()
- _validate_shop_stock()
- _validate_inactive_save_compatibility()
- _validate_fallback_attachment()
- print("Rod catalog validation: PASS")
- quit()
-
-
-func _validate_catalog_activation() -> void:
- var rods: Array[FishingRodDataType] = []
- for item: ItemDataType in ItemCatalogResource.get_valid_items():
- var rod := item as FishingRodDataType
- if rod == null:
- continue
- rods.append(rod)
- assert(rod.is_valid())
- if rod.icon == null:
- assert(not rod.active)
- assert(rods.size() == 21)
- var available_rods: Array[FishingRodDataType] = []
- for rod: FishingRodDataType in rods:
- if rod.is_available():
- available_rods.append(rod)
- assert(available_rods.size() == 1)
- var basic: FishingRodDataType = available_rods.front()
- assert(basic.item_id == &"basic_fishing_rod")
- assert(basic.icon != null)
- assert(basic.rod_model_scene == null)
- assert(
- ItemCatalogResource.get_available_item_by_id(&"aurora_rod") == null
- )
- assert(ItemCatalogResource.get_item_by_id(&"aurora_rod") != null)
-
-
-func _validate_shop_stock() -> void:
- var stock: Array[FishingRodDataType] = (
- FishingShopStockType.get_rod_stock(ItemCatalogResource)
- )
- assert(stock.size() == 1)
- assert(stock.front().item_id == &"basic_fishing_rod")
- var wallet := PlayerWalletType.new()
- wallet.current_balance = 100
- var bag := PlayerBagType.new()
- bag.setup(ItemCatalogResource)
- assert(
- FishingShopStockType.purchase_one(
- &"basic_fishing_rod",
- wallet,
- bag,
- ItemCatalogResource,
- )
- )
- assert(bag.owns_item(&"basic_fishing_rod"))
- assert(wallet.get_balance() == 100)
- assert(
- not FishingShopStockType.purchase_one(
- &"aurora_rod",
- wallet,
- bag,
- ItemCatalogResource,
- )
- )
- bag.free()
- wallet.free()
-
-
-func _validate_inactive_save_compatibility() -> void:
- var bag := PlayerBagType.new()
- bag.setup(ItemCatalogResource)
- assert(not bag.can_add_item(&"aurora_rod", 1))
- assert(not bag.add_item(&"aurora_rod", 1))
- var legacy_record := OwnedItemType.new()
- legacy_record.item_id = &"aurora_rod"
- legacy_record.quantity = 1
- var legacy_items: Array[OwnedItemType] = [legacy_record]
- assert(bag.replace_all_items(legacy_items))
- assert(bag.owns_item(&"aurora_rod"))
- bag.free()
-
-
-func _validate_fallback_attachment() -> void:
- var attachment := RodAttachmentScene.instantiate()
- assert(attachment.get_node_or_null("FishingRod/FallbackRodMesh") != null)
- assert(attachment.get_node_or_null("FishingRod/ModelMount") != null)
- assert(attachment.get_node_or_null("FishingRod/FishingRodTip") != null)
- attachment.free()
diff --git a/tests/rod_catalog_validation.gd.uid b/tests/rod_catalog_validation.gd.uid
deleted file mode 100644
index ae0537e..0000000
--- a/tests/rod_catalog_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bjbgbahx5iuyy
diff --git a/tests/session_switch_multiplayer_validation.gd b/tests/session_switch_multiplayer_validation.gd
deleted file mode 100644
index 86c9a84..0000000
--- a/tests/session_switch_multiplayer_validation.gd
+++ /dev/null
@@ -1,177 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-const FIRST_HOST_PORT: int = 18142
-const SECOND_HOST_PORT: int = 18143
-const WAIT_SECONDS: int = 20
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var arguments: PackedStringArray = OS.get_cmdline_user_args()
- if arguments.has("first_host"):
- await _run_host(FIRST_HOST_PORT, "first")
- return
- if arguments.has("second_host"):
- await _run_host(SECOND_HOST_PORT, "second")
- return
- if arguments.has("client"):
- await _run_client()
- return
- push_error(
- "Session switch multiplayer validation needs first_host, "
- + "second_host, or client mode."
- )
- quit(1)
-
-
-func _run_host(port: int, label: String) -> void:
- var main: Node = await _create_initialized_main()
- var session := main.get_node("%NetworkSession") as NetworkSession
- assert(bool(main.call(
- "_apply_world", WorldLayout.STARTER_ISLAND, 1, true
- )))
- assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
- assert(session.start_dedicated_host(port, 8, "127.0.0.1"))
- assert(session.set_host_open(true))
- var remote_peer_id: int = await _wait_for_remote_peer(session)
- assert(remote_peer_id > 1)
- var disconnect_deadline: int = (
- Time.get_ticks_msec() + WAIT_SECONDS * 1000
- )
- while (
- Time.get_ticks_msec() < disconnect_deadline
- and session.is_authenticated_peer(remote_peer_id)
- ):
- await process_frame
- assert(not session.is_authenticated_peer(remote_peer_id))
- print("Session switch %s host validation: PASS" % label)
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _run_client() -> void:
- var main: Node = await _create_initialized_main()
- var session := main.get_node("%NetworkSession") as NetworkSession
- main.call(
- "_on_title_join_game_requested",
- "127.0.0.1:%d" % FIRST_HOST_PORT,
- )
- await _wait_for_join(main, session, FIRST_HOST_PORT)
- var game_ui := main.get("_game_ui") as GameUI
- var pause_menu := game_ui.get_pause_menu()
- var join_page := pause_menu.get_node("%JoinGamePage") as JoinGamePage
- var discovery := main.get_node("%DiscoveryClient") as DiscoveryClient
- pause_menu.show()
- (pause_menu.get_node("%RootPage") as Control).hide()
- join_page.open_page()
- join_page.set("_mode", JoinGamePage.Mode.DISCOVER)
- var rooms: Array[Dictionary] = [{
- "room_id": "deferred-public-room",
- "room_name": "Deferred public room",
- "address": "127.0.0.1",
- "port": SECOND_HOST_PORT,
- "current_players": 0,
- "max_players": 8,
- }]
- join_page.set("_discovery_rooms", rooms)
- join_page.set("_selected_discovery_index", 0)
- assert(bool(join_page.get("_gameplay_context")))
- assert(
- str(
- (join_page.call("_selected_discovery_room") as Dictionary).get(
- "room_id", ""
- )
- ) == "deferred-public-room"
- )
- join_page.call("_request_join")
- assert(not bool(discovery.get("_join_request_in_flight")))
- assert(
- str(
- (join_page.get("_pending_confirmation_room") as Dictionary).get(
- "room_id", ""
- )
- ) == "deferred-public-room"
- )
- assert(
- int(pause_menu.get("_confirmation_action"))
- == PauseMenu.ConfirmationAction.JOIN_ANOTHER
- )
- join_page.cancel_pending_join_confirmation()
- (pause_menu.get_node("%ConfirmationPage") as Control).hide()
- pause_menu.set("_confirmation_action", PauseMenu.ConfirmationAction.NONE)
- pause_menu.set("_confirmation_returns_to_join_game", false)
- join_page.open_page("127.0.0.1:%d" % SECOND_HOST_PORT)
- join_page.set("_mode", JoinGamePage.Mode.DIRECT)
- join_page.call("_request_join")
- assert(
- int(pause_menu.get("_confirmation_action"))
- == PauseMenu.ConfirmationAction.JOIN_ANOTHER
- )
- assert(not join_page.visible)
- assert((pause_menu.get_node("%ConfirmationPage") as Control).visible)
- pause_menu.call(
- "_finish_confirmation_accept",
- PauseMenu.ConfirmationAction.JOIN_ANOTHER,
- )
- await _wait_for_join(main, session, SECOND_HOST_PORT)
- assert(bool(main.get("_gameplay_started")))
- print("Session switch multiplayer client validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _wait_for_join(
- main: Node,
- session: NetworkSession,
- expected_port: int,
-) -> void:
- var deadline: int = Time.get_ticks_msec() + WAIT_SECONDS * 1000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
- main.call("_confirm_server_trust")
- var endpoint: ConnectionEndpoint = session.get_current_endpoint()
- if (
- session.is_joined_client()
- and bool(main.get("_gameplay_started"))
- and endpoint != null
- and endpoint.port == expected_port
- ):
- return
- assert(false, "Timed out joining UDP %d." % expected_port)
-
-
-func _wait_for_remote_peer(session: NetworkSession) -> int:
- var deadline: int = Time.get_ticks_msec() + WAIT_SECONDS * 1000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- for peer_id: int in session.get_authenticated_peer_ids():
- if peer_id > 1:
- return peer_id
- return 0
-
-
-func _create_initialized_main() -> Node:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- return main
diff --git a/tests/session_switch_multiplayer_validation.gd.uid b/tests/session_switch_multiplayer_validation.gd.uid
deleted file mode 100644
index 6e6f24c..0000000
--- a/tests/session_switch_multiplayer_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bkuih0ht2vny3
diff --git a/tests/shoreline_ambience_validation.gd b/tests/shoreline_ambience_validation.gd
deleted file mode 100644
index 78829f1..0000000
--- a/tests/shoreline_ambience_validation.gd
+++ /dev/null
@@ -1,50 +0,0 @@
-extends SceneTree
-
-const ShorelineAmbienceType := preload("res://world/shoreline_ambience.gd")
-const WavesStream := preload("res://audio/ambience/waves.wav")
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var waves := WavesStream as AudioStreamWAV
- assert(waves != null)
-
- var segments: Array[PackedVector2Array] = [
- PackedVector2Array([Vector2.ZERO, Vector2(10.0, 0.0)]),
- ]
- assert(is_equal_approx(
- ShorelineAmbienceType.distance_to_segments(Vector2(5.0, 3.0), segments),
- 3.0,
- ))
- assert(is_equal_approx(
- ShorelineAmbienceType.distance_to_segments(Vector2(-4.0, 0.0), segments),
- 4.0,
- ))
-
- var controller := ShorelineAmbienceType.new() as ShorelineAmbience
- var runtime_audio := AudioStreamPlayer.new()
- runtime_audio.name = "WavesAudio"
- runtime_audio.unique_name_in_owner = true
- runtime_audio.stream = WavesStream
- runtime_audio.bus = &"SFX"
- controller.add_child(runtime_audio)
- runtime_audio.owner = controller
- root.add_child(controller)
- await process_frame
- assert(runtime_audio.bus == &"Environment")
- assert(runtime_audio.stream == null)
- controller.set_audio_enabled(true)
- assert(controller.near_distance < controller.far_distance)
- assert(controller.near_volume_db > controller.far_volume_db)
- var runtime_waves := runtime_audio.stream as AudioStreamWAV
- assert(runtime_waves.loop_mode == AudioStreamWAV.LOOP_FORWARD)
- assert(runtime_waves.loop_begin == 0)
- assert(runtime_waves.loop_end == 1044956)
- controller.free()
- await process_frame
-
- print("Shoreline ambience validation: PASS")
- quit()
diff --git a/tests/shoreline_ambience_validation.gd.uid b/tests/shoreline_ambience_validation.gd.uid
deleted file mode 100644
index 4b7987b..0000000
--- a/tests/shoreline_ambience_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bwvhgpbkb00l1
diff --git a/tests/shoreline_ribbon_validation.gd b/tests/shoreline_ribbon_validation.gd
deleted file mode 100644
index e78acb0..0000000
--- a/tests/shoreline_ribbon_validation.gd
+++ /dev/null
@@ -1,97 +0,0 @@
-extends SceneTree
-
-const ShorelineMesh: ArrayMesh = preload(
- "res://world/generated/shorelines/starter_ocean_shoreline.tres"
-)
-const GEOMETRY_EPSILON := 0.0001
-const CROSSING_NEIGHBORHOOD := 32
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- assert(ShorelineMesh.get_surface_count() == 1)
- var arrays := ShorelineMesh.surface_get_arrays(0)
- var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
- var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
- assert(not vertices.is_empty())
- assert(vertices.size() % 2 == 0)
- assert(indices.size() == vertices.size() * 3)
- var point_count := vertices.size() / 2
- var land_edge := PackedVector2Array()
- var water_edge := PackedVector2Array()
- for index: int in point_count:
- var land := vertices[index * 2]
- var water := vertices[index * 2 + 1]
- land_edge.append(Vector2(land.x, land.z))
- water_edge.append(Vector2(water.x, water.z))
- var ribbon_width := land.distance_to(water)
- assert(ribbon_width >= 0.34)
- assert(ribbon_width <= 1.5)
- var crossing_count := (
- _count_local_crossings(water_edge)
- + _count_local_crossings(land_edge)
- )
- if crossing_count > 0:
- push_error(
- "Shoreline ribbon contains %d local edge crossings."
- % crossing_count
- )
- quit(1)
- return
- print("Shoreline ribbon validation: PASS")
- quit()
-
-
-func _count_local_crossings(points: PackedVector2Array) -> int:
- var crossing_count := 0
- for first: int in points.size():
- var first_next := (first + 1) % points.size()
- var checked_neighbors := mini(
- CROSSING_NEIGHBORHOOD,
- points.size() - 2,
- )
- for offset: int in range(2, checked_neighbors + 1):
- var second := (first + offset) % points.size()
- var second_next := (second + 1) % points.size()
- if second_next == first:
- continue
- if _segments_cross(
- points[first],
- points[first_next],
- points[second],
- points[second_next],
- ):
- crossing_count += 1
- return crossing_count
-
-
-func _segments_cross(
- a: Vector2,
- b: Vector2,
- c: Vector2,
- d: Vector2,
-) -> bool:
- if (
- maxf(a.x, b.x) < minf(c.x, d.x) - GEOMETRY_EPSILON
- or maxf(c.x, d.x) < minf(a.x, b.x) - GEOMETRY_EPSILON
- or maxf(a.y, b.y) < minf(c.y, d.y) - GEOMETRY_EPSILON
- or maxf(c.y, d.y) < minf(a.y, b.y) - GEOMETRY_EPSILON
- ):
- return false
- var first_direction := b - a
- var second_direction := d - c
- var denominator := first_direction.cross(second_direction)
- if absf(denominator) <= GEOMETRY_EPSILON:
- return false
- var offset := c - a
- var first_amount := offset.cross(second_direction) / denominator
- var second_amount := offset.cross(first_direction) / denominator
- return (
- first_amount > GEOMETRY_EPSILON
- and first_amount < 1.0 - GEOMETRY_EPSILON
- and second_amount > GEOMETRY_EPSILON
- and second_amount < 1.0 - GEOMETRY_EPSILON
- )
diff --git a/tests/shoreline_ribbon_validation.gd.uid b/tests/shoreline_ribbon_validation.gd.uid
deleted file mode 100644
index 7989d89..0000000
--- a/tests/shoreline_ribbon_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://c73frygpfhred
diff --git a/tests/surface_drawing_multiplayer_validation.gd b/tests/surface_drawing_multiplayer_validation.gd
deleted file mode 100644
index f79700e..0000000
--- a/tests/surface_drawing_multiplayer_validation.gd
+++ /dev/null
@@ -1,451 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-const TEST_PORT: int = 18133
-const WAIT_MSEC: int = 60000
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var arguments: PackedStringArray = OS.get_cmdline_user_args()
- if arguments.has("host"):
- await _run_host()
- return
- if arguments.has("client"):
- await _run_client()
- return
- push_error("Surface drawing multiplayer validation needs host or client mode.")
- quit(1)
-
-
-func _run_host() -> void:
- var main: Node = await _create_initialized_main()
- var session := main.get_node("%NetworkSession") as NetworkSession
- assert(bool(main.call(
- "_apply_world", WorldLayout.STARTER_ISLAND, 1, true
- )))
- assert(bool(main.call(
- "_prepare_host_world", WorldLayout.STARTER_ISLAND, 1
- )))
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND))
- assert(session.start_private_host(TEST_PORT))
- main.call("_enter_gameplay")
- await physics_frame
- await physics_frame
- var player := main.get("_player") as Player
- assert(player.bag.add_item(&"art_kit", 1))
- assert(player.hotbar.assign_item(0, &"art_kit"))
- assert(player.hotbar.select_slot(0))
- assert(player.art_unlocks.restore_mask(PlayerArtUnlocks.ALL_UNLOCK_MASK))
-
- var service := main.get_node(
- "%NetworkSurfaceDrawingService"
- ) as NetworkSurfaceDrawingService
- var surface: Dictionary = _surface_below_local_player(main)
- assert(not surface.is_empty())
- assert(service.request_canvas_at_surface(
- surface["position"], surface["normal"], Vector3.RIGHT
- ))
- var canvas_id: String = service.get_canvas_ids()[0]
- assert(service.request_cell_edits(canvas_id, [{
- "x": 7,
- "y": 7,
- "color_id": "coral",
- }]))
- assert(session.set_host_open(true))
-
- var remote_peer_id: int = await _wait_for_remote_peer(session)
- assert(remote_peer_id > 1)
- var remote_record: PeerRegistry.PeerRecord = session.get_peer_record(
- remote_peer_id
- )
- assert(remote_record != null)
- var network_home := main.get_node(
- "%NetworkHomeService"
- ) as NetworkHomeService
- var home_world := main.get_node("%HomeWorldService") as HomeWorldService
- var spawn_service := main.get_node(
- "%PlayerSpawnService"
- ) as PlayerSpawnService
- assert(await _wait_for_home(
- network_home, remote_record.identity_fingerprint
- ))
- var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id)
- assert(remote_avatar != null)
- var exterior_spawn: Transform3D = home_world.get_exterior_spawn_transform(
- remote_record.identity_fingerprint
- )
- var exterior_deadline: int = Time.get_ticks_msec() + 5000
- while (
- Time.get_ticks_msec() < exterior_deadline
- and remote_avatar.global_position.distance_to(exterior_spawn.origin) > 0.25
- ):
- await physics_frame
- assert(remote_avatar.global_position.distance_to(exterior_spawn.origin) <= 0.25)
- var canvas_state: Dictionary = service.get_canvas_state(canvas_id)
- var canvas_origin: Vector3 = SurfaceDrawingProtocol.array_to_vector(
- canvas_state["origin"]
- )
- remote_avatar.global_position = Vector3(
- canvas_origin.x + 1.0,
- player.global_position.y,
- canvas_origin.z,
- )
- session.publish_authoritative_teleport(remote_peer_id)
- for _frame: int in 4:
- await physics_frame
- assert(session.peer_supports_capability(
- remote_peer_id, SurfaceDrawingProtocol.CAPABILITY
- ))
-
- var overwrite_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while Time.get_ticks_msec() < overwrite_deadline:
- await process_frame
- var state: Dictionary = service.get_canvas_state(canvas_id)
- if int(state.get("revision", 0)) < 2:
- continue
- var cell: Dictionary = _find_cell(state, 7, 7)
- if (
- str(cell.get("color_id", "")) == "blue"
- and str(cell.get("author_fingerprint", ""))
- == remote_record.identity_fingerprint
- ):
- break
- var overwritten_state: Dictionary = service.get_canvas_state(canvas_id)
- assert(int(overwritten_state["revision"]) >= 2)
- var overwritten_cell: Dictionary = _find_cell(overwritten_state, 7, 7)
- assert(str(overwritten_cell["color_id"]) == "blue")
- assert(
- str(overwritten_cell["author_fingerprint"])
- == remote_record.identity_fingerprint
- )
- assert(
- remote_record.identity_fingerprint
- in overwritten_state.get("participant_fingerprints", [])
- )
-
- assert(service.request_cell_edits(canvas_id, [{
- "x": 8,
- "y": 7,
- "color_id": "sunny",
- }]))
- var undo_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while Time.get_ticks_msec() < undo_deadline:
- await process_frame
- var state: Dictionary = service.get_canvas_state(canvas_id)
- if str(_find_cell(state, 7, 7).get("color_id", "")) == "coral":
- break
- assert(
- str(_find_cell(service.get_canvas_state(canvas_id), 7, 7)["color_id"])
- == "coral"
- )
- var guide_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while Time.get_ticks_msec() < guide_deadline:
- await process_frame
- if not bool(service.get_canvas_state(canvas_id).get("guide_visible", true)):
- break
- assert(not bool(service.get_canvas_state(canvas_id)["guide_visible"]))
- # Keep the shared hidden state observable before publishing the restore.
- await create_timer(1.0).timeout
- assert(service.request_guide_visibility(canvas_id, true))
- await create_timer(1.0).timeout
- assert(service.request_guide_visibility(canvas_id, false, true))
- await create_timer(1.0).timeout
- assert(service.clear_session_artwork())
- var stamp_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while (
- Time.get_ticks_msec() < stamp_deadline
- and service.get_canvas_ids().is_empty()
- ):
- await process_frame
- assert(service.get_canvas_ids().size() == 1)
- var stamp_id: String = service.get_canvas_ids()[0]
- var stamp_state: Dictionary = service.get_canvas_state(stamp_id)
- assert(bool(stamp_state.get("stamp", false)))
- assert(bool(stamp_state["finalized"]))
- assert((stamp_state["cells"] as Array).size() == 1)
- assert(str(stamp_state["cells"][0]["color_id"]) == "coral")
- assert(
- stamp_state.get("participant_fingerprints", [])
- == [remote_record.identity_fingerprint]
- )
- # Give the client a full observation window before the authoritative clear.
- await create_timer(1.0).timeout
- assert(
- service.clear_artwork_by_fingerprint(
- remote_record.identity_fingerprint
- ) == 1
- )
- assert(service.get_canvas_ids().is_empty())
- var second_stamp_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while (
- Time.get_ticks_msec() < second_stamp_deadline
- and service.get_canvas_ids().is_empty()
- ):
- await process_frame
- assert(service.get_canvas_ids().size() == 1)
- # Keep the second stamp observable on the client before the ban removes all
- # artwork carrying that participant fingerprint.
- await create_timer(1.0).timeout
- var player_list := main.get_node(
- "%NetworkPlayerListService"
- ) as NetworkPlayerListService
- var remote_entry: PlayerListEntry = null
- for entry: PlayerListEntry in player_list.get_entries():
- if entry.peer_id == remote_peer_id:
- remote_entry = entry
- break
- assert(remote_entry != null and remote_entry.can_ban)
- assert(player_list.ban(
- remote_peer_id,
- remote_record.identity_fingerprint,
- remote_record.display_name,
- remote_entry.revision,
- ))
- assert(service.get_canvas_ids().is_empty())
- var removed_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while (
- Time.get_ticks_msec() < removed_deadline
- and session.is_authenticated_peer(remote_peer_id)
- ):
- await process_frame
- assert(not session.is_authenticated_peer(remote_peer_id))
- print("Surface drawing multiplayer host validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _run_client() -> void:
- var main: Node = await _create_initialized_main()
- main.call(
- "_on_title_join_game_requested",
- "127.0.0.1:%d" % TEST_PORT,
- )
- var session := main.get_node("%NetworkSession") as NetworkSession
- var join_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while Time.get_ticks_msec() < join_deadline:
- await process_frame
- if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
- main.call("_confirm_server_trust")
- if session.is_joined_client() and bool(main.get("_gameplay_started")):
- break
- assert(session.is_joined_client())
- assert(session.supports_server_capability(
- SurfaceDrawingProtocol.CAPABILITY
- ))
- var player := main.get("_player") as Player
- assert(player.bag.add_item(&"art_kit", 1))
- assert(player.hotbar.assign_item(0, &"art_kit"))
- assert(player.hotbar.select_slot(0))
- assert(player.art_unlocks.restore_mask(PlayerArtUnlocks.ALL_UNLOCK_MASK))
-
- var service := main.get_node(
- "%NetworkSurfaceDrawingService"
- ) as NetworkSurfaceDrawingService
- var snapshot_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while (
- Time.get_ticks_msec() < snapshot_deadline
- and service.get_canvas_ids().is_empty()
- ):
- await process_frame
- assert(service.get_canvas_ids().size() == 1)
- var canvas_id: String = service.get_canvas_ids()[0]
- var snapshot: Dictionary = service.get_canvas_state(canvas_id)
- assert(str(_find_cell(snapshot, 7, 7).get("color_id", "")) == "coral")
- var canvas_origin: Vector3 = SurfaceDrawingProtocol.array_to_vector(
- snapshot["origin"]
- )
- var placement_deadline: int = Time.get_ticks_msec() + 10000
- while (
- Time.get_ticks_msec() < placement_deadline
- and player.global_position.distance_to(canvas_origin)
- > NetworkSurfaceDrawingService.MAX_DRAW_DISTANCE + 2.0
- ):
- await physics_frame
- assert(
- player.global_position.distance_to(canvas_origin)
- <= NetworkSurfaceDrawingService.MAX_DRAW_DISTANCE + 2.0
- )
- assert(service.request_cell_edits(canvas_id, [{
- "x": 7,
- "y": 7,
- "color_id": "blue",
- }]))
-
- var shared_edit_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while Time.get_ticks_msec() < shared_edit_deadline:
- await process_frame
- var state: Dictionary = service.get_canvas_state(canvas_id)
- if str(_find_cell(state, 8, 7).get("color_id", "")) == "sunny":
- break
- var final_state: Dictionary = service.get_canvas_state(canvas_id)
- assert(str(_find_cell(final_state, 7, 7)["color_id"]) == "blue")
- assert(str(_find_cell(final_state, 8, 7)["color_id"]) == "sunny")
- assert(service.request_undo_last_stroke())
- var undo_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while Time.get_ticks_msec() < undo_deadline:
- await process_frame
- var state: Dictionary = service.get_canvas_state(canvas_id)
- if str(_find_cell(state, 7, 7).get("color_id", "")) == "coral":
- break
- assert(
- str(_find_cell(service.get_canvas_state(canvas_id), 7, 7)["color_id"])
- == "coral"
- )
- assert(service.request_guide_visibility(canvas_id, false))
- var guide_hidden_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while Time.get_ticks_msec() < guide_hidden_deadline:
- await process_frame
- if not bool(service.get_canvas_state(canvas_id).get("guide_visible", true)):
- break
- assert(not bool(service.get_canvas_state(canvas_id)["guide_visible"]))
- var guide_restored_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while Time.get_ticks_msec() < guide_restored_deadline:
- await process_frame
- if bool(service.get_canvas_state(canvas_id).get("guide_visible", false)):
- break
- assert(bool(service.get_canvas_state(canvas_id)["guide_visible"]))
- var finalized_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while Time.get_ticks_msec() < finalized_deadline:
- await process_frame
- if bool(service.get_canvas_state(canvas_id).get("finalized", false)):
- break
- assert(bool(service.get_canvas_state(canvas_id)["finalized"]))
- var reset_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while Time.get_ticks_msec() < reset_deadline:
- await process_frame
- if service.get_canvas_ids().is_empty():
- break
- assert(service.get_canvas_ids().is_empty())
- var data_root := main.get("_data_root") as PlayerDataRoot
- var stamp_directory: String = data_root.root_path.path_join("artwork")
- assert(DirAccess.make_dir_recursive_absolute(stamp_directory) == OK)
- var stamp_path: String = stamp_directory.path_join("multiplayer-stamp.png")
- var stamp_image := Image.create(16, 16, false, Image.FORMAT_RGBA8)
- stamp_image.fill(Color.TRANSPARENT)
- stamp_image.set_pixel(3, 11, SurfaceDrawingPalette.get_color(&"coral"))
- assert(stamp_image.save_png(stamp_path) == OK)
- assert(service.select_saved_stamp(stamp_path))
- var stamp_surface: Dictionary = _surface_below_local_player(main)
- assert(not stamp_surface.is_empty())
- assert(service.request_stamp_at_surface(
- stamp_surface["position"], stamp_surface["normal"], Vector3.RIGHT
- ))
- var shared_stamp_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while (
- Time.get_ticks_msec() < shared_stamp_deadline
- and service.get_canvas_ids().is_empty()
- ):
- await process_frame
- assert(service.get_canvas_ids().size() == 1)
- var stamp_state: Dictionary = service.get_canvas_state(
- service.get_canvas_ids()[0]
- )
- assert(bool(stamp_state.get("stamp", false)))
- assert(bool(stamp_state["finalized"]))
- assert(str(stamp_state["cells"][0]["color_id"]) == "coral")
- assert(
- session.get_local_identity_fingerprint()
- in stamp_state.get("participant_fingerprints", [])
- )
- var clear_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while (
- Time.get_ticks_msec() < clear_deadline
- and not service.get_canvas_ids().is_empty()
- ):
- await process_frame
- assert(service.get_canvas_ids().is_empty())
- assert(service.request_stamp_at_surface(
- stamp_surface["position"], stamp_surface["normal"], Vector3.RIGHT
- ))
- var second_stamp_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while (
- Time.get_ticks_msec() < second_stamp_deadline
- and service.get_canvas_ids().is_empty()
- ):
- await process_frame
- assert(service.get_canvas_ids().size() == 1)
- var ban_deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while (
- Time.get_ticks_msec() < ban_deadline
- and session.state != NetworkSession.State.SERVER_LOST
- ):
- await process_frame
- assert(session.state == NetworkSession.State.SERVER_LOST)
- assert(service.get_canvas_ids().is_empty())
- print("Surface drawing multiplayer client validation: PASS")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _surface_below_local_player(main: Node) -> Dictionary:
- var player := main.get("_player") as Player
- var query := PhysicsRayQueryParameters3D.create(
- player.global_position + Vector3.UP * 3.0,
- player.global_position + Vector3.DOWN * 6.0,
- 1,
- )
- query.collide_with_areas = false
- query.collide_with_bodies = true
- query.exclude = [player.get_rid()]
- return player.get_world_3d().direct_space_state.intersect_ray(query)
-
-
-func _wait_for_remote_peer(session: NetworkSession) -> int:
- var deadline: int = Time.get_ticks_msec() + WAIT_MSEC
- while Time.get_ticks_msec() < deadline:
- await process_frame
- for peer_id: int in session.get_authenticated_peer_ids():
- if peer_id != session.get_local_peer_id():
- return peer_id
- return 0
-
-
-func _find_cell(state: Dictionary, x: int, y: int) -> Dictionary:
- for value: Variant in state.get("cells", []):
- if typeof(value) != TYPE_DICTIONARY:
- continue
- var cell: Dictionary = value
- if int(cell.get("x", -1)) == x and int(cell.get("y", -1)) == y:
- return cell
- return {}
-
-
-func _create_initialized_main() -> Node:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- return main
-
-
-func _wait_for_home(
- network_home: NetworkHomeService,
- fingerprint: String,
-) -> bool:
- var deadline: int = Time.get_ticks_msec() + 10000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- var manifests: Dictionary = network_home.get(
- "_manifests_by_fingerprint"
- )
- if manifests.has(fingerprint):
- return true
- return false
diff --git a/tests/surface_drawing_multiplayer_validation.gd.uid b/tests/surface_drawing_multiplayer_validation.gd.uid
deleted file mode 100644
index cc43853..0000000
--- a/tests/surface_drawing_multiplayer_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bj5dje2scya34
diff --git a/tests/surface_drawing_runtime_validation.gd b/tests/surface_drawing_runtime_validation.gd
deleted file mode 100644
index 85110cd..0000000
--- a/tests/surface_drawing_runtime_validation.gd
+++ /dev/null
@@ -1,238 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-const TEST_PORT: int = 18132
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- var session := main.get_node("%NetworkSession") as NetworkSession
- assert(bool(main.call(
- "_apply_world", WorldLayout.STARTER_ISLAND, 1, true
- )))
- assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
- assert(session.start_private_host(TEST_PORT))
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND))
- main.call("_enter_gameplay")
- await physics_frame
- await physics_frame
-
- var player := main.get("_player") as Player
- assert(player.bag.add_item(&"art_kit", 1))
- assert(player.hotbar.assign_item(0, &"art_kit"))
- assert(player.hotbar.select_slot(0))
- assert(player.art_unlocks.restore_mask(PlayerArtUnlocks.ALL_UNLOCK_MASK))
- var service := main.get_node(
- "%NetworkSurfaceDrawingService"
- ) as NetworkSurfaceDrawingService
- assert(service != null)
- assert(session.supports_server_capability(SurfaceDrawingProtocol.CAPABILITY))
- service.deactivate()
- _validate_marker_controls(service, player)
- var query := PhysicsRayQueryParameters3D.create(
- player.global_position + Vector3.UP * 3.0,
- player.global_position + Vector3.DOWN * 6.0,
- 1,
- )
- query.collide_with_areas = false
- query.collide_with_bodies = true
- query.exclude = [player.get_rid()]
- var hit: Dictionary = player.get_world_3d().direct_space_state.intersect_ray(query)
- assert(not hit.is_empty())
- assert(service.request_canvas_at_surface(
- hit["position"], hit["normal"], Vector3.RIGHT
- ))
- assert(service.get_canvas_ids().size() == 1)
- var canvas_id: String = service.get_canvas_ids()[0]
- assert(service.request_cell_edits(canvas_id, [{
- "x": 8,
- "y": 8,
- "color_id": "ocean_teal",
- }]))
- var state: Dictionary = service.get_canvas_state(canvas_id)
- assert(int(state["revision"]) == 1)
- assert((state["cells"] as Array).size() == 1)
- assert(state["cells"][0]["color_id"] == "ocean_teal")
- assert(
- state["cells"][0]["author_fingerprint"]
- == session.get_local_identity_fingerprint()
- )
- service.set("_last_hovered_canvas_id", canvas_id)
- service.set("_hovered_canvas_id", "")
- var game_ui := main.get_node("%GameUI") as GameUI
- var toolbar := game_ui.get_node(
- "%SurfaceDrawingToolbar"
- ) as SurfaceDrawingToolbar
- var export_button := toolbar.get_node("%ExportButton") as Button
- export_button.pressed.emit()
- var exported_entries: Array[Dictionary] = service.get_saved_stamp_entries()
- assert(exported_entries.size() == 1)
- var export_path: String = str(exported_entries[0]["path"])
- assert(not export_path.is_empty())
- var data_root := main.get("_data_root") as PlayerDataRoot
- assert(export_path.begins_with(data_root.root_path.path_join("artwork")))
- assert(FileAccess.file_exists(export_path))
- var status_label := game_ui.get_node("%StatusLabel") as Label
- assert(status_label.text == "artwork exported to the data folder • artwork")
- var exported_image: Image = Image.load_from_file(export_path)
- assert(exported_image.get_size() == Vector2i(16, 16))
- assert(exported_image.get_pixel(8, 7).is_equal_approx(
- SurfaceDrawingPalette.get_color(&"ocean_teal")
- ))
- assert(service.request_guide_visibility(canvas_id, false))
- state = service.get_canvas_state(canvas_id)
- assert(not bool(state["guide_visible"]))
- var canvas_nodes: Dictionary = service.get("_canvas_nodes")
- var canvas := canvas_nodes[canvas_id] as SurfaceDrawingCanvas
- assert(not canvas.is_guide_visible())
- assert(service.request_guide_visibility(canvas_id, true))
- assert(bool(service.get_canvas_state(canvas_id)["guide_visible"]))
- assert(service.request_guide_visibility(canvas_id, false, true))
- assert(bool(service.get_canvas_state(canvas_id)["finalized"]))
- assert(canvas.is_finalized())
- assert(not canvas.has_guide_geometry())
- assert(service.request_canvas_at_surface(
- hit["position"], hit["normal"], Vector3.RIGHT
- ))
- assert(service.get_canvas_ids().size() == 2)
- var replacement_canvas_id: String = ""
- for candidate_id: String in service.get_canvas_ids():
- if candidate_id != canvas_id:
- replacement_canvas_id = candidate_id
- assert(not replacement_canvas_id.is_empty())
- assert(service.request_cell_edits(replacement_canvas_id, [{
- "x": 8,
- "y": 8,
- "color_id": "blue",
- }]))
- assert((service.get_canvas_state(canvas_id)["cells"] as Array).is_empty())
- assert(
- str(service.get_canvas_state(replacement_canvas_id)["cells"][0]["color_id"])
- == "blue"
- )
- assert(service.request_undo_last_stroke())
- assert((service.get_canvas_state(replacement_canvas_id)["cells"] as Array).is_empty())
- assert(
- str(service.get_canvas_state(canvas_id)["cells"][0]["color_id"])
- == "ocean_teal"
- )
- var player_list := main.get_node(
- "%NetworkPlayerListService"
- ) as NetworkPlayerListService
- assert(player_list.get_session_artwork_counts() == Vector2i(2, 1))
- assert(player_list.reset_session_artwork())
- assert(service.get_canvas_ids().is_empty())
- var stamp_entries: Array[Dictionary] = service.get_saved_stamp_entries()
- assert(stamp_entries.size() == 1)
- assert(str(stamp_entries[0]["path"]) == export_path)
- assert(int(stamp_entries[0]["grid_size"]) == 16)
- assert(bool(stamp_entries[0]["available"]))
- service.activate(Vector2(640.0, 360.0))
- assert(service.select_saved_stamp(export_path))
- assert(service.is_stamp_mode())
- assert(service.request_stamp_at_surface(
- hit["position"], hit["normal"], Vector3.RIGHT
- ))
- assert(service.get_canvas_ids().size() == 1)
- var stamp_id: String = service.get_canvas_ids()[0]
- var stamp_state: Dictionary = service.get_canvas_state(stamp_id)
- assert(bool(stamp_state["stamp"]))
- assert(bool(stamp_state["finalized"]))
- assert(not bool(stamp_state["guide_visible"]))
- assert((stamp_state["cells"] as Array).size() == 1)
- assert(int(stamp_state["cells"][0]["x"]) == 8)
- assert(int(stamp_state["cells"][0]["y"]) == 8)
- assert(str(stamp_state["cells"][0]["color_id"]) == "ocean_teal")
- assert(
- stamp_state.get("participant_fingerprints", [])
- == [session.get_local_identity_fingerprint()]
- )
- var stamp_canvas_nodes: Dictionary = service.get("_canvas_nodes")
- var stamp_canvas := stamp_canvas_nodes[stamp_id] as SurfaceDrawingCanvas
- assert(stamp_canvas.is_finalized())
- assert(not stamp_canvas.has_guide_geometry())
- service.set_placement_mode(false)
- assert(not service.is_stamp_mode())
- assert(service.clear_session_artwork())
-
- print("Surface drawing runtime validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _validate_marker_controls(
- service: NetworkSurfaceDrawingService,
- player: Player,
-) -> void:
- var prior_mouse_mode: Input.MouseMode = Input.mouse_mode
- var activation_pointer := Vector2(824.0, 318.0)
- service.activate(activation_pointer)
- assert(service.is_active())
- assert(not service.is_placement_mode())
- assert(service.get_pointer_screen_position() == activation_pointer)
- if DisplayServer.get_name() != "headless":
- assert(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE)
-
- var placement_key := InputEventKey.new()
- placement_key.physical_keycode = KEY_R
- placement_key.pressed = true
- assert(service.handle_input(placement_key, true))
- assert(service.is_placement_mode())
-
- var pointer_before: Vector2 = service.get_pointer_screen_position()
- var pointer_motion := InputEventMouseMotion.new()
- pointer_motion.position = pointer_before + Vector2(30.0, -12.0)
- assert(service.handle_input(pointer_motion, true))
- assert(service.get_pointer_screen_position() != pointer_before)
- var scaled_ui_motion := InputEventMouseMotion.new()
- scaled_ui_motion.position = Vector2(260.0, 140.0)
- var root_viewport_pointer := Vector2(960.0, 540.0)
- assert(service.handle_input(
- scaled_ui_motion,
- true,
- root_viewport_pointer,
- ))
- assert(service.get_pointer_screen_position() == root_viewport_pointer)
- var zoom_event := InputEventMouseButton.new()
- zoom_event.button_index = MOUSE_BUTTON_WHEEL_UP
- zoom_event.shift_pressed = true
- zoom_event.pressed = true
- assert(not service.handle_input(zoom_event, true))
- var prior_zoom: float = float(player.get("_target_zoom"))
- player.call("_unhandled_input", zoom_event)
- assert(float(player.get("_target_zoom")) < prior_zoom)
-
- var camera_press := InputEventMouseButton.new()
- camera_press.button_index = MOUSE_BUTTON_RIGHT
- camera_press.pressed = true
- assert(not service.handle_input(camera_press, true))
- var pointer_before_camera: Vector2 = service.get_pointer_screen_position()
- assert(not service.handle_input(pointer_motion, true))
- assert(service.get_pointer_screen_position() == pointer_before_camera)
- var camera_release := InputEventMouseButton.new()
- camera_release.button_index = MOUSE_BUTTON_RIGHT
- camera_release.pressed = false
- assert(not service.handle_input(camera_release, true))
-
- assert(service.handle_input(placement_key, true))
- assert(not service.is_placement_mode())
- service.deactivate()
- assert(Input.mouse_mode == prior_mouse_mode)
diff --git a/tests/surface_drawing_runtime_validation.gd.uid b/tests/surface_drawing_runtime_validation.gd.uid
deleted file mode 100644
index af3cd35..0000000
--- a/tests/surface_drawing_runtime_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://8c30wxeu4lxv
diff --git a/tests/surface_drawing_texture_renderer_validation.gd b/tests/surface_drawing_texture_renderer_validation.gd
deleted file mode 100644
index ed99383..0000000
--- a/tests/surface_drawing_texture_renderer_validation.gd
+++ /dev/null
@@ -1,101 +0,0 @@
-extends SceneTree
-
-const CANVAS_COUNT: int = 5
-const GRID_SIZE: int = 32
-const FINGERPRINT: String = (
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
-)
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var world := Node3D.new()
- root.add_child(world)
- var color_ids: Array[StringName] = SurfaceDrawingPalette.get_color_ids()
- var cells: Array[Dictionary] = []
- for y: int in range(GRID_SIZE):
- for x: int in range(GRID_SIZE):
- cells.append({
- "x": x,
- "y": y,
- "color_id": str(color_ids[(x + y) % color_ids.size()]),
- "author_fingerprint": FINGERPRINT,
- })
- var canvases: Array[SurfaceDrawingCanvas] = []
- var persistent_meshes: Array[Mesh] = []
- var persistent_textures: Array[Texture2D] = []
- for canvas_index: int in range(CANVAS_COUNT):
- var canvas := SurfaceDrawingCanvas.new()
- world.add_child(canvas)
- assert(canvas.setup({
- "session_id": "texture-renderer-session",
- "canvas_id": "canvas-%d" % canvas_index,
- "origin": [float(canvas_index) * 3.0, 0.0, 0.0],
- "normal": [0.0, 1.0, 0.0],
- "tangent": [1.0, 0.0, 0.0],
- "width": GRID_SIZE,
- "height": GRID_SIZE,
- "cell_size": SurfaceDrawingProtocol.CELL_SIZE,
- "revision": 0,
- "guide_visible": true,
- "finalized": false,
- "layer": canvas_index,
- "creator_fingerprint": FINGERPRINT,
- "cells": cells,
- }, null))
- assert(canvas.get_rendered_pixel_count() == GRID_SIZE * GRID_SIZE)
- var pixel_instance := canvas.get("_pixel_instance") as MeshInstance3D
- assert(pixel_instance != null)
- assert(pixel_instance.mesh.get_surface_count() == 1)
- var material := pixel_instance.mesh.surface_get_material(
- 0
- ) as StandardMaterial3D
- assert(material != null)
- assert(material.albedo_texture is ImageTexture)
- canvases.append(canvas)
- persistent_meshes.append(pixel_instance.mesh)
- persistent_textures.append(material.albedo_texture)
- for canvas_index: int in range(CANVAS_COUNT):
- var canvas: SurfaceDrawingCanvas = canvases[canvas_index]
- assert(canvas.apply_update({
- "session_id": "texture-renderer-session",
- "canvas_id": "canvas-%d" % canvas_index,
- "revision": 1,
- "edits": [{
- "x": canvas_index,
- "y": canvas_index,
- "color_id": "coral",
- "author_fingerprint": FINGERPRINT,
- }],
- }))
- var pixel_instance := canvas.get("_pixel_instance") as MeshInstance3D
- var material := pixel_instance.mesh.surface_get_material(
- 0
- ) as StandardMaterial3D
- assert(pixel_instance.mesh == persistent_meshes[canvas_index])
- assert(material.albedo_texture == persistent_textures[canvas_index])
- assert(canvas.apply_guide_update({
- "session_id": "texture-renderer-session",
- "canvas_id": "canvas-%d" % canvas_index,
- "revision": 2,
- "guide_visible": false,
- "finalized": true,
- }))
- assert(not canvas.has_guide_geometry())
- await process_frame
- for canvas: SurfaceDrawingCanvas in canvases:
- var artwork_mesh_count: int = 0
- for child: Node in canvas.get_children():
- if child is MultiMeshInstance3D:
- assert(false, "Art Kit pixels regressed to per-color MultiMeshes.")
- if child is MeshInstance3D:
- artwork_mesh_count += 1
- assert(artwork_mesh_count == 1)
- assert(canvas.get_export_image().get_size() == Vector2i(32, 32))
- world.queue_free()
- await process_frame
- print("Surface drawing texture renderer validation: PASS")
- quit()
diff --git a/tests/surface_drawing_texture_renderer_validation.gd.uid b/tests/surface_drawing_texture_renderer_validation.gd.uid
deleted file mode 100644
index c64b594..0000000
--- a/tests/surface_drawing_texture_renderer_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://qj7jd2p3b4p8
diff --git a/tests/surface_drawing_validation.gd b/tests/surface_drawing_validation.gd
deleted file mode 100644
index ac48a67..0000000
--- a/tests/surface_drawing_validation.gd
+++ /dev/null
@@ -1,373 +0,0 @@
-extends SceneTree
-
-const FINGERPRINT_A: String = (
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
-)
-const FINGERPRINT_B: String = (
- "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
-)
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- _validate_palette()
- _validate_art_unlocks()
- _validate_protocol_bounds()
- _validate_grid_snapping()
- await _validate_canvas_geometry_and_collaboration()
- await _validate_blocked_author_visibility()
- _validate_peer_capability_tracking()
- print("Surface drawing validation: PASS")
- for _frame: int in 4:
- await process_frame
- quit()
-
-
-func _validate_palette() -> void:
- var ids: Array[StringName] = SurfaceDrawingPalette.get_color_ids()
- assert(ids.size() == 8)
- assert(ids.front() == SurfaceDrawingPalette.DEFAULT_COLOR_ID)
- for color_id: StringName in ids:
- assert(SurfaceDrawingPalette.has_color(color_id))
- assert(not SurfaceDrawingPalette.get_display_name(color_id).is_empty())
- assert(
- SurfaceDrawingPalette.filter_unlocked_ids([])
- == [SurfaceDrawingPalette.DEFAULT_COLOR_ID]
- )
-
-
-func _validate_protocol_bounds() -> void:
- assert(SurfaceDrawingProtocol.GRID_WIDTH == 16)
- assert(SurfaceDrawingProtocol.GRID_HEIGHT == 16)
- assert(SurfaceDrawingProtocol.GRID_SIZES == [16, 32, 64, 128])
- assert(is_equal_approx(SurfaceDrawingProtocol.CELL_SIZE, 0.075))
- assert(is_equal_approx(
- float(SurfaceDrawingProtocol.GRID_WIDTH)
- * SurfaceDrawingProtocol.CELL_SIZE,
- 1.2,
- ))
- var canvas_request: Dictionary = {
- "request_id": "canvas-1",
- "session_id": "session",
- "origin": [0.0, 1.0, 2.0],
- "normal": [0.0, 1.0, 0.0],
- "tangent": [1.0, 0.0, 0.0],
- "width": SurfaceDrawingProtocol.GRID_WIDTH,
- "height": SurfaceDrawingProtocol.GRID_HEIGHT,
- "cell_size": SurfaceDrawingProtocol.CELL_SIZE,
- }
- assert(SurfaceDrawingProtocol.validate_canvas_request(canvas_request))
- var malformed: Dictionary = canvas_request.duplicate(true)
- malformed["origin"] = [INF, 0.0, 0.0]
- assert(not SurfaceDrawingProtocol.validate_canvas_request(malformed))
- var edit_request: Dictionary = {
- "request_id": "edit-1",
- "session_id": "session",
- "canvas_id": "canvas-1",
- "stroke_id": "stroke-1",
- "brush_size": 1,
- "edits": [{"x": 0, "y": 0, "color_id": "ocean_teal"}],
- }
- assert(SurfaceDrawingProtocol.validate_edit_request(edit_request))
- edit_request["edits"] = [{
- "x": SurfaceDrawingProtocol.MAX_GRID_SIZE,
- "y": 0,
- "color_id": "ocean_teal",
- }]
- assert(not SurfaceDrawingProtocol.validate_edit_request(edit_request))
- edit_request["edits"] = [{"x": 0, "y": 0, "color_id": "ocean_teal"}]
- edit_request["brush_size"] = 5
- assert(not SurfaceDrawingProtocol.validate_edit_request(edit_request))
- for grid_size: int in SurfaceDrawingProtocol.GRID_SIZES:
- var sized_request: Dictionary = canvas_request.duplicate(true)
- sized_request["width"] = grid_size
- sized_request["height"] = grid_size
- assert(SurfaceDrawingProtocol.validate_canvas_request(sized_request))
- var unsupported_size: Dictionary = canvas_request.duplicate(true)
- unsupported_size["width"] = 48
- unsupported_size["height"] = 48
- assert(not SurfaceDrawingProtocol.validate_canvas_request(unsupported_size))
- var stamp_pixels := PackedByteArray()
- stamp_pixels.resize(16 * 16)
- stamp_pixels[0] = 1
- var stamp_request: Dictionary = canvas_request.duplicate(true)
- stamp_request["request_id"] = "stamp-1"
- stamp_request["pixels"] = stamp_pixels
- assert(SurfaceDrawingProtocol.validate_stamp_request(stamp_request))
- var invalid_stamp: Dictionary = stamp_request.duplicate(true)
- var invalid_pixels: PackedByteArray = invalid_stamp["pixels"]
- invalid_pixels[0] = SurfaceDrawingPalette.COLORS.size() + 1
- invalid_stamp["pixels"] = invalid_pixels
- assert(not SurfaceDrawingProtocol.validate_stamp_request(invalid_stamp))
- var empty_stamp: Dictionary = stamp_request.duplicate(true)
- empty_stamp["pixels"] = PackedByteArray()
- assert(not SurfaceDrawingProtocol.validate_stamp_request(empty_stamp))
- var guide_request: Dictionary = {
- "request_id": "guide-1",
- "session_id": "session",
- "canvas_id": "canvas-1",
- "guide_visible": false,
- "finalized": false,
- }
- assert(SurfaceDrawingProtocol.validate_guide_request(guide_request))
- guide_request["guide_visible"] = 0
- assert(not SurfaceDrawingProtocol.validate_guide_request(guide_request))
- var undo_request: Dictionary = {
- "request_id": "undo-1",
- "session_id": "session",
- "stroke_id": "stroke-1",
- }
- assert(SurfaceDrawingProtocol.validate_undo_request(undo_request))
-
-
-func _validate_art_unlocks() -> void:
- var unlocks := PlayerArtUnlocks.new()
- assert(unlocks.get_unlocked_color_ids() == [&"chalk_white"])
- assert(unlocks.get_unlocked_brush_sizes() == [1])
- assert(unlocks.get_unlocked_grid_sizes() == [16])
- assert(unlocks.unlock_product(&"marker_ocean_teal"))
- assert(unlocks.unlock_product(&"brush_4x"))
- assert(unlocks.unlock_product(&"grid_128x"))
- assert(unlocks.unlock_product(PlayerArtUnlocks.STAMP_PRODUCT_ID))
- assert(unlocks.is_color_unlocked(&"ocean_teal"))
- assert(unlocks.is_brush_size_unlocked(4))
- assert(unlocks.is_grid_size_unlocked(128))
- assert(unlocks.is_stamp_unlocked())
- assert(not unlocks.restore_mask(PlayerArtUnlocks.ALL_UNLOCK_MASK + 1))
- unlocks.free()
-
-
-func _validate_grid_snapping() -> void:
- var anchor: Dictionary = {
- "session_id": "session",
- "canvas_id": "anchor",
- "origin": [0.0, 0.0, 0.0],
- "normal": [0.0, 1.0, 0.0],
- "tangent": [1.0, 0.0, 0.0],
- "width": SurfaceDrawingProtocol.GRID_WIDTH,
- "height": SurfaceDrawingProtocol.GRID_HEIGHT,
- "cell_size": SurfaceDrawingProtocol.CELL_SIZE,
- "revision": 0,
- "guide_visible": false,
- "finalized": true,
- "layer": 1,
- "creator_fingerprint": FINGERPRINT_A,
- "cells": [],
- }
- var snapped: Dictionary = SurfaceDrawingPlacement.resolve(
- Vector3(1.08, 0.02, 0.08),
- Vector3.UP,
- Vector3.RIGHT,
- [anchor],
- )
- assert(bool(snapped["snapped"]))
- var snapped_origin: Vector3 = snapped["origin"]
- assert(snapped_origin.is_equal_approx(Vector3(1.2, 0.0, 0.0)))
- var unsnapped: Dictionary = SurfaceDrawingPlacement.resolve(
- Vector3(0.6, 0.0, 0.0),
- Vector3.UP,
- Vector3.RIGHT,
- [anchor],
- )
- assert(not bool(unsnapped["snapped"]))
-
-
-func _validate_canvas_geometry_and_collaboration() -> void:
- var world := Node3D.new()
- root.add_child(world)
- var canvas := SurfaceDrawingCanvas.new()
- world.add_child(canvas)
- var state: Dictionary = {
- "session_id": "session",
- "canvas_id": "canvas-1",
- "origin": [0.0, 0.0, 0.0],
- "normal": [0.0, 1.0, 0.0],
- "tangent": [1.0, 0.0, 0.0],
- "width": SurfaceDrawingProtocol.GRID_WIDTH,
- "height": SurfaceDrawingProtocol.GRID_HEIGHT,
- "cell_size": SurfaceDrawingProtocol.CELL_SIZE,
- "revision": 0,
- "creator_fingerprint": FINGERPRINT_A,
- "cells": [],
- }
- assert(canvas.setup(state, null))
- var pixel_instance := canvas.get("_pixel_instance") as MeshInstance3D
- assert(pixel_instance != null)
- assert(pixel_instance.name == "ArtworkTexture")
- var persistent_mesh: Mesh = pixel_instance.mesh
- var pixel_material := persistent_mesh.surface_get_material(
- 0
- ) as StandardMaterial3D
- assert(pixel_material != null)
- var persistent_texture: Texture2D = pixel_material.albedo_texture
- assert(persistent_texture is ImageTexture)
- var first_center: Vector3 = canvas.get_cell_world_position(0, 0)
- assert(canvas.cell_at_world_point(first_center) == Vector2i(0, 0))
- assert(canvas.contains_world_point(first_center))
- var first_update: Dictionary = {
- "session_id": "session",
- "canvas_id": "canvas-1",
- "revision": 1,
- "edits": [{
- "x": 3,
- "y": 4,
- "color_id": "coral",
- "author_fingerprint": FINGERPRINT_A,
- }],
- }
- assert(canvas.apply_update(first_update))
- assert(pixel_instance.mesh == persistent_mesh)
- assert(pixel_material.albedo_texture == persistent_texture)
- assert(canvas.get_rendered_pixel_count() == 1)
- var export_image: Image = canvas.get_export_image()
- assert(export_image.get_size() == Vector2i(16, 16))
- assert(export_image.get_pixel(3, 11).is_equal_approx(
- SurfaceDrawingPalette.get_color(&"coral")
- ))
- assert(export_image.get_pixel(0, 0).is_equal_approx(Color.TRANSPARENT))
- var finish_update: Dictionary = {
- "session_id": "session",
- "canvas_id": "canvas-1",
- "revision": 2,
- "guide_visible": false,
- "finalized": true,
- }
- assert(canvas.apply_guide_update(finish_update))
- assert(not canvas.is_guide_visible())
- assert(canvas.is_finalized())
- assert(not canvas.has_guide_geometry())
- assert(pixel_instance.mesh == persistent_mesh)
- assert(pixel_material.albedo_texture == persistent_texture)
- await process_frame
- var finished_pixel_scale: float = canvas.get_rendered_pixel_size()
- assert(
- is_equal_approx(
- finished_pixel_scale, SurfaceDrawingProtocol.CELL_SIZE
- ),
- "finished pixel scale %f does not match cell size %f"
- % [finished_pixel_scale, SurfaceDrawingProtocol.CELL_SIZE],
- )
- var second_update: Dictionary = {
- "session_id": "session",
- "canvas_id": "canvas-1",
- "revision": 3,
- "edits": [{
- "x": 3,
- "y": 4,
- "color_id": "blue",
- "author_fingerprint": FINGERPRINT_B,
- }],
- }
- assert(canvas.apply_update(second_update))
- assert(pixel_instance.mesh == persistent_mesh)
- assert(pixel_material.albedo_texture == persistent_texture)
- var cells: Array[Dictionary] = canvas.get_authoritative_cells()
- assert(cells.size() == 1)
- assert(cells[0]["color_id"] == "blue")
- assert(cells[0]["author_fingerprint"] == FINGERPRINT_B)
- var erase_update: Dictionary = {
- "session_id": "session",
- "canvas_id": "canvas-1",
- "revision": 4,
- "edits": [{
- "x": 3,
- "y": 4,
- "color_id": "",
- "author_fingerprint": "",
- }],
- }
- assert(canvas.apply_update(erase_update))
- assert(canvas.get_authoritative_cells().is_empty())
- assert(canvas.get_rendered_pixel_count() == 0)
- assert(canvas.get_export_image().get_pixel(3, 11).is_equal_approx(
- Color.TRANSPARENT
- ))
- world.queue_free()
- await process_frame
-
-
-func _validate_peer_capability_tracking() -> void:
- var registry := PeerRegistry.new()
- assert(registry.add_peer(
- 1,
- "profile",
- "Voyager",
- NetworkProtocol.PROTOCOL_VERSION,
- FINGERPRINT_A,
- "public key fixture",
- PackedStringArray([str(SurfaceDrawingProtocol.CAPABILITY)]),
- ))
- var record: PeerRegistry.PeerRecord = registry.get_peer(1)
- assert(record != null)
- assert(str(SurfaceDrawingProtocol.CAPABILITY) in record.capability_flags)
-
-
-func _validate_blocked_author_visibility() -> void:
- var relationships := PlayerRelationshipStore.new()
- relationships.set("_loaded", true)
- relationships.set("_records", {
- FINGERPRINT_B: {"blocked": true, "muted": true},
- })
- var world := Node3D.new()
- root.add_child(world)
- world.add_child(relationships)
- var canvas := SurfaceDrawingCanvas.new()
- world.add_child(canvas)
- var state: Dictionary = {
- "session_id": "session",
- "canvas_id": "blocked-author-canvas",
- "origin": [0.0, 0.0, 0.0],
- "normal": [0.0, 1.0, 0.0],
- "tangent": [1.0, 0.0, 0.0],
- "width": SurfaceDrawingProtocol.GRID_WIDTH,
- "height": SurfaceDrawingProtocol.GRID_HEIGHT,
- "cell_size": SurfaceDrawingProtocol.CELL_SIZE,
- "revision": 1,
- "creator_fingerprint": FINGERPRINT_A,
- "participant_fingerprints": [FINGERPRINT_A, FINGERPRINT_B],
- "cells": [
- {
- "x": 1,
- "y": 1,
- "color_id": "coral",
- "author_fingerprint": FINGERPRINT_A,
- },
- {
- "x": 2,
- "y": 1,
- "color_id": "blue",
- "author_fingerprint": FINGERPRINT_B,
- },
- ],
- }
- assert(canvas.setup(state, relationships))
- assert(canvas.is_hidden_by_relationship())
- assert(canvas.get_rendered_pixel_count() == 0)
- relationships.set("_records", {})
- canvas.refresh_relationship_visibility()
- await process_frame
- assert(not canvas.is_hidden_by_relationship())
- assert(canvas.get_rendered_pixel_count() == 2)
- relationships.set("_records", {
- FINGERPRINT_B: {"blocked": true, "muted": true},
- })
- assert(canvas.apply_update({
- "session_id": "session",
- "canvas_id": "blocked-author-canvas",
- "revision": 2,
- "participant_fingerprints": [FINGERPRINT_A, FINGERPRINT_B],
- "edits": [{
- "x": 2,
- "y": 1,
- "color_id": "",
- "author_fingerprint": "",
- }],
- }))
- canvas.refresh_relationship_visibility()
- assert(canvas.is_hidden_by_relationship())
- assert(canvas.get_rendered_pixel_count() == 0)
- world.queue_free()
- await process_frame
diff --git a/tests/surface_drawing_validation.gd.uid b/tests/surface_drawing_validation.gd.uid
deleted file mode 100644
index 72e0102..0000000
--- a/tests/surface_drawing_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dui5ueufqooul
diff --git a/tests/tackle_order_validation.gd b/tests/tackle_order_validation.gd
deleted file mode 100644
index 5a38e06..0000000
--- a/tests/tackle_order_validation.gd
+++ /dev/null
@@ -1,61 +0,0 @@
-extends SceneTree
-
-const Catalog: ItemCatalog = preload("res://items/catalog/item_catalog.tres")
-const PlayerMenuType = preload("res://ui/player_menu.gd")
-
-const EXPECTED_BAIT_ORDER: Array[StringName] = [
- &"worms",
- &"snails",
- &"shrimp",
- &"squid_chunks",
- &"whole_sardine",
- &"whole_anchovy",
- &"luminous_roe",
-]
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var player_menu := PlayerMenuType.new() as PlayerMenu
- player_menu.set("_item_catalog", Catalog)
- var tackle_items: Array[OwnedItem] = []
- for item_id: StringName in [
- &"luminous_roe",
- &"whole_anchovy",
- &"shrimp",
- &"snails",
- &"whole_sardine",
- &"squid_chunks",
- &"worms",
- ]:
- var owned := OwnedItem.new()
- owned.item_id = item_id
- owned.quantity = 1
- tackle_items.append(owned)
- tackle_items.sort_custom(
- func(first: OwnedItem, second: OwnedItem) -> bool:
- return bool(player_menu.call(
- "_sort_tackle_items", first, second
- ))
- )
- var sorted_ids: Array[StringName] = []
- for owned: OwnedItem in tackle_items:
- sorted_ids.append(owned.item_id)
- assert(sorted_ids == EXPECTED_BAIT_ORDER)
- var bag := PlayerBag.new()
- bag.setup(Catalog)
- assert(bag.add_item(&"worms", 1))
- assert(bag.add_item(&"shrimp", 1))
- var worms_slot: int = bag.get_storage_slot(&"worms")
- var shrimp_slot: int = bag.get_storage_slot(&"shrimp")
- assert(worms_slot >= 0 and shrimp_slot >= 0 and worms_slot != shrimp_slot)
- assert(bag.move_item_to_storage_slot(&"worms", shrimp_slot))
- assert(bag.get_storage_slot(&"worms") == shrimp_slot)
- assert(bag.get_storage_slot(&"shrimp") == worms_slot)
- bag.free()
- player_menu.free()
- print("Tackle order validation: PASS")
- quit()
diff --git a/tests/tackle_order_validation.gd.uid b/tests/tackle_order_validation.gd.uid
deleted file mode 100644
index 353ba7e..0000000
--- a/tests/tackle_order_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://27tkdvxgkv3y
diff --git a/tests/terrain_biome_validation.gd b/tests/terrain_biome_validation.gd
deleted file mode 100644
index 968ff5e..0000000
--- a/tests/terrain_biome_validation.gd
+++ /dev/null
@@ -1,135 +0,0 @@
-extends SceneTree
-
-const BIOME_CATALOG: TerrainBiomeCatalog = preload(
- "res://world/generation/biomes/terrain_biome_catalog.tres"
-)
-const PROP_CATALOG: TerrainPropCatalog = preload(
- "res://world/generation/props/terrain_prop_catalog.tres"
-)
-const EXPECTED_BIOMES: Array[StringName] = [
- &"biome_plains",
- &"biome_forest",
- &"biome_pine_forest",
- &"biome_coast",
-]
-const TEST_SEED := 13001
-
-var _failures: Array[String] = []
-
-
-func _initialize() -> void:
- call_deferred(&"_run")
-
-
-func _run() -> void:
- _validate_catalog()
- _validate_assignment()
- _finish()
-
-
-func _validate_catalog() -> void:
- for error: String in BIOME_CATALOG.validation_errors():
- _check(false, error)
- _check(
- BIOME_CATALOG.definitions.size() == EXPECTED_BIOMES.size(),
- "The biome catalog must contain all initial authored profiles.",
- )
- for biome_id: StringName in EXPECTED_BIOMES:
- var biome := BIOME_CATALOG.definition_for_id(biome_id)
- _check(biome != null, "The biome catalog is missing %s." % biome_id)
- if biome == null:
- continue
- for rule: TerrainBiomePropRule in biome.prop_rules:
- for prop_id_value: String in rule.allowed_prop_ids:
- var prop := PROP_CATALOG.definition_for_id(
- StringName(prop_id_value)
- )
- _check(
- prop != null and rule.allows_prop(prop),
- "%s has an invalid %s prop rule."
- % [biome_id, prop_id_value],
- )
-
-
-func _validate_assignment() -> void:
- var records := _synthetic_records()
- var first := TerrainBiomeAssigner.assign(
- BIOME_CATALOG,
- records,
- TEST_SEED,
- )
- var second := TerrainBiomeAssigner.assign(
- BIOME_CATALOG,
- records,
- TEST_SEED,
- )
- _check(first == second, "Biome assignment must be deterministic by seed.")
- _check(
- TerrainBiomeAssigner.fingerprint(first).length() == 64,
- "Biome assignments must expose a SHA-256 fingerprint.",
- )
- var counts := TerrainBiomeAssigner.counts(first)
- for biome_id: StringName in EXPECTED_BIOMES:
- _check(
- counts.get(biome_id, 0) > 0,
- "The synthetic map must contain %s." % biome_id,
- )
- _check(
- first.get(Vector2i(3, 3), &"") == &"biome_plains",
- "The spawn-tagged center must remain plains.",
- )
- for record: Dictionary in records:
- var coordinate: Vector2i = record["coordinate"]
- var tags: PackedStringArray = record["tags"]
- var biome_id: StringName = first.get(coordinate, &"")
- if "fresh_water" in tags:
- _check(
- biome_id == &"",
- "Freshwater-only chunks must not inherit a land-prop biome.",
- )
- continue
- var biome := BIOME_CATALOG.definition_for_id(biome_id)
- _check(
- biome != null and biome.supports_chunk_tags(tags),
- "Biome %s does not support chunk %s."
- % [biome_id, coordinate],
- )
-
-
-func _synthetic_records() -> Array[Dictionary]:
- var result: Array[Dictionary] = []
- for row: int in 7:
- for column: int in 7:
- var coordinate := Vector2i(column, row)
- var tags := PackedStringArray(["land", "walkable", "grass"])
- if row == 0:
- tags = PackedStringArray(["land", "walkable", "sand"])
- elif coordinate == Vector2i(0, 6):
- tags = PackedStringArray([
- "land",
- "walkable",
- "fresh_water",
- "pond",
- ])
- elif coordinate == Vector2i(3, 3):
- tags.append("spawn")
- result.append({
- "coordinate": coordinate,
- "tags": tags,
- })
- return result
-
-
-func _check(condition: bool, message: String) -> void:
- if not condition:
- _failures.append(message)
-
-
-func _finish() -> void:
- if _failures.is_empty():
- print("Terrain biome validation: PASS")
- quit(0)
- return
- for failure: String in _failures:
- printerr("Terrain biome validation: ", failure)
- quit(1)
diff --git a/tests/terrain_biome_validation.gd.uid b/tests/terrain_biome_validation.gd.uid
deleted file mode 100644
index 40de9e4..0000000
--- a/tests/terrain_biome_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cxioi62f6oap0
diff --git a/tests/terrain_blender_material_validation.gd b/tests/terrain_blender_material_validation.gd
deleted file mode 100644
index fe3de22..0000000
--- a/tests/terrain_blender_material_validation.gd
+++ /dev/null
@@ -1,190 +0,0 @@
-extends SceneTree
-
-const REGION := preload("res://world/regions/starter_island_region.tscn")
-const TERRAIN_MODEL_PATH: String = (
- "res://art/exported/environment/terrain/starter_island.glb"
-)
-const EXPECTED_SURFACES: Array[String] = [
- "grass_lite",
- "dirt",
- "cliff_wall",
- "dirt_wall",
- "sand",
- "sandstone_heavy",
-]
-
-var failures: Array[String] = []
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var model_scene := load(TERRAIN_MODEL_PATH) as PackedScene
- _check(model_scene != null, "Starter-island GLB must load as a scene.")
- if model_scene == null:
- _finish()
- return
-
- var model_root := model_scene.instantiate()
- var model_terrain := _find_mesh(model_root, "starter_island")
- _check(model_terrain != null, "GLB must contain the starter_island mesh.")
- _check(_find_mesh(model_root, "Cube") == null, "GLB must not contain Cube.")
- if model_terrain != null:
- _validate_terrain_materials(model_terrain, "GLB")
- model_root.free()
-
- var region := REGION.instantiate()
- root.add_child(region)
- await process_frame
- var runtime_terrain := region.get_node_or_null(
- "Terrain/Visual/starter_island"
- ) as MeshInstance3D
- _check(runtime_terrain != null, "Region must contain the terrain mesh.")
- if runtime_terrain != null:
- _validate_terrain_materials(runtime_terrain, "Region")
- for surface_index: int in runtime_terrain.mesh.get_surface_count():
- _check(
- runtime_terrain.get_surface_override_material(surface_index) == null,
- "Region surface %d must use its Blender material." % surface_index,
- )
- _validate_region_collision(region)
- region.free()
- _finish()
-
-
-func _find_mesh(node: Node, mesh_name: String) -> MeshInstance3D:
- if node is MeshInstance3D and node.name == mesh_name:
- return node as MeshInstance3D
- for child: Node in node.get_children():
- var match := _find_mesh(child, mesh_name)
- if match != null:
- return match
- return null
-
-
-func _validate_terrain_materials(
- terrain: MeshInstance3D,
- context: String,
-) -> void:
- _check(terrain.mesh != null, "%s terrain must have a mesh." % context)
- if terrain.mesh == null:
- return
- _check(
- terrain.mesh.get_surface_count() == EXPECTED_SURFACES.size(),
- "%s terrain must have %d surfaces." % [
- context,
- EXPECTED_SURFACES.size(),
- ],
- )
- var checked_count := mini(
- terrain.mesh.get_surface_count(),
- EXPECTED_SURFACES.size(),
- )
- for surface_index: int in checked_count:
- var expected_name := EXPECTED_SURFACES[surface_index]
- _check(
- terrain.mesh.surface_get_name(surface_index) == expected_name,
- "%s surface %d must be named %s." % [
- context,
- surface_index,
- expected_name,
- ],
- )
- var material := terrain.get_active_material(surface_index)
- _check(
- material is StandardMaterial3D,
- "%s surface %s must use a Blender material." % [
- context,
- expected_name,
- ],
- )
- if material is StandardMaterial3D:
- var texture := (material as StandardMaterial3D).albedo_texture
- _check(
- texture != null,
- "%s surface %s must have an embedded texture." % [
- context,
- expected_name,
- ],
- )
- if texture != null:
- _check(
- texture.resource_path.begins_with(TERRAIN_MODEL_PATH + "::"),
- "%s surface %s must source its texture from the GLB." % [
- context,
- expected_name,
- ],
- )
-
-
-func _validate_region_collision(region: StarterIslandRegion) -> void:
- _check(
- region.rebuild_terrain_collision_on_ready,
- "Starter-island collision must rebuild from the imported GLB.",
- )
- var visual_root := region.get_node_or_null("Terrain/Visual") as Node3D
- var collision_shape := region.get_node_or_null(
- "Terrain/Collision/Shape"
- ) as CollisionShape3D
- _check(visual_root != null, "Region must contain its visual hierarchy.")
- _check(collision_shape != null, "Region must contain its collision owner.")
- if visual_root == null or collision_shape == null:
- return
- var concave_shape := collision_shape.shape as ConcavePolygonShape3D
- _check(
- concave_shape != null,
- "Region collision must be a generated concave polygon shape.",
- )
- if concave_shape == null:
- return
- var expected_faces := PackedVector3Array()
- var mesh_count := _append_collision_faces(
- region,
- visual_root,
- expected_faces,
- )
- var actual_faces := concave_shape.get_faces()
- _check(mesh_count > 1, "Collision must include the imported prop meshes.")
- _check(not expected_faces.is_empty(), "Imported meshes must provide collision.")
- _check(
- actual_faces == expected_faces,
- "Generated collision must exactly match every imported mesh transform.",
- )
-
-
-func _append_collision_faces(
- region: StarterIslandRegion,
- node: Node,
- faces: PackedVector3Array,
-) -> int:
- var mesh_count := 0
- if node is MeshInstance3D:
- var mesh_instance := node as MeshInstance3D
- if mesh_instance.mesh != null:
- var mesh_shape := mesh_instance.mesh.create_trimesh_shape()
- if mesh_shape != null:
- mesh_count += 1
- for face_vertex: Vector3 in mesh_shape.get_faces():
- faces.append(
- region.to_local(mesh_instance.to_global(face_vertex))
- )
- for child: Node in node.get_children():
- mesh_count += _append_collision_faces(region, child, faces)
- return mesh_count
-
-
-func _check(condition: bool, message: String) -> void:
- if not condition:
- failures.append(message)
-
-
-func _finish() -> void:
- if failures.is_empty():
- print("Blender terrain and collision validation: PASS")
- quit(0)
- return
- for failure: String in failures:
- printerr("Blender terrain and collision validation: ", failure)
- quit(1)
diff --git a/tests/terrain_blender_material_validation.gd.uid b/tests/terrain_blender_material_validation.gd.uid
deleted file mode 100644
index ad92fbb..0000000
--- a/tests/terrain_blender_material_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cto4rc6bn0n4o
diff --git a/tests/terrain_chunk_generator_validation.gd b/tests/terrain_chunk_generator_validation.gd
deleted file mode 100644
index ec736d9..0000000
--- a/tests/terrain_chunk_generator_validation.gd
+++ /dev/null
@@ -1,2082 +0,0 @@
-extends SceneTree
-
-const CATALOG: TerrainChunkCatalog = preload(
- "res://world/generation/chunks/terrain_chunk_catalog.tres"
-)
-const EXPECTED_IDS: Array[String] = [
- "chunk_0000",
- "chunk_0001",
- "chunk_0002",
- "chunk_0003",
- "chunk_0004",
- "chunk_0005",
- "chunk_0006",
- "chunk_0007",
- "chunk_0008",
- "chunk_0009",
- "chunk_0010",
- "chunk_0011",
- "chunk_0012",
- "chunk_0013",
- "chunk_0014",
- "chunk_0015",
- "chunk_0016",
- "chunk_0017",
- "chunk_0018",
- "chunk_0019",
- "chunk_0020",
- "chunk_0021",
- "chunk_0022",
- "chunk_0023",
- "chunk_0024",
- "chunk_0025",
- "chunk_0026",
- "chunk_0027",
- "chunk_0028",
- "chunk_0029",
- "chunk_0030",
- "chunk_0031",
- "chunk_0032",
- "chunk_0033",
- "chunk_0034",
- "chunk_0035",
- "chunk_0036",
- "chunk_0037",
- "chunk_0038",
- "chunk_9999",
- "chunk_9998",
- "chunk_9997",
- "chunk_9996",
- "chunk_9995",
- "chunk_spawn",
-]
-const REQUIRED_IDS: Array[String] = [
- "chunk_spawn",
- "chunk_0001",
- "chunk_0002",
- "chunk_0003",
- "chunk_0004",
-]
-const TEST_SEED := 13001
-const CONNECTOR_STRESS_SEED := 287
-const EXPECTED_SOLVER_VARIANT_COUNT := 35
-const EXPECTED_VARIANT_COUNTS: Dictionary[StringName, int] = {
- &"chunk_0000": 4,
- &"chunk_0001": 4,
- &"chunk_0002": 4,
- &"chunk_0003": 4,
- &"chunk_0004": 4,
- &"chunk_0005": 4,
- &"chunk_0006": 4,
- &"chunk_0007": 4,
- &"chunk_0008": 4,
- &"chunk_0009": 4,
- &"chunk_0010": 4,
- &"chunk_0011": 4,
- &"chunk_0012": 4,
- &"chunk_0013": 4,
- &"chunk_0014": 4,
- &"chunk_0015": 4,
- &"chunk_0016": 4,
- &"chunk_0017": 4,
- &"chunk_0018": 4,
- &"chunk_0019": 1,
- &"chunk_0020": 4,
- &"chunk_0021": 4,
- &"chunk_0022": 4,
- &"chunk_0023": 4,
- &"chunk_0024": 4,
- &"chunk_0025": 4,
- &"chunk_0026": 4,
- &"chunk_0027": 4,
- &"chunk_0028": 4,
- &"chunk_0029": 4,
- &"chunk_0030": 4,
- &"chunk_0031": 4,
- &"chunk_0032": 4,
- &"chunk_0033": 4,
- &"chunk_0034": 4,
- &"chunk_0035": 4,
- &"chunk_0036": 4,
- &"chunk_0037": 4,
- &"chunk_0038": 4,
- &"chunk_9999": 4,
- &"chunk_9998": 4,
- &"chunk_9997": 4,
- &"chunk_9996": 4,
- &"chunk_9995": 4,
- &"chunk_spawn": 1,
-}
-
-var _failures: Array[String] = []
-
-
-func _initialize() -> void:
- call_deferred(&"_run")
-
-
-func _run() -> void:
- _validate_catalog()
- _validate_profiles()
- _validate_second_tier_coastal_layout()
- _validate_generation()
- _finish()
-
-
-func _validate_catalog() -> void:
- for error: String in CATALOG.validation_errors():
- _check(false, error)
- _check(
- CATALOG.definitions.size() == EXPECTED_IDS.size(),
- "Catalog must contain the authored chunks and spawn definition.",
- )
- for stable_id: String in EXPECTED_IDS:
- var definition := CATALOG.definition_for_id(StringName(stable_id))
- _check(definition != null, "Catalog is missing %s." % stable_id)
- if definition == null:
- continue
- _check(
- definition.packed_scene != null,
- "%s must reference an imported GLB." % stable_id,
- )
- var grass := CATALOG.definition_for_id(&"chunk_0000")
- var sand := CATALOG.definition_for_id(&"chunk_0001")
- var beach := CATALOG.definition_for_id(&"chunk_0002")
- var grass_ocean_edge := CATALOG.definition_for_id(&"chunk_0005")
- var grass_beach_transition := CATALOG.definition_for_id(&"chunk_0006")
- var grass_ocean_corner := CATALOG.definition_for_id(&"chunk_0007")
- var beach_ocean_corner := CATALOG.definition_for_id(&"chunk_0008")
- var grass_sand_diagonal := CATALOG.definition_for_id(&"chunk_0013")
- var pond := CATALOG.definition_for_id(&"chunk_0004")
- var cliff_sea_edge := CATALOG.definition_for_id(&"chunk_0014")
- var cliff_sea_corner := CATALOG.definition_for_id(&"chunk_0015")
- var cliff_sea_transition_right := CATALOG.definition_for_id(&"chunk_9999")
- var cliff_sea_transition_left := CATALOG.definition_for_id(&"chunk_9998")
- var cliff_beach_transition_right := CATALOG.definition_for_id(&"chunk_9997")
- var cliff_beach_transition_left := CATALOG.definition_for_id(&"chunk_9996")
- var stream_variant := CATALOG.definition_for_id(&"chunk_0016")
- var lake_edge := CATALOG.definition_for_id(&"chunk_0017")
- var lake_edge_variant := CATALOG.definition_for_id(&"chunk_0037")
- var lake_corner := CATALOG.definition_for_id(&"chunk_0018")
- var lake_fill := CATALOG.definition_for_id(&"chunk_0019")
- var lake_diagonal := CATALOG.definition_for_id(&"chunk_0020")
- var lake_narrow := CATALOG.definition_for_id(&"chunk_0021")
- var lake_regular_to_narrow := CATALOG.definition_for_id(&"chunk_0022")
- var lake_narrow_to_regular := CATALOG.definition_for_id(&"chunk_0023")
- var river_edge := CATALOG.definition_for_id(&"chunk_0024")
- var river_edge_variant := CATALOG.definition_for_id(&"chunk_0025")
- var river_stone_bridge := CATALOG.definition_for_id(&"chunk_0038")
- var river_source_right := CATALOG.definition_for_id(&"chunk_0029")
- var river_source_left := CATALOG.definition_for_id(&"chunk_0030")
- var river_outlet_right := CATALOG.definition_for_id(&"chunk_0031")
- var river_outlet_left := CATALOG.definition_for_id(&"chunk_0032")
- var river_bend_c := CATALOG.definition_for_id(&"chunk_0033")
- var river_bend_b := CATALOG.definition_for_id(&"chunk_0034")
- var river_bend_a := CATALOG.definition_for_id(&"chunk_0035")
- var river_bend_d := CATALOG.definition_for_id(&"chunk_0036")
- var river_cap := CATALOG.definition_for_id(&"chunk_9995")
- var cliff_top := CATALOG.definition_for_id(&"chunk_0009")
- var cliff_corner := CATALOG.definition_for_id(&"chunk_0010")
- var cliff_edge := CATALOG.definition_for_id(&"chunk_0011")
- var cliff_ramp := CATALOG.definition_for_id(&"chunk_0012")
- var cliff_third_tier_edge := CATALOG.definition_for_id(&"chunk_0026")
- var cliff_third_tier_corner := CATALOG.definition_for_id(&"chunk_0027")
- var cliff_third_tier_top := CATALOG.definition_for_id(&"chunk_0028")
- var spawn := CATALOG.definition_for_id(&"chunk_spawn")
- _check(
- (
- grass != null
- and sand != null
- and beach != null
- and grass_ocean_edge != null
- and grass_beach_transition != null
- and grass_ocean_corner != null
- and beach_ocean_corner != null
- and grass_sand_diagonal != null
- and pond != null
- and cliff_sea_edge != null
- and cliff_sea_corner != null
- and cliff_sea_transition_right != null
- and cliff_sea_transition_left != null
- and cliff_beach_transition_right != null
- and cliff_beach_transition_left != null
- and stream_variant != null
- and lake_edge != null
- and lake_edge_variant != null
- and lake_corner != null
- and lake_fill != null
- and lake_diagonal != null
- and lake_narrow != null
- and lake_regular_to_narrow != null
- and lake_narrow_to_regular != null
- and river_edge != null
- and river_edge_variant != null
- and river_stone_bridge != null
- and river_source_right != null
- and river_source_left != null
- and river_outlet_right != null
- and river_outlet_left != null
- and river_bend_a != null
- and river_bend_b != null
- and river_bend_c != null
- and river_bend_d != null
- and river_cap != null
- and cliff_top != null
- and cliff_corner != null
- and cliff_edge != null
- and cliff_ramp != null
- and cliff_third_tier_edge != null
- and cliff_third_tier_corner != null
- and cliff_third_tier_top != null
- and spawn != null
- ),
- "The authored biome-rule definitions must be available.",
- )
- if (
- grass != null
- and sand != null
- and beach != null
- and grass_ocean_edge != null
- and grass_beach_transition != null
- and grass_ocean_corner != null
- and beach_ocean_corner != null
- and grass_sand_diagonal != null
- and pond != null
- and cliff_sea_edge != null
- and cliff_sea_corner != null
- and cliff_sea_transition_right != null
- and cliff_sea_transition_left != null
- and cliff_beach_transition_right != null
- and cliff_beach_transition_left != null
- and stream_variant != null
- and lake_edge != null
- and lake_edge_variant != null
- and lake_corner != null
- and lake_fill != null
- and lake_diagonal != null
- and lake_narrow != null
- and lake_regular_to_narrow != null
- and lake_narrow_to_regular != null
- and river_edge != null
- and river_edge_variant != null
- and river_stone_bridge != null
- and river_source_right != null
- and river_source_left != null
- and river_outlet_right != null
- and river_outlet_left != null
- and river_bend_a != null
- and river_bend_b != null
- and river_bend_c != null
- and river_bend_d != null
- and river_cap != null
- and cliff_top != null
- and cliff_corner != null
- and cliff_edge != null
- and cliff_ramp != null
- and cliff_third_tier_edge != null
- and cliff_third_tier_corner != null
- and cliff_third_tier_top != null
- and spawn != null
- ):
- _check(
- sand.allows_non_water_neighbor(beach),
- "Flat sand must permit an adjacent authored beach.",
- )
- _check(
- sand.allows_non_water_neighbor(grass),
- "Flat sand must permit the inland transition to grass.",
- )
- _check(
- grass.allows_non_water_neighbor(sand),
- "Grass must permit the inland side of a flat-sand transition.",
- )
- _check(
- (
- "sand" in sand.required_neighbor_tags
- and "coast" in sand.required_neighbor_tags
- and sand.maximum_boundary_distance == 1
- ),
- (
- "Every flat-sand chunk must remain attached to sand or authored "
- + "coast within the first inland terrain ring."
- ),
- )
- _check(
- spawn.minimum_required_neighbors == 3,
- "Spawn must retain its three-sided safe-grass requirement.",
- )
- _check(
- beach.ocean_facing_edges
- == (1 << int(TerrainChunkTopology.Edge.EAST)),
- "The authored beach must descend eastward into ocean before rotation.",
- )
- _check(
- grass_ocean_edge.ocean_facing_edges
- == (1 << int(TerrainChunkTopology.Edge.EAST)),
- "The authored grass cliff must face eastward ocean before rotation.",
- )
- _check(
- grass_beach_transition.ocean_facing_edges
- == (1 << int(TerrainChunkTopology.Edge.EAST)),
- "The authored grass/beach join must face eastward ocean before rotation.",
- )
- _check(
- grass_beach_transition.minimum_required_neighbors == 2
- and "coast" in grass_beach_transition.required_neighbor_tags,
- "The grass/beach join must remain between two coastline chunks.",
- )
- _check(
- grass_beach_transition.allows_non_water_neighbor_on_edge(
- grass,
- TerrainChunkTopology.Edge.WEST,
- )
- and not grass_beach_transition.allows_non_water_neighbor_on_edge(
- sand,
- TerrainChunkTopology.Edge.WEST,
- ),
- "The grass-backed transition edge must accept grass and reject sand.",
- )
- _check(
- grass_beach_transition.allows_non_water_neighbor_on_edge(
- beach,
- TerrainChunkTopology.Edge.NORTH,
- )
- and grass_beach_transition.allows_non_water_neighbor_on_edge(
- grass_ocean_edge,
- TerrainChunkTopology.Edge.SOUTH,
- ),
- "The transition's coastline seams must retain their authored roles.",
- )
- _check(
- not grass_ocean_edge.allows_non_water_neighbor_on_edge(
- sand,
- TerrainChunkTopology.Edge.WEST,
- ),
- "The straight grass coast's inland edge must reject sand.",
- )
- _check(
- grass_ocean_corner.ocean_facing_edges
- == (
- (1 << int(TerrainChunkTopology.Edge.EAST))
- | (1 << int(TerrainChunkTopology.Edge.SOUTH))
- ),
- "The authored grass corner must expose east and south to ocean.",
- )
- _check(
- grass_ocean_corner.minimum_required_neighbors == 2
- and (
- "grass_ocean_edge"
- in grass_ocean_corner.required_neighbor_tags
- ),
- "The grass corner must join two straight grass ocean edges.",
- )
- _check(
- grass_ocean_corner.allows_non_water_neighbor_on_edge(
- grass_ocean_edge,
- TerrainChunkTopology.Edge.NORTH,
- )
- and grass_ocean_corner.allows_non_water_neighbor_on_edge(
- grass_ocean_edge,
- TerrainChunkTopology.Edge.WEST,
- ),
- "Both corner land seams must accept straight grass ocean edges.",
- )
- _check(
- beach_ocean_corner.ocean_facing_edges
- == (
- (1 << int(TerrainChunkTopology.Edge.EAST))
- | (1 << int(TerrainChunkTopology.Edge.SOUTH))
- ),
- "The authored beach corner must expose east and south to ocean.",
- )
- _check(
- beach_ocean_corner.minimum_required_neighbors == 2
- and (
- "beach_ocean_edge"
- in beach_ocean_corner.required_neighbor_tags
- ),
- "The beach corner must join two straight beach ocean edges.",
- )
- _check(
- grass_sand_diagonal.must_be_interior
- and not grass_sand_diagonal.participates_in_base_solver
- and grass_sand_diagonal.maximum_boundary_distance == 2
- and "mixed_surface" in grass_sand_diagonal.tags,
- (
- "The grass-sand diagonal must remain an inland post-process "
- + "mixed-surface chunk."
- ),
- )
- _check(
- grass_sand_diagonal.surface_tags_for_edge(
- TerrainChunkTopology.Edge.NORTH
- ) == PackedStringArray(["sand"])
- and grass_sand_diagonal.surface_tags_for_edge(
- TerrainChunkTopology.Edge.WEST
- ) == PackedStringArray(["sand"])
- and grass_sand_diagonal.surface_tags_for_edge(
- TerrainChunkTopology.Edge.EAST
- ) == PackedStringArray(["grass"])
- and grass_sand_diagonal.surface_tags_for_edge(
- TerrainChunkTopology.Edge.SOUTH
- ) == PackedStringArray(["grass"]),
- "The diagonal's authored edges must retain two sand and two grass sides.",
- )
- var stream := CATALOG.definition_for_id(&"chunk_0003")
- _check(
- pond.water_inlet_edges == pond.water_outlet_edges
- and pond.water_inlet_edges
- == (1 << int(TerrainChunkTopology.Edge.NORTH)),
- "A pond must terminate either end of an inland freshwater chain.",
- )
- _check(
- pond.maximum_placements == 2,
- "Generated worlds must contain only one pond/stream chain.",
- )
- _check(
- stream != null
- and is_equal_approx(stream.selection_weight, 0.3)
- and is_equal_approx(stream_variant.selection_weight, 0.3)
- and stream.maximum_placements == 2
- and stream_variant.maximum_placements == 1
- and "pond" in stream.tags
- and "river" not in stream.tags
- and stream_variant.tags == stream.tags
- and stream_variant.allowed_neighbor_tags
- == stream.allowed_neighbor_tags
- and stream_variant.water_inlet_edges == stream.water_inlet_edges
- and stream_variant.water_outlet_edges == stream.water_outlet_edges
- and stream_variant.water_surface_size == stream.water_surface_size
- and stream.water_surface_offset.x < 0.0
- and stream_variant.water_surface_offset.x > 0.0
- and is_equal_approx(
- stream_variant.water_surface_offset.y,
- stream.water_surface_offset.y,
- ),
- (
- "Both pond-stream visuals must share one combined selection weight "
- + "and mirrored water footprints."
- ),
- )
- _check(
- (
- not lake_edge.participates_in_base_solver
- and not lake_edge_variant.participates_in_base_solver
- and not lake_corner.participates_in_base_solver
- and not lake_fill.participates_in_base_solver
- and not lake_diagonal.participates_in_base_solver
- and not lake_narrow.participates_in_base_solver
- and not lake_regular_to_narrow.participates_in_base_solver
- and not lake_narrow_to_regular.participates_in_base_solver
- and lake_edge.water_inlet_edges == lake_edge.water_outlet_edges
- and lake_edge.water_inlet_edges == 7
- and lake_edge_variant.water_inlet_edges
- == lake_edge.water_inlet_edges
- and lake_edge_variant.water_outlet_edges
- == lake_edge.water_outlet_edges
- and lake_edge_variant.water_surface_size
- == lake_edge.water_surface_size
- and lake_edge_variant.water_surface_offset
- == lake_edge.water_surface_offset
- and lake_corner.water_inlet_edges
- == lake_corner.water_outlet_edges
- and lake_corner.water_inlet_edges == 6
- and lake_fill.water_inlet_edges
- == lake_fill.water_outlet_edges
- and lake_fill.water_inlet_edges == 15
- and lake_fill.water_surface_size == Vector2(10.0, 10.0)
- and lake_diagonal.water_inlet_edges
- == lake_diagonal.water_outlet_edges
- and lake_diagonal.water_inlet_edges == 6
- and lake_diagonal.maximum_placements == 4
- and lake_diagonal.water_surface_polygon.size() == 6
- and lake_narrow.water_inlet_edges == 14
- and lake_narrow.water_outlet_edges == 14
- and lake_narrow.water_surface_polygon.size() == 4
- and lake_regular_to_narrow.water_inlet_edges == 14
- and lake_regular_to_narrow.water_outlet_edges == 14
- and lake_regular_to_narrow.water_surface_polygon.size() == 5
- and lake_narrow_to_regular.water_inlet_edges == 14
- and lake_narrow_to_regular.water_outlet_edges == 14
- and lake_narrow_to_regular.water_surface_polygon.size() == 5
- and "lake" in lake_edge.tags
- and "lake" in lake_edge_variant.tags
- and "lake" in lake_corner.tags
- and "lake" in lake_fill.tags
- and "lake" in lake_diagonal.tags
- and "lake" in lake_narrow.tags
- and "lake" in lake_regular_to_narrow.tags
- and "lake" in lake_narrow_to_regular.tags
- ),
- "The deterministic lake ring and bottom must remain connected freshwater chunks.",
- )
- _check(
- (
- not river_edge.participates_in_base_solver
- and not river_edge_variant.participates_in_base_solver
- and not river_stone_bridge.participates_in_base_solver
- and river_stone_bridge.maximum_placements == 2
- and not river_source_right.participates_in_base_solver
- and not river_source_left.participates_in_base_solver
- and not river_outlet_right.participates_in_base_solver
- and not river_outlet_left.participates_in_base_solver
- and not river_bend_a.participates_in_base_solver
- and not river_bend_b.participates_in_base_solver
- and not river_bend_c.participates_in_base_solver
- and not river_bend_d.participates_in_base_solver
- and not river_cap.participates_in_base_solver
- and river_edge.water_inlet_edges == 14
- and river_edge.water_outlet_edges == 14
- and river_edge_variant.water_inlet_edges == 14
- and river_edge_variant.water_outlet_edges == 14
- and river_stone_bridge.water_inlet_edges == 14
- and river_stone_bridge.water_outlet_edges == 14
- and river_source_right.water_inlet_edges == 6
- and river_source_right.water_outlet_edges == 6
- and river_source_left.water_inlet_edges == 3
- and river_source_left.water_outlet_edges == 3
- and river_outlet_right.water_inlet_edges == 14
- and river_outlet_right.water_outlet_edges == 14
- and river_outlet_left.water_inlet_edges == 11
- and river_outlet_left.water_outlet_edges == 11
- and river_bend_a.water_inlet_edges == 14
- and river_bend_a.water_outlet_edges == 14
- and river_bend_b.water_inlet_edges == 12
- and river_bend_b.water_outlet_edges == 12
- and river_bend_c.water_inlet_edges == 15
- and river_bend_c.water_outlet_edges == 15
- and river_bend_c.continuous_water_seam_edges == 15
- and river_bend_d.water_inlet_edges == 13
- and river_bend_d.water_outlet_edges == 13
- and river_bend_d.continuous_water_seam_edges == 1
- and river_cap.water_inlet_edges == 6
- and river_cap.water_outlet_edges == 6
- and river_edge.water_surface_polygon.size() == 13
- and river_edge_variant.water_surface_polygon.size() == 13
- and river_stone_bridge.water_surface_polygon.size() == 13
- and river_source_right.water_surface_polygon.size() == 13
- and river_source_left.water_surface_polygon.size() == 13
- and river_outlet_right.water_surface_polygon.size() == 19
- and river_outlet_left.water_surface_polygon.size() == 19
- and river_outlet_right.ocean_facing_edges == 2
- and river_outlet_left.ocean_facing_edges == 2
- and river_cap.water_surface_polygon.size() == 6
- and river_cap.primary_mesh_name == &"chunk_0020"
- and "river" in river_edge.tags
- and "river" in river_edge_variant.tags
- and "river_bridge" in river_stone_bridge.tags
- and "stepping_stones" in river_stone_bridge.tags
- and "river_source_right" in river_source_right.tags
- and "river_source_left" in river_source_left.tags
- and "river_outlet_right" in river_outlet_right.tags
- and "river_outlet_left" in river_outlet_left.tags
- and "river_bend_a" in river_bend_a.tags
- and "river_bend_b" in river_bend_b.tags
- and "river_bend_c" in river_bend_c.tags
- and "river_bend_d" in river_bend_d.tags
- and "river" in river_cap.tags
- ),
- (
- "The river banks, bend, cliff source, ocean outlet, and legacy "
- + "caps must remain connected freshwater chunks."
- ),
- )
- for cliff_definition: TerrainChunkDefinition in [
- cliff_top,
- cliff_corner,
- cliff_edge,
- cliff_ramp,
- ]:
- _check(
- cliff_definition.overlay_only
- and cliff_definition.must_be_interior
- and cliff_definition.base_layer_scene != null
- and cliff_definition.base_layer_mesh_name == &"chunk_0000",
- "Every second-tier piece must overlay level-one grass inland.",
- )
- for cliff_definition: TerrainChunkDefinition in [
- cliff_third_tier_top,
- cliff_third_tier_corner,
- cliff_third_tier_edge,
- ]:
- _check(
- cliff_definition.overlay_only
- and cliff_definition.must_be_interior
- and cliff_definition.base_layer_scene != null
- and cliff_definition.base_layer_mesh_name == &"chunk_0000"
- and "elevation_3" in cliff_definition.tags,
- "Every third-tier piece must overlay level-one grass inland.",
- )
- for coastal_cliff: TerrainChunkDefinition in [
- cliff_sea_edge,
- cliff_sea_corner,
- ]:
- _check(
- coastal_cliff.overlay_only
- and coastal_cliff.base_layer_scene == null
- and not coastal_cliff.must_be_interior
- and coastal_cliff.prefers_map_boundary
- and "coast" in coastal_cliff.tags
- and "elevation_2_sea_edge" in coastal_cliff.tags,
- (
- "Each second-tier sea piece must be a complete coastal "
- + "overlay without an inland grass base."
- ),
- )
- for transition: TerrainChunkDefinition in [
- cliff_sea_transition_right,
- cliff_sea_transition_left,
- ]:
- _check(
- transition.overlay_only
- and transition.primary_mesh_name == &"chunk_0010"
- and transition.base_layer_scene != null
- and transition.base_layer_mesh_name == &"chunk_0007"
- and not transition.must_be_interior
- and transition.prefers_map_boundary
- and "transition" in transition.tags,
- (
- "Each raised-coast endpoint must layer the inland corner "
- + "over an authored level-one coastline."
- ),
- )
- _check(
- cliff_sea_transition_right.base_layer_quarter_turns == 3
- and cliff_sea_transition_right.ocean_facing_edges
- == (1 << int(TerrainChunkTopology.Edge.SOUTH)),
- "The right coastal transition must align its south-facing base coast.",
- )
- _check(
- cliff_sea_transition_left.base_layer_quarter_turns == 1
- and cliff_sea_transition_left.ocean_facing_edges
- == (1 << int(TerrainChunkTopology.Edge.EAST)),
- "The left coastal transition must align its east-facing base coast.",
- )
- for transition: TerrainChunkDefinition in [
- cliff_beach_transition_right,
- cliff_beach_transition_left,
- ]:
- _check(
- transition.overlay_only
- and transition.primary_mesh_name == &"chunk_0010"
- and transition.base_layer_scene != null
- and transition.base_layer_mesh_name == &"chunk_0008"
- and not transition.must_be_interior
- and transition.prefers_map_boundary
- and "sand" in transition.tags
- and "transition" in transition.tags,
- (
- "Each raised beach endpoint must close over an authored "
- + "level-one beach corner."
- ),
- )
- _check(
- cliff_beach_transition_right.base_layer_quarter_turns == 3
- and cliff_beach_transition_right.ocean_facing_edges
- == (1 << int(TerrainChunkTopology.Edge.SOUTH))
- and cliff_beach_transition_left.base_layer_quarter_turns == 1
- and cliff_beach_transition_left.ocean_facing_edges
- == (1 << int(TerrainChunkTopology.Edge.EAST)),
- "The raised beach endpoint corners must face their open coastline.",
- )
- _check(
- cliff_sea_edge.ocean_facing_edges
- == (1 << int(TerrainChunkTopology.Edge.EAST)),
- "The second-tier sea edge must face east before rotation.",
- )
- _check(
- cliff_sea_corner.ocean_facing_edges
- == (
- (1 << int(TerrainChunkTopology.Edge.EAST))
- | (1 << int(TerrainChunkTopology.Edge.SOUTH))
- ),
- "The second-tier sea corner must face east and south before rotation.",
- )
- _check(
- cliff_sea_corner.minimum_required_neighbors == 2
- and (
- "elevation_2_sea_edge"
- in cliff_sea_corner.required_neighbor_tags
- ),
- "The second-tier sea corner must join two straight sea edges.",
- )
- _check(
- cliff_top.minimum_required_neighbors == 4
- and "elevation_2" in cliff_top.required_neighbor_tags,
- "The second-tier top must be enclosed on all four sides.",
- )
- _check(
- "ramp" in cliff_ramp.tags
- and cliff_ramp.maximum_placements > 0,
- "The second-tier ramp must remain available without being required.",
- )
- _check(
- cliff_third_tier_top.minimum_required_neighbors == 4
- and "elevation_3" in cliff_third_tier_top.required_neighbor_tags
- and "ramp" not in cliff_third_tier_edge.tags
- and "ramp" not in cliff_third_tier_corner.tags,
- "The third-tier set must form a closed, intentionally unramped plateau.",
- )
-
-
-func _validate_profiles() -> void:
- for definition: TerrainChunkDefinition in CATALOG.definitions:
- var variants := TerrainChunkAnalyzer.create_variants(
- definition,
- CATALOG.chunk_size,
- )
- _check(
- not variants.is_empty(),
- "%s must produce at least one rotation variant."
- % definition.stable_id,
- )
- for variant: TerrainChunkVariant in variants:
- _check(
- variant.edge_profiles.size() == 4,
- "%s must expose four edge profiles." % variant.stable_key(),
- )
- var ocean_edges := variant.rotated_edge_mask(
- definition.ocean_facing_edges
- )
- for edge_index: int in variant.edge_profiles.size():
- var profile := variant.edge_profiles[edge_index]
- _check(
- (
- not profile.points.is_empty()
- or (ocean_edges & (1 << edge_index)) != 0
- ),
- (
- "%s has an empty non-ocean edge profile."
- % variant.stable_key()
- ),
- )
-
-
-func _validate_second_tier_coastal_layout() -> void:
- var generator := TerrainChunkGenerator.new()
- generator.catalog = CATALOG
- generator.grid_size = Vector2i(4, 4)
- generator.generate_on_ready = false
- generator.build_collision = false
- generator.force_center_chunk_id = &""
- root.add_child(generator)
- var keys := PackedStringArray()
- for row: int in generator.grid_size.y:
- for column: int in generator.grid_size.x:
- var at_north := row == 0
- var at_east := column == generator.grid_size.x - 1
- var at_south := row == generator.grid_size.y - 1
- var at_west := column == 0
- if at_north and at_west:
- keys.append("chunk_0015@2")
- elif at_north and at_east:
- keys.append("chunk_0015@1")
- elif at_south and at_east:
- keys.append("chunk_0015@0")
- elif at_south and at_west:
- keys.append("chunk_0015@3")
- elif at_north:
- keys.append("chunk_0014@1")
- elif at_east:
- keys.append("chunk_0014@0")
- elif at_south:
- keys.append("chunk_0014@3")
- elif at_west:
- keys.append("chunk_0014@2")
- else:
- keys.append("chunk_0009@0")
- _check(
- generator.generate_from_placement_keys(keys),
- (
- "The second-tier sea edge and corner must form a complete "
- + "raised coastal perimeter."
- ),
- )
- if generator.placement_keys().size() == keys.size():
- _check(
- _all_neighbor_edges_match(generator),
- "Every raised coastal-perimeter seam must match.",
- )
- _check(
- _count_placements(generator.placement_keys(), "chunk_0014") == 8
- and _count_placements(
- generator.placement_keys(), "chunk_0015"
- ) == 4,
- "The raised coastal perimeter must use its straight and corner pieces.",
- )
- for record: Dictionary in generator.placement_records():
- var stable_id: StringName = record.get("stable_id", &"")
- if stable_id not in [&"chunk_0014", &"chunk_0015"]:
- continue
- var coordinate: Vector2i = record.get(
- "coordinate", Vector2i.ZERO
- )
- var variant := _find_variant(
- generator,
- stable_id,
- int(record.get("rotation_quarters", 0)),
- )
- _check(
- variant != null
- and generator._variant_respects_ocean_boundary(
- variant, coordinate
- ),
- "Every second-tier sea opening must face outside the terrain grid.",
- )
- var generated := generator.get_generated_chunks_root()
- if generated != null:
- for child: Node in generated.get_children():
- var stable_id := StringName(
- child.get_meta(&"terrain_chunk_id", &"")
- )
- if stable_id in [&"chunk_0014", &"chunk_0015"]:
- _check(
- child.get_node_or_null("TerrainBaseLayer") == null,
- "Second-tier sea pieces must not render inland grass over ocean.",
- )
- generator.free()
-
-
-func _validate_generation() -> void:
- var first := _make_generator()
- root.add_child(first)
- var first_solved := first.generate()
- _check(first_solved, "Generator must solve the prototype 9x9 grid.")
- if not first_solved:
- first.free()
- return
- var first_keys := first.placement_keys()
- _check(first_keys.size() == 81, "Generator must place exactly 81 chunks.")
- for stable_id: String in REQUIRED_IDS:
- _check(
- _placements_contain(first_keys, stable_id),
- "Generated diagnostic must contain %s." % stable_id,
- )
- _check(
- _all_neighbor_edges_match(first),
- "Every generated neighboring edge must match.",
- )
- _check(
- _count_placements(first_keys, "chunk_spawn") == 1,
- "Generated layouts must contain exactly one spawn chunk.",
- )
- _check(
- first_keys[40].begins_with("chunk_spawn@"),
- "The spawn chunk must occupy the center cell.",
- )
- _validate_layout_rules(first)
- _validate_authored_rotations(first)
- _validate_generation_summary(first)
- var first_generated_root := first.get_generated_chunks_root()
- _check(
- first.generate(),
- "Repeated generation on one generator must solve.",
- )
- _check(
- first.placement_keys() == first_keys,
- "Repeated generation on one generator must remain deterministic.",
- )
- _check(
- first.get_generated_chunks_root() != first_generated_root,
- "Successful regeneration must replace the generated scene atomically.",
- )
-
- var second := _make_generator()
- root.add_child(second)
- var second_solved := second.generate()
- _check(second_solved, "Repeated deterministic generation must solve.")
- if not second_solved:
- first.free()
- second.free()
- return
- _check(
- second.placement_keys() == first_keys,
- "The same seed and catalog must produce the same layout.",
- )
- var restored := _make_generator()
- restored.generation_seed = TEST_SEED + 999
- root.add_child(restored)
- _check(
- restored.generate_from_placement_keys(first_keys),
- "A resolved placement manifest must rebuild successfully.",
- )
- _check(
- restored.placement_keys() == first_keys,
- "A resolved placement manifest must preserve exact rotations.",
- )
- first.free()
- second.free()
- restored.free()
-
- for seed_offset: int in range(1, 6):
- var generator := _make_generator()
- generator.generation_seed = TEST_SEED + seed_offset
- root.add_child(generator)
- var solved := generator.generate()
- _check(
- solved,
- "Generator must solve seed %d." % generator.generation_seed,
- )
- if solved:
- var keys := generator.placement_keys()
- for stable_id: String in REQUIRED_IDS:
- _check(
- _placements_contain(keys, stable_id),
- "Seed %d must contain %s."
- % [generator.generation_seed, stable_id],
- )
- _check(
- _all_neighbor_edges_match(generator),
- "Seed %d has a mismatched neighboring edge."
- % generator.generation_seed,
- )
- _validate_layout_rules(generator)
- generator.free()
-
- var connector_stress := _make_generator()
- connector_stress.generation_seed = CONNECTOR_STRESS_SEED
- connector_stress.maximum_backtracks = 100
- root.add_child(connector_stress)
- _check(
- connector_stress.generate(),
- "Connector propagation must solve the known stress seed.",
- )
- _check(
- connector_stress._backtrack_count < 100,
- "Connector propagation must reject unsupported branches early.",
- )
- connector_stress.free()
-
-
-func _validate_authored_rotations(generator: TerrainChunkGenerator) -> void:
- var counts: Dictionary[StringName, int] = {}
- for variant: TerrainChunkVariant in generator._variants:
- var stable_id := variant.definition.stable_id
- counts[stable_id] = counts.get(stable_id, 0) + 1
- for stable_id: StringName in EXPECTED_VARIANT_COUNTS:
- _check(
- counts.get(stable_id, 0) == EXPECTED_VARIANT_COUNTS[stable_id],
- "%s must preserve every explicitly allowed rotation." % stable_id,
- )
-
- var stream := CATALOG.definition_for_id(&"chunk_0003")
- var alternate_stream := CATALOG.definition_for_id(&"chunk_0016")
- _check(
- stream != null and alternate_stream != null,
- "Both stream visual definitions must be available.",
- )
- if stream == null or alternate_stream == null:
- return
- for quarter_turns: int in 4:
- var stream_variant := _find_variant(
- generator,
- &"chunk_0003",
- quarter_turns,
- )
- _check(
- stream_variant != null,
- "The stream must support rotation %d." % quarter_turns,
- )
- var alternate_variant := _find_variant(
- generator,
- &"chunk_0016",
- quarter_turns,
- )
- _check(
- alternate_variant != null,
- "The alternate stream must support rotation %d." % quarter_turns,
- )
- if stream_variant == null or alternate_variant == null:
- continue
- var expected_inlet := 1 << int(TerrainChunkTopology.rotated_edge(
- TerrainChunkTopology.Edge.NORTH,
- quarter_turns,
- ))
- var expected_outlet := 1 << int(TerrainChunkTopology.rotated_edge(
- TerrainChunkTopology.Edge.SOUTH,
- quarter_turns,
- ))
- _check(
- stream_variant.rotated_edge_mask(stream.water_inlet_edges)
- == expected_inlet,
- "The rotated stream inlet must follow its authored orientation.",
- )
- _check(
- stream_variant.rotated_edge_mask(stream.water_outlet_edges)
- == expected_outlet,
- "The rotated stream outlet must follow its authored orientation.",
- )
- _check(
- alternate_variant.rotated_edge_mask(
- alternate_stream.water_inlet_edges
- ) == expected_inlet
- and alternate_variant.rotated_edge_mask(
- alternate_stream.water_outlet_edges
- ) == expected_outlet,
- "The alternate stream connectors must rotate exactly like the original.",
- )
- for edge_value: int in TerrainChunkTopology.Edge.values():
- var edge := edge_value as TerrainChunkTopology.Edge
- _check(
- stream_variant.profile(edge).signature(
- TerrainChunkGenerator.CONSTRAINT_PROFILE_QUANTIZATION
- ) == alternate_variant.profile(edge).signature(
- TerrainChunkGenerator.CONSTRAINT_PROFILE_QUANTIZATION
- ),
- "Both stream visuals must expose identical terrain edge profiles.",
- )
-
- var beach := CATALOG.definition_for_id(&"chunk_0002")
- _check(beach != null, "The beach definition must be available.")
- if beach == null:
- return
- for quarter_turns: int in 4:
- var beach_variant := _find_variant(
- generator,
- &"chunk_0002",
- quarter_turns,
- )
- _check(
- beach_variant != null,
- "The beach must support rotation %d." % quarter_turns,
- )
- if beach_variant == null:
- continue
- var expected_ocean_edge := 1 << int(
- TerrainChunkTopology.rotated_edge(
- TerrainChunkTopology.Edge.EAST,
- quarter_turns,
- )
- )
- _check(
- beach_variant.rotated_edge_mask(beach.ocean_facing_edges)
- == expected_ocean_edge,
- "The rotated beach must keep its low edge facing the ocean.",
- )
- _check(
- not generator._variant_respects_ocean_boundary(
- beach_variant,
- Vector2i(3, 3),
- ),
- "A beach slope must never direct its low edge into the map.",
- )
- var boundary_coordinate := Vector2i(3, 3)
- match TerrainChunkTopology.rotated_edge(
- TerrainChunkTopology.Edge.EAST,
- quarter_turns,
- ):
- TerrainChunkTopology.Edge.NORTH:
- boundary_coordinate.y = 0
- TerrainChunkTopology.Edge.EAST:
- boundary_coordinate.x = generator.grid_size.x - 1
- TerrainChunkTopology.Edge.SOUTH:
- boundary_coordinate.y = generator.grid_size.y - 1
- TerrainChunkTopology.Edge.WEST:
- boundary_coordinate.x = 0
- _check(
- generator._variant_respects_ocean_boundary(
- beach_variant,
- boundary_coordinate,
- ),
- "A beach slope must be legal when its low edge faces open ocean.",
- )
-
- var grass_ocean_edge := CATALOG.definition_for_id(&"chunk_0005")
- var grass_beach_transition := CATALOG.definition_for_id(&"chunk_0006")
- var grass_ocean_corner := CATALOG.definition_for_id(&"chunk_0007")
- var beach_ocean_corner := CATALOG.definition_for_id(&"chunk_0008")
- _check(
- (
- grass_ocean_edge != null
- and grass_beach_transition != null
- and grass_ocean_corner != null
- and beach_ocean_corner != null
- ),
- "The authored coastline additions must be available.",
- )
- if (
- grass_ocean_edge == null
- or grass_beach_transition == null
- or grass_ocean_corner == null
- or beach_ocean_corner == null
- ):
- return
- var beach_zero := _find_variant(generator, &"chunk_0002", 0)
- var grass_edge_zero := _find_variant(generator, &"chunk_0005", 0)
- var grass_edge_south := _find_variant(generator, &"chunk_0005", 3)
- var transition_zero := _find_variant(generator, &"chunk_0006", 0)
- var corner_zero := _find_variant(generator, &"chunk_0007", 0)
- var beach_corner_zero := _find_variant(generator, &"chunk_0008", 0)
- _check(
- beach_zero != null
- and grass_edge_zero != null
- and grass_edge_south != null
- and transition_zero != null
- and corner_zero != null
- and beach_corner_zero != null,
- "The unrotated coastline variants must be available.",
- )
- if (
- beach_zero != null
- and grass_edge_zero != null
- and grass_edge_south != null
- and transition_zero != null
- and corner_zero != null
- and beach_corner_zero != null
- ):
- _check(
- grass_edge_zero.profile(
- TerrainChunkTopology.Edge.EAST
- ).points.is_empty(),
- "The grass cliff's authored east edge must remain open to ocean.",
- )
- _check(
- transition_zero.profile(
- TerrainChunkTopology.Edge.NORTH
- ).matches(
- beach_zero.profile(TerrainChunkTopology.Edge.SOUTH),
- generator.edge_match_tolerance,
- ),
- "The transition's north side must match the authored beach.",
- )
- _check(
- transition_zero.profile(
- TerrainChunkTopology.Edge.SOUTH
- ).matches(
- grass_edge_zero.profile(TerrainChunkTopology.Edge.NORTH),
- generator.edge_match_tolerance,
- ),
- "The transition's south side must match the grass cliff.",
- )
- _check(
- corner_zero.profile(TerrainChunkTopology.Edge.EAST).points.is_empty()
- and corner_zero.profile(
- TerrainChunkTopology.Edge.SOUTH
- ).points.is_empty(),
- "The corner's authored east and south edges must remain open to ocean.",
- )
- _check(
- corner_zero.profile(TerrainChunkTopology.Edge.NORTH).matches(
- grass_edge_zero.profile(TerrainChunkTopology.Edge.SOUTH),
- generator.edge_match_tolerance,
- ),
- "The corner's north side must join a straight grass ocean edge.",
- )
- _check(
- corner_zero.profile(TerrainChunkTopology.Edge.WEST).matches(
- grass_edge_south.profile(TerrainChunkTopology.Edge.EAST),
- generator.edge_match_tolerance,
- ),
- "The corner's west side must join a straight grass ocean edge.",
- )
- _check(
- beach_corner_zero.profile(TerrainChunkTopology.Edge.NORTH).matches(
- beach_zero.profile(TerrainChunkTopology.Edge.SOUTH),
- generator.edge_match_tolerance,
- ),
- "The beach corner's north side must join a straight beach.",
- )
- var beach_south := _find_variant(generator, &"chunk_0002", 3)
- _check(
- beach_south != null
- and beach_corner_zero.profile(
- TerrainChunkTopology.Edge.WEST
- ).matches(
- beach_south.profile(TerrainChunkTopology.Edge.EAST),
- generator.edge_match_tolerance,
- ),
- "The beach corner's west side must join a rotated straight beach.",
- )
- var cliff_top_zero := _find_variant(generator, &"chunk_0009", 0)
- var cliff_edge_zero := _find_variant(generator, &"chunk_0011", 0)
- var cliff_edge_south := _find_variant(generator, &"chunk_0011", 3)
- var cliff_sea_edge_zero := _find_variant(generator, &"chunk_0014", 0)
- var cliff_sea_edge_south := _find_variant(generator, &"chunk_0014", 3)
- var cliff_sea_corner_zero := _find_variant(generator, &"chunk_0015", 0)
- _check(
- cliff_top_zero != null
- and cliff_edge_zero != null
- and cliff_edge_south != null
- and cliff_sea_edge_zero != null
- and cliff_sea_edge_south != null
- and cliff_sea_corner_zero != null,
- "The inland and coastal second-tier cliff variants must be available.",
- )
- if (
- cliff_top_zero != null
- and cliff_edge_zero != null
- and cliff_edge_south != null
- and cliff_sea_edge_zero != null
- and cliff_sea_edge_south != null
- and cliff_sea_corner_zero != null
- ):
- _check(
- cliff_sea_edge_zero.profile(
- TerrainChunkTopology.Edge.EAST
- ).points.is_empty(),
- "The second-tier sea edge must remain open on its ocean side.",
- )
- _check(
- cliff_sea_corner_zero.profile(
- TerrainChunkTopology.Edge.EAST
- ).points.is_empty()
- and cliff_sea_corner_zero.profile(
- TerrainChunkTopology.Edge.SOUTH
- ).points.is_empty(),
- "The second-tier sea corner must retain both ocean openings.",
- )
- _check(
- generator._edges_are_compatible(
- cliff_sea_edge_zero,
- TerrainChunkTopology.Edge.WEST,
- cliff_top_zero,
- TerrainChunkTopology.Edge.EAST,
- ),
- "The second-tier sea edge must join the raised grass top inland.",
- )
- _check(
- generator._edges_are_compatible(
- cliff_sea_corner_zero,
- TerrainChunkTopology.Edge.NORTH,
- cliff_sea_edge_zero,
- TerrainChunkTopology.Edge.SOUTH,
- )
- and generator._edges_are_compatible(
- cliff_sea_corner_zero,
- TerrainChunkTopology.Edge.WEST,
- cliff_sea_edge_south,
- TerrainChunkTopology.Edge.EAST,
- ),
- "The second-tier sea corner must join two straight sea edges.",
- )
- _check(
- generator._edges_are_compatible(
- cliff_sea_edge_zero,
- TerrainChunkTopology.Edge.NORTH,
- cliff_edge_zero,
- TerrainChunkTopology.Edge.SOUTH,
- )
- and generator._edges_are_compatible(
- cliff_sea_edge_zero,
- TerrainChunkTopology.Edge.SOUTH,
- cliff_edge_zero,
- TerrainChunkTopology.Edge.NORTH,
- ),
- (
- "A coastal cliff edge must transition directly into its "
- + "inland edge counterpart."
- ),
- )
- _check(
- generator._edges_are_compatible(
- cliff_sea_corner_zero,
- TerrainChunkTopology.Edge.NORTH,
- cliff_edge_zero,
- TerrainChunkTopology.Edge.SOUTH,
- )
- and generator._edges_are_compatible(
- cliff_sea_corner_zero,
- TerrainChunkTopology.Edge.WEST,
- cliff_edge_south,
- TerrainChunkTopology.Edge.EAST,
- ),
- (
- "A coastal cliff corner must transition directly into "
- + "ordinary inland cliff edges."
- ),
- )
- var flat_grass := _find_variant(generator, &"chunk_0000", 0)
- var flat_sand := _find_variant(generator, &"chunk_0001", 0)
- _check(
- flat_grass != null and flat_sand != null,
- "Flat grass and sand variants must be available for seam validation.",
- )
- if flat_grass != null and flat_sand != null:
- for quarter_turns: int in 4:
- var transition := _find_variant(
- generator,
- &"chunk_0006",
- quarter_turns,
- )
- if transition == null:
- continue
- var inland_edge := TerrainChunkTopology.rotated_edge(
- TerrainChunkTopology.Edge.WEST,
- quarter_turns,
- )
- var neighbor_edge := TerrainChunkTopology.opposite_edge(inland_edge)
- _check(
- generator._edges_are_compatible(
- transition,
- inland_edge,
- flat_grass,
- neighbor_edge,
- ),
- "Every transition rotation must accept grass behind it.",
- )
- _check(
- not generator._edges_are_compatible(
- transition,
- inland_edge,
- flat_sand,
- neighbor_edge,
- ),
- "No transition rotation may accept sand behind it.",
- )
- for quarter_turns: int in 4:
- var diagonal := _find_variant(
- generator,
- &"chunk_0013",
- quarter_turns,
- )
- _check(
- diagonal != null,
- "The grass-sand diagonal must support rotation %d."
- % quarter_turns,
- )
- if diagonal == null:
- continue
- for source_edge_value: int in TerrainChunkTopology.Edge.values():
- var source_edge := source_edge_value as TerrainChunkTopology.Edge
- var edge := TerrainChunkTopology.rotated_edge(
- source_edge,
- quarter_turns,
- )
- var neighbor_edge := TerrainChunkTopology.opposite_edge(edge)
- var expects_sand := source_edge in [
- TerrainChunkTopology.Edge.NORTH,
- TerrainChunkTopology.Edge.WEST,
- ]
- var expected := flat_sand if expects_sand else flat_grass
- var rejected := flat_grass if expects_sand else flat_sand
- _check(
- generator._edges_are_compatible(
- diagonal,
- edge,
- expected,
- neighbor_edge,
- ),
- "Each rotated diagonal edge must accept its authored surface.",
- )
- _check(
- not generator._edges_are_compatible(
- diagonal,
- edge,
- rejected,
- neighbor_edge,
- ),
- "No rotated diagonal edge may accept the opposite surface.",
- )
- var lake_diagonal_northwest := _find_variant(generator, &"chunk_0020", 0)
- var lake_diagonal_northeast := _find_variant(generator, &"chunk_0020", 3)
- var lake_narrow_top := _find_variant(generator, &"chunk_0021", 0)
- var lake_narrow_left := _find_variant(generator, &"chunk_0021", 1)
- var lake_regular_to_narrow := _find_variant(generator, &"chunk_0022", 0)
- var lake_narrow_to_regular := _find_variant(generator, &"chunk_0023", 0)
- var lake_regular_top := _find_variant(generator, &"chunk_0017", 3)
- var lake_regular_variant_top := _find_variant(generator, &"chunk_0037", 3)
- _check(
- (
- lake_diagonal_northwest != null
- and lake_diagonal_northeast != null
- and lake_narrow_top != null
- and lake_narrow_left != null
- and lake_regular_to_narrow != null
- and lake_narrow_to_regular != null
- and lake_regular_top != null
- and lake_regular_variant_top != null
- ),
- "Every authored diagonal lake perimeter variant must be available.",
- )
- if (
- lake_diagonal_northwest != null
- and lake_diagonal_northeast != null
- and lake_narrow_top != null
- and lake_narrow_left != null
- and lake_regular_to_narrow != null
- and lake_narrow_to_regular != null
- and lake_regular_top != null
- and lake_regular_variant_top != null
- ):
- _check(
- lake_regular_variant_top.topology_signature(0.001)
- == lake_regular_top.topology_signature(0.001),
- "Both regular lake edge visuals must expose identical seams.",
- )
- _check(
- generator._edges_are_compatible(
- lake_diagonal_northwest,
- TerrainChunkTopology.Edge.EAST,
- lake_narrow_top,
- TerrainChunkTopology.Edge.WEST,
- )
- and generator._edges_are_compatible(
- lake_diagonal_northwest,
- TerrainChunkTopology.Edge.SOUTH,
- lake_narrow_left,
- TerrainChunkTopology.Edge.NORTH,
- ),
- "Both diagonal banks must join the exact narrow lake profile.",
- )
- _check(
- generator._edges_are_compatible(
- lake_narrow_top,
- TerrainChunkTopology.Edge.EAST,
- lake_narrow_to_regular,
- TerrainChunkTopology.Edge.WEST,
- )
- and generator._edges_are_compatible(
- lake_narrow_to_regular,
- TerrainChunkTopology.Edge.EAST,
- lake_regular_top,
- TerrainChunkTopology.Edge.WEST,
- )
- and generator._edges_are_compatible(
- lake_regular_top,
- TerrainChunkTopology.Edge.EAST,
- lake_regular_to_narrow,
- TerrainChunkTopology.Edge.WEST,
- )
- and generator._edges_are_compatible(
- lake_regular_to_narrow,
- TerrainChunkTopology.Edge.EAST,
- lake_narrow_top,
- TerrainChunkTopology.Edge.WEST,
- ),
- "The narrow lake run must transition exactly into the regular edge.",
- )
- _check(
- generator._edges_are_compatible(
- lake_diagonal_northwest,
- TerrainChunkTopology.Edge.EAST,
- lake_narrow_to_regular,
- TerrainChunkTopology.Edge.WEST,
- )
- and generator._edges_are_compatible(
- lake_narrow_to_regular,
- TerrainChunkTopology.Edge.EAST,
- lake_regular_to_narrow,
- TerrainChunkTopology.Edge.WEST,
- )
- and generator._edges_are_compatible(
- lake_regular_to_narrow,
- TerrainChunkTopology.Edge.EAST,
- lake_diagonal_northeast,
- TerrainChunkTopology.Edge.WEST,
- ),
- "The five-chunk lake side must remain valid without a narrow run.",
- )
- var river_cap_northwest := _find_variant(generator, &"chunk_9995", 0)
- var river_cap_northeast := _find_variant(generator, &"chunk_9995", 3)
- var river_cap_southwest := _find_variant(generator, &"chunk_9995", 1)
- var river_cap_southeast := _find_variant(generator, &"chunk_9995", 2)
- var river_edge_top := _find_variant(generator, &"chunk_0024", 0)
- var river_edge_variant_top := _find_variant(generator, &"chunk_0025", 0)
- var river_edge_variant_bottom := _find_variant(generator, &"chunk_0025", 2)
- var river_edge_vertical_left := _find_variant(generator, &"chunk_0024", 1)
- var river_edge_vertical_right := _find_variant(generator, &"chunk_0025", 3)
- var river_stones_top := _find_variant(generator, &"chunk_0038", 0)
- var river_stones_bottom := _find_variant(generator, &"chunk_0038", 2)
- var river_stones_left := _find_variant(generator, &"chunk_0038", 1)
- var river_stones_right := _find_variant(generator, &"chunk_0038", 3)
- var river_source_right := _find_variant(generator, &"chunk_0029", 0)
- var river_source_left := _find_variant(generator, &"chunk_0030", 0)
- var river_outlet_right := _find_variant(generator, &"chunk_0031", 0)
- var river_outlet_left := _find_variant(generator, &"chunk_0032", 0)
- var river_bend_a := _find_variant(generator, &"chunk_0035", 0)
- var river_bend_b := _find_variant(generator, &"chunk_0034", 0)
- var river_bend_c := _find_variant(generator, &"chunk_0033", 2)
- var river_bend_d := _find_variant(generator, &"chunk_0036", 0)
- var beach_east := _find_variant(generator, &"chunk_0002", 0)
- var canonical_bend_specs := generator._river_bend_placement_specs(
- Vector2i(5, 5),
- 0,
- )
- var rotated_bend_specs := generator._river_bend_placement_specs(
- Vector2i(5, 5),
- 1,
- )
- _check(
- (
- canonical_bend_specs.size() == 4
- and canonical_bend_specs[2]["id"] == &"chunk_0033"
- and int(canonical_bend_specs[2]["turns"]) == 2
- and rotated_bend_specs.size() == 4
- and rotated_bend_specs[2]["id"] == &"chunk_0033"
- and int(rotated_bend_specs[2]["turns"]) == 3
- ),
- "River bend C must retain its authored half-turn at every bend rotation.",
- )
- _check(
- (
- river_cap_northwest != null
- and river_cap_northeast != null
- and river_cap_southwest != null
- and river_cap_southeast != null
- and river_edge_top != null
- and river_edge_variant_top != null
- and river_edge_variant_bottom != null
- and river_edge_vertical_left != null
- and river_edge_vertical_right != null
- and river_stones_top != null
- and river_stones_bottom != null
- and river_stones_left != null
- and river_stones_right != null
- and river_source_right != null
- and river_source_left != null
- and river_outlet_right != null
- and river_outlet_left != null
- and river_bend_a != null
- and river_bend_b != null
- and river_bend_c != null
- and river_bend_d != null
- and beach_east != null
- ),
- (
- "Every authored river-bank, bend, source, outlet, and legacy-cap "
- + "variant must be available."
- ),
- )
- if (
- river_cap_northwest != null
- and river_cap_northeast != null
- and river_cap_southwest != null
- and river_cap_southeast != null
- and river_edge_top != null
- and river_edge_variant_top != null
- and river_edge_variant_bottom != null
- and river_edge_vertical_left != null
- and river_edge_vertical_right != null
- and river_stones_top != null
- and river_stones_bottom != null
- and river_stones_left != null
- and river_stones_right != null
- and river_source_right != null
- and river_source_left != null
- and river_outlet_right != null
- and river_outlet_left != null
- and river_bend_a != null
- and river_bend_b != null
- and river_bend_c != null
- and river_bend_d != null
- and beach_east != null
- ):
- _check(
- generator._edges_are_compatible(
- river_edge_top,
- TerrainChunkTopology.Edge.EAST,
- river_stones_top,
- TerrainChunkTopology.Edge.WEST,
- )
- and generator._edges_are_compatible(
- river_stones_top,
- TerrainChunkTopology.Edge.SOUTH,
- river_stones_bottom,
- TerrainChunkTopology.Edge.NORTH,
- )
- and generator._edges_are_compatible(
- river_stones_left,
- TerrainChunkTopology.Edge.EAST,
- river_stones_right,
- TerrainChunkTopology.Edge.WEST,
- ),
- (
- "The stepping-stone banks must replace one paired straight river "
- + "station in either orientation."
- ),
- )
- _check(
- generator._edges_are_compatible(
- river_source_right,
- TerrainChunkTopology.Edge.SOUTH,
- river_source_left,
- TerrainChunkTopology.Edge.NORTH,
- )
- and generator._edges_are_compatible(
- river_source_right,
- TerrainChunkTopology.Edge.EAST,
- river_edge_top,
- TerrainChunkTopology.Edge.WEST,
- )
- and generator._edges_are_compatible(
- river_source_left,
- TerrainChunkTopology.Edge.EAST,
- river_edge_variant_bottom,
- TerrainChunkTopology.Edge.WEST,
- ),
- "The paired cliff source must open directly into both river banks.",
- )
- _check(
- generator._edges_are_compatible(
- river_bend_a,
- TerrainChunkTopology.Edge.EAST,
- river_bend_b,
- TerrainChunkTopology.Edge.WEST,
- )
- and generator._edges_are_compatible(
- river_bend_a,
- TerrainChunkTopology.Edge.SOUTH,
- river_bend_c,
- TerrainChunkTopology.Edge.NORTH,
- )
- and generator._edges_are_compatible(
- river_bend_b,
- TerrainChunkTopology.Edge.SOUTH,
- river_bend_d,
- TerrainChunkTopology.Edge.NORTH,
- )
- and generator._edges_are_compatible(
- river_bend_c,
- TerrainChunkTopology.Edge.EAST,
- river_bend_d,
- TerrainChunkTopology.Edge.WEST,
- ),
- "The four authored river-bend quadrants must form one continuous turn.",
- )
- _check(
- generator._edges_are_compatible(
- river_edge_top,
- TerrainChunkTopology.Edge.EAST,
- river_bend_a,
- TerrainChunkTopology.Edge.WEST,
- )
- and generator._edges_are_compatible(
- river_edge_variant_bottom,
- TerrainChunkTopology.Edge.EAST,
- river_bend_c,
- TerrainChunkTopology.Edge.WEST,
- )
- and generator._edges_are_compatible(
- river_bend_c,
- TerrainChunkTopology.Edge.SOUTH,
- river_edge_vertical_left,
- TerrainChunkTopology.Edge.NORTH,
- )
- and generator._edges_are_compatible(
- river_bend_d,
- TerrainChunkTopology.Edge.SOUTH,
- river_edge_vertical_right,
- TerrainChunkTopology.Edge.NORTH,
- ),
- "The bend must connect both incoming and outgoing authored river banks.",
- )
- _check(
- generator._edges_are_compatible(
- river_edge_top,
- TerrainChunkTopology.Edge.EAST,
- river_outlet_right,
- TerrainChunkTopology.Edge.WEST,
- )
- and generator._edges_are_compatible(
- river_edge_variant_bottom,
- TerrainChunkTopology.Edge.EAST,
- river_outlet_left,
- TerrainChunkTopology.Edge.WEST,
- )
- and generator._edges_are_compatible(
- river_outlet_right,
- TerrainChunkTopology.Edge.SOUTH,
- river_outlet_left,
- TerrainChunkTopology.Edge.NORTH,
- ),
- "Both river banks must terminate in the paired beach outlet.",
- )
- _check(
- generator._edges_are_compatible(
- beach_east,
- TerrainChunkTopology.Edge.SOUTH,
- river_outlet_right,
- TerrainChunkTopology.Edge.NORTH,
- )
- and generator._edges_are_compatible(
- river_outlet_left,
- TerrainChunkTopology.Edge.SOUTH,
- beach_east,
- TerrainChunkTopology.Edge.NORTH,
- ),
- "The ocean outlet must remain straddled by ordinary beach tiles.",
- )
- _check(
- generator._edges_are_compatible(
- river_cap_northwest,
- TerrainChunkTopology.Edge.EAST,
- river_edge_top,
- TerrainChunkTopology.Edge.WEST,
- )
- and generator._edges_are_compatible(
- river_edge_top,
- TerrainChunkTopology.Edge.EAST,
- river_edge_variant_top,
- TerrainChunkTopology.Edge.WEST,
- )
- and generator._edges_are_compatible(
- river_edge_variant_top,
- TerrainChunkTopology.Edge.EAST,
- river_cap_northeast,
- TerrainChunkTopology.Edge.WEST,
- ),
- "Both river-bank visuals must chain exactly between composite caps.",
- )
- _check(
- generator._edges_are_compatible(
- river_cap_northwest,
- TerrainChunkTopology.Edge.SOUTH,
- river_cap_southwest,
- TerrainChunkTopology.Edge.NORTH,
- )
- and generator._edges_are_compatible(
- river_edge_top,
- TerrainChunkTopology.Edge.SOUTH,
- river_edge_variant_bottom,
- TerrainChunkTopology.Edge.NORTH,
- )
- and generator._edges_are_compatible(
- river_cap_northeast,
- TerrainChunkTopology.Edge.SOUTH,
- river_cap_southeast,
- TerrainChunkTopology.Edge.NORTH,
- ),
- "Opposing river banks and end caps must close the water channel.",
- )
- for stable_id: StringName in [
- &"chunk_0005",
- &"chunk_0006",
- &"chunk_0007",
- &"chunk_0008",
- &"chunk_0014",
- &"chunk_0015",
- &"chunk_9999",
- &"chunk_9998",
- &"chunk_9997",
- &"chunk_9996",
- ]:
- for quarter_turns: int in 4:
- var coast_variant := _find_variant(
- generator,
- stable_id,
- quarter_turns,
- )
- _check(
- coast_variant != null,
- "%s must support rotation %d." % [stable_id, quarter_turns],
- )
- if coast_variant == null:
- continue
- _check(
- not generator._variant_respects_ocean_boundary(
- coast_variant,
- Vector2i(3, 3),
- ),
- "%s must never place its ocean edge inside the map." % stable_id,
- )
- var coast_definition := CATALOG.definition_for_id(stable_id)
- if coast_definition == null:
- continue
- var boundary_coordinate := Vector2i(3, 3)
- var ocean_edges := coast_variant.rotated_edge_mask(
- coast_definition.ocean_facing_edges
- )
- for edge_value: int in TerrainChunkTopology.Edge.values():
- if (ocean_edges & (1 << edge_value)) == 0:
- continue
- match edge_value as TerrainChunkTopology.Edge:
- TerrainChunkTopology.Edge.NORTH:
- boundary_coordinate.y = 0
- TerrainChunkTopology.Edge.EAST:
- boundary_coordinate.x = generator.grid_size.x - 1
- TerrainChunkTopology.Edge.SOUTH:
- boundary_coordinate.y = generator.grid_size.y - 1
- TerrainChunkTopology.Edge.WEST:
- boundary_coordinate.x = 0
- _check(
- generator._variant_respects_ocean_boundary(
- coast_variant,
- boundary_coordinate,
- ),
- "Each coast rotation must fit its matching open map boundary.",
- )
-
-
-func _validate_generation_summary(generator: TerrainChunkGenerator) -> void:
- var summary := generator._build_summary()
- _check(
- int(summary.get("variant_count", 0)) == 174,
- "The current catalog must expose all 174 authored rotations.",
- )
- _check(
- int(summary.get("solver_variant_count", 0))
- == EXPECTED_SOLVER_VARIANT_COUNT,
- "Equivalent visual rotations must share one solver constraint.",
- )
- _check(
- int(summary.get("adjacent_repeat_edges", -1))
- == generator._count_adjacent_repeat_edges(),
- "The summary must report adjacent repeated chunk definitions.",
- )
- _check(
- str(summary.get("layout_fingerprint", ""))
- == generator.placement_fingerprint(),
- "The summary must identify the exact resolved layout.",
- )
- _check(
- generator.placement_fingerprint().length() == 64,
- "The resolved layout fingerprint must be SHA-256.",
- )
- var variant_counts: Dictionary = summary.get("variant_counts", {})
- for stable_id: StringName in EXPECTED_VARIANT_COUNTS:
- _check(
- int(variant_counts.get(stable_id, 0))
- == EXPECTED_VARIANT_COUNTS[stable_id],
- "The summary must report %s rotation candidates." % stable_id,
- )
-
-
-func _find_variant(
- generator: TerrainChunkGenerator,
- stable_id: StringName,
- quarter_turns: int,
-) -> TerrainChunkVariant:
- for variant: TerrainChunkVariant in generator._variants:
- if (
- variant.definition.stable_id == stable_id
- and variant.quarter_turns == quarter_turns
- ):
- return variant
- return null
-
-
-func _make_generator() -> TerrainChunkGenerator:
- var generator := TerrainChunkGenerator.new()
- generator.catalog = CATALOG
- generator.grid_size = Vector2i(9, 9)
- generator.generation_seed = TEST_SEED
- generator.generate_on_ready = false
- generator.build_collision = false
- generator.force_center_chunk_id = &"chunk_spawn"
- generator.required_chunk_ids = PackedStringArray(REQUIRED_IDS)
- generator.grass_sand_smoothing_enabled = true
- generator.maximum_backtracks = 100000
- return generator
-
-
-func _placements_contain(keys: PackedStringArray, stable_id: String) -> bool:
- for key: String in keys:
- if key.begins_with(stable_id + "@"):
- return true
- return false
-
-
-func _count_placements(keys: PackedStringArray, stable_id: String) -> int:
- var count := 0
- for key: String in keys:
- if key.begins_with(stable_id + "@"):
- count += 1
- return count
-
-
-func _all_neighbor_edges_match(generator: TerrainChunkGenerator) -> bool:
- for index: int in generator._placements.size():
- var coordinate := Vector2i(
- index % generator.grid_size.x,
- index / generator.grid_size.x,
- )
- var current: TerrainChunkVariant = generator._placements[index]
- if coordinate.x > 0:
- var west: TerrainChunkVariant = generator._placements[index - 1]
- if not generator._edges_are_compatible(
- current,
- TerrainChunkTopology.Edge.WEST,
- west,
- TerrainChunkTopology.Edge.EAST,
- ):
- return false
- if coordinate.y > 0:
- var north: TerrainChunkVariant = (
- generator._placements[index - generator.grid_size.x]
- )
- if not generator._edges_are_compatible(
- current,
- TerrainChunkTopology.Edge.NORTH,
- north,
- TerrainChunkTopology.Edge.SOUTH,
- ):
- return false
- return true
-
-
-func _validate_layout_rules(generator: TerrainChunkGenerator) -> void:
- _check(
- generator._neighbor_requirement_validation_error(
- generator._placements
- ).is_empty(),
- "Every generated chunk must satisfy its authored neighbor count.",
- )
- _check(
- generator._walkable_connectivity_validation_error(
- generator._placements
- ).is_empty(),
- "All walkable generated terrain must remain connected to spawn.",
- )
- var center := Vector2i(
- generator.grid_size.x / 2,
- generator.grid_size.y / 2,
- )
- var safe_spawn_neighbors := 0
- for edge_value: int in TerrainChunkTopology.Edge.values():
- var neighbor_coordinate := (
- center
- + TerrainChunkTopology.grid_offset(
- edge_value as TerrainChunkTopology.Edge
- )
- )
- var neighbor := generator._placements[
- neighbor_coordinate.y * generator.grid_size.x
- + neighbor_coordinate.x
- ]
- if "spawn_safe" in neighbor.definition.tags:
- safe_spawn_neighbors += 1
- _check(
- safe_spawn_neighbors >= 3,
- "At least three cardinal spawn neighbors must be safe flat grass.",
- )
- for index: int in generator._placements.size():
- var placement := generator._placements[index]
- var coordinate := Vector2i(
- index % generator.grid_size.x,
- index / generator.grid_size.x,
- )
- if "coast" in placement.definition.tags:
- _check(
- generator._variant_respects_ocean_boundary(
- placement,
- coordinate,
- ),
- "Every coastline chunk must direct its ocean edge outside the grid.",
- )
- var ocean_edges := placement.rotated_edge_mask(
- placement.definition.ocean_facing_edges
- )
- for edge_value: int in TerrainChunkTopology.Edge.values():
- var neighbor_coordinate := (
- coordinate
- + TerrainChunkTopology.grid_offset(
- edge_value as TerrainChunkTopology.Edge
- )
- )
- var outside := not generator._coordinate_is_inside_grid(
- neighbor_coordinate
- )
- _check(
- ((ocean_edges & (1 << edge_value)) != 0) == outside,
- "Every map-boundary edge must use authored coastline geometry.",
- )
- if placement.definition.stable_id == &"chunk_0006":
- var touches_beach := false
- var touches_grass_edge := false
- for edge_value: int in TerrainChunkTopology.Edge.values():
- var neighbor_coordinate := (
- coordinate
- + TerrainChunkTopology.grid_offset(
- edge_value as TerrainChunkTopology.Edge
- )
- )
- if not generator._coordinate_is_inside_grid(neighbor_coordinate):
- continue
- var neighbor := generator._placements[
- neighbor_coordinate.y * generator.grid_size.x
- + neighbor_coordinate.x
- ]
- touches_beach = (
- touches_beach
- or neighbor.definition.stable_id == &"chunk_0002"
- )
- touches_grass_edge = (
- touches_grass_edge
- or neighbor.definition.stable_id == &"chunk_0005"
- )
- _check(
- touches_beach and touches_grass_edge,
- "Every grass/beach transition must join both authored edge types.",
- )
- if placement.definition.stable_id == &"chunk_0007":
- var straight_edge_neighbors := 0
- for edge_value: int in TerrainChunkTopology.Edge.values():
- var neighbor_coordinate := (
- coordinate
- + TerrainChunkTopology.grid_offset(
- edge_value as TerrainChunkTopology.Edge
- )
- )
- if not generator._coordinate_is_inside_grid(neighbor_coordinate):
- continue
- var neighbor := generator._placements[
- neighbor_coordinate.y * generator.grid_size.x
- + neighbor_coordinate.x
- ]
- if neighbor.definition.stable_id == &"chunk_0005":
- straight_edge_neighbors += 1
- _check(
- straight_edge_neighbors == 2,
- "Every grass corner must join two straight grass ocean edges.",
- )
- if placement.definition.stable_id == &"chunk_0008":
- var straight_beach_neighbors := 0
- for edge_value: int in TerrainChunkTopology.Edge.values():
- var neighbor_coordinate := (
- coordinate
- + TerrainChunkTopology.grid_offset(
- edge_value as TerrainChunkTopology.Edge
- )
- )
- if not generator._coordinate_is_inside_grid(neighbor_coordinate):
- continue
- var neighbor := generator._placements[
- neighbor_coordinate.y * generator.grid_size.x
- + neighbor_coordinate.x
- ]
- if neighbor.definition.stable_id == &"chunk_0002":
- straight_beach_neighbors += 1
- _check(
- straight_beach_neighbors == 2,
- "Every beach corner must join two straight beach ocean edges.",
- )
- if placement.definition.stable_id != &"chunk_0001":
- continue
- var touches_sand_or_coast := false
- for edge_value: int in TerrainChunkTopology.Edge.values():
- var neighbor_coordinate := (
- coordinate
- + TerrainChunkTopology.grid_offset(
- edge_value as TerrainChunkTopology.Edge
- )
- )
- if not generator._coordinate_is_inside_grid(neighbor_coordinate):
- continue
- var neighbor := generator._placements[
- neighbor_coordinate.y * generator.grid_size.x
- + neighbor_coordinate.x
- ]
- if (
- "sand" in neighbor.definition.tags
- or "coast" in neighbor.definition.tags
- ):
- touches_sand_or_coast = true
- _check(
- touches_sand_or_coast,
- "Every flat-sand chunk must remain attached to sand or coast.",
- )
-
-
-func _check(condition: bool, message: String) -> void:
- if not condition:
- _failures.append(message)
-
-
-func _finish() -> void:
- if _failures.is_empty():
- print("Terrain chunk generator validation: PASS")
- quit(0)
- return
- for failure: String in _failures:
- printerr("Terrain chunk generator validation: ", failure)
- quit(1)
diff --git a/tests/terrain_chunk_generator_validation.gd.uid b/tests/terrain_chunk_generator_validation.gd.uid
deleted file mode 100644
index d13a22b..0000000
--- a/tests/terrain_chunk_generator_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bt6pjgkomx76s
diff --git a/tests/texture_sampling_validation.gd b/tests/texture_sampling_validation.gd
deleted file mode 100644
index dcccc38..0000000
--- a/tests/texture_sampling_validation.gd
+++ /dev/null
@@ -1,158 +0,0 @@
-extends SceneTree
-
-const POLICY := preload("res://main/texture_sampling_policy.gd")
-const PROJECTED_TERRAIN_TEXTURE_IMPORTS: Array[String] = [
- "res://world/generation/chunks/assets/chunk_0000_grass_lite.png.import",
- "res://world/generation/chunks/assets/chunk_0001_sand.png.import",
- "res://world/generation/chunks/assets/chunk_0031_dirt.png.import",
-]
-
-var failures: Array[String] = []
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- _validate_runtime_policy()
- _validate_repository_files("res://")
- if failures.is_empty():
- print("Texture sampling validation passed.")
- quit(0)
- return
- for failure: String in failures:
- printerr("Texture sampling validation: ", failure)
- quit(1)
-
-
-func _validate_runtime_policy() -> void:
- var texture_rect := TextureRect.new()
- texture_rect.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
- POLICY.enforce_node(texture_rect)
- _check(
- texture_rect.texture_filter == CanvasItem.TEXTURE_FILTER_NEAREST,
- "CanvasItem policy must replace linear sampling with nearest sampling.",
- )
- texture_rect.free()
-
- var sprite := Sprite3D.new()
- sprite.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR
- POLICY.enforce_node(sprite)
- _check(
- sprite.texture_filter == BaseMaterial3D.TEXTURE_FILTER_NEAREST,
- "Sprite3D policy must replace linear sampling with nearest sampling.",
- )
- sprite.free()
-
- var material := StandardMaterial3D.new()
- material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR
- var quad := QuadMesh.new()
- quad.material = material
- var mesh_instance := MeshInstance3D.new()
- mesh_instance.mesh = quad
- POLICY.enforce_node(mesh_instance)
- _check(
- material.texture_filter == BaseMaterial3D.TEXTURE_FILTER_NEAREST,
- "3D material policy must replace linear sampling with nearest sampling.",
- )
- mesh_instance.free()
-
-
-func _validate_repository_files(path: String) -> void:
- var directory := DirAccess.open(path)
- if directory == null:
- failures.append("Could not scan %s." % path)
- return
- directory.list_dir_begin()
- while true:
- var name := directory.get_next()
- if name.is_empty():
- break
- if name == ".godot" or name == ".git":
- continue
- var child_path := path.path_join(name)
- if directory.current_is_dir():
- _validate_repository_files(child_path)
- continue
- if name.ends_with(".import"):
- _validate_import_file(child_path)
- elif name.ends_with(".gdshader"):
- _validate_shader_file(child_path)
- elif name.ends_with(".tscn") or name.ends_with(".tres"):
- _validate_serialized_resource(child_path)
- elif name.ends_with(".gd") and child_path != get_script().resource_path:
- _validate_script_file(child_path)
- directory.list_dir_end()
-
-
-func _validate_import_file(path: String) -> void:
- var source := FileAccess.get_file_as_string(path)
- var expects_mipmaps := path in PROJECTED_TERRAIN_TEXTURE_IMPORTS
- _check(
- ("mipmaps/generate=true" in source) == expects_mipmaps,
- (
- "Projected terrain mipmap policy mismatch: %s"
- if expects_mipmaps
- else "Texture import generates unapproved mipmaps: %s"
- ) % path,
- )
-
-
-func _validate_shader_file(path: String) -> void:
- var source := FileAccess.get_file_as_string(path)
- var declaration_start := source.find("uniform sampler")
- while declaration_start >= 0:
- var declaration_end := source.find(";", declaration_start)
- if declaration_end < 0:
- _check(false, "Incomplete sampler declaration in %s." % path)
- return
- var declaration := source.substr(
- declaration_start,
- declaration_end - declaration_start,
- )
- _check(
- "filter_nearest" in declaration,
- "Shader sampler must declare filter_nearest: %s" % path,
- )
- declaration_start = source.find("uniform sampler", declaration_end + 1)
-
-
-func _validate_serialized_resource(path: String) -> void:
- var source := FileAccess.get_file_as_string(path)
- var uses_3d_filter_enum := false
- for line: String in source.split("\n"):
- if line.begins_with("["):
- uses_3d_filter_enum = (
- ("type=\"Sprite3D\"" in line)
- or ("type=\"AnimatedSprite3D\"" in line)
- or ("type=\"StandardMaterial3D\"" in line)
- or ("type=\"ORMMaterial3D\"" in line)
- )
- if not line.begins_with("texture_filter = "):
- continue
- var value := int(line.trim_prefix("texture_filter = "))
- if uses_3d_filter_enum:
- _check(
- value == BaseMaterial3D.TEXTURE_FILTER_NEAREST,
- "3D resource declares non-nearest filtering: %s" % path,
- )
- else:
- _check(
- value == CanvasItem.TEXTURE_FILTER_PARENT_NODE
- or value == CanvasItem.TEXTURE_FILTER_NEAREST,
- "Canvas resource declares non-nearest filtering: %s" % path,
- )
-
-
-func _validate_script_file(path: String) -> void:
- var source := FileAccess.get_file_as_string(path)
- _check(
- "TEXTURE_FILTER_LINEAR" not in source,
- "Script explicitly requests linear texture filtering: %s" % path,
- )
-
-
-func _check(condition: bool, message: String) -> void:
- if not condition:
- failures.append(message)
diff --git a/tests/texture_sampling_validation.gd.uid b/tests/texture_sampling_validation.gd.uid
deleted file mode 100644
index 9a2f48b..0000000
--- a/tests/texture_sampling_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dqm0j1xnkeox1
diff --git a/tests/title_credits_validation.gd b/tests/title_credits_validation.gd
deleted file mode 100644
index e5ac4cc..0000000
--- a/tests/title_credits_validation.gd
+++ /dev/null
@@ -1,267 +0,0 @@
-extends SceneTree
-
-const TitleScreenScene := preload("res://ui/title_screen.tscn")
-const TitleCreditsPageType = preload("res://ui/title_credits_page.gd")
-
-var _failures: Array[String] = []
-
-
-func _initialize() -> void:
- _run.call_deferred()
-
-
-func _run() -> void:
- var original_size: Vector2i = root.size
- root.size = Vector2i(1280, 720)
- var title_screen := TitleScreenScene.instantiate() as TitleScreen
- root.add_child(title_screen)
- await process_frame
- await process_frame
-
- var credits_button := title_screen.get_node(
- "%CreditsButton"
- ) as BubbleButton
- var play_button := title_screen.get_node("%PlayButton") as BubbleButton
- var continue_stats_drawer := title_screen.get_node(
- "%ContinueStatsDrawer"
- ) as RichTextLabel
- var credits_page := title_screen.get_node(
- "%CreditsPage"
- ) as TitleCreditsPageType
- var back_button: Button = credits_page.get_back_button()
- var presentation := title_screen.get_node("%Center") as Control
- var bubble_field := title_screen.get_node("%BubbleField") as Control
- title_screen.call("_update_title_layout")
- (title_screen.get_node("%ButtonCenter") as Control).show()
- bubble_field.show()
- title_screen.call("_set_title_bubbles_interactive", true)
- await process_frame
-
- _expect(credits_button != null, "title menu exposes a credits button")
- _expect(play_button != null, "title menu exposes the play button")
- _expect(
- continue_stats_drawer != null,
- "play button owns a dedicated save-preview drawer",
- )
- if play_button != null and continue_stats_drawer != null:
- _expect(
- continue_stats_drawer.get_parent() == play_button,
- "save preview extends directly from the play button",
- )
- _expect(
- continue_stats_drawer.position.y < play_button.size.y
- and (
- continue_stats_drawer.position.y
- + continue_stats_drawer.size.y
- ) > play_button.size.y,
- "save preview overlaps the play edge and extends below it",
- )
- _expect(
- continue_stats_drawer.mouse_filter
- == Control.MOUSE_FILTER_IGNORE,
- "save preview drawer cannot intercept menu input",
- )
- _expect(
- credits_button.icon != null and credits_button.text.is_empty(),
- "credits action uses its icon in the square-button presentation",
- )
- _expect(credits_page != null, "title screen contains the credits page")
- _expect(not credits_page.visible, "credits page starts closed")
- _expect(
- Rect2(Vector2.ZERO, bubble_field.size).encloses(
- Rect2(credits_button.position, credits_button.size)
- ),
- "credits button stays inside the authored title field",
- )
- _expect(
- credits_button.mouse_filter == Control.MOUSE_FILTER_STOP,
- "credits button accepts pointer input",
- )
- if OS.has_environment("straywild_TITLE_MENU_CAPTURE"):
- (title_screen.get_node("%StartPromptCenter") as Control).hide()
- title_screen.set_process(true)
- await create_timer(0.5).timeout
- _expect(
- _save_capture(
- OS.get_environment("straywild_TITLE_MENU_CAPTURE")
- ) == OK,
- "title menu validation capture saves successfully",
- )
- title_screen.set_process(false)
-
- title_screen.set("_navigation_focus_active", false)
- credits_button.call("_set_hovered", true)
- credits_button.advance_emphasis(1.0)
- _expect(
- (credits_button.get_node("HoverDrawer") as Control).visible,
- "pointer hover reveals the credits drawer before activation",
- )
- credits_button.pressed.emit()
- await process_frame
- _expect(credits_page.visible, "pointer activation opens credits")
- _expect(not presentation.visible, "credits replace the primary title menu")
- _expect(
- not (credits_button.get_node("HoverDrawer") as Control).visible,
- "opening a title page collapses its hover drawer",
- )
- _expect(
- root.gui_get_focus_owner() != back_button,
- "pointer activation does not pseudo-select back",
- )
- await create_timer(0.25).timeout
- _expect(
- credits_page.mouse_filter == Control.MOUSE_FILTER_PASS,
- "credits page becomes pointer-interactive after entry motion",
- )
- _expect(
- back_button.mouse_filter == Control.MOUSE_FILTER_STOP,
- "credits back button accepts pointer clicks",
- )
- if OS.has_environment("straywild_CREDITS_CAPTURE"):
- await process_frame
- _expect(
- _save_capture(
- OS.get_environment("straywild_CREDITS_CAPTURE")
- ) == OK,
- "credits validation capture saves successfully",
- )
- back_button.pressed.emit()
- await process_frame
- _expect(not credits_page.visible, "pointer back closes credits")
- _expect(presentation.visible, "closing credits restores the title menu")
- _expect(
- root.gui_get_focus_owner() == null,
- "pointer return leaves the title menu unfocused",
- )
-
- var online_button := title_screen.get_node(
- "%JoinGameButton"
- ) as BubbleButton
- var join_game_page := title_screen.get_node("%JoinGamePage") as Control
- online_button.call("_set_hovered", true)
- online_button.advance_emphasis(1.0)
- _expect(
- (online_button.get_node("HoverDrawer") as Control).visible,
- "pointer hover reveals the online drawer before activation",
- )
- online_button.pressed.emit()
- await process_frame
- _expect(join_game_page.visible, "online activation opens the join page")
- _expect(
- not (online_button.get_node("HoverDrawer") as Control).visible,
- "opening the join page collapses the online drawer",
- )
- title_screen.call("_close_join_game")
- await process_frame
- _expect(
- not (online_button.get_node("HoverDrawer") as Control).visible,
- "returning from the join page does not restore stale pointer hover",
- )
-
- title_screen.set("_navigation_focus_active", true)
- credits_button.grab_focus()
- credits_button.pressed.emit()
- await process_frame
- await process_frame
- _expect(
- root.gui_get_focus_owner() == back_button,
- "keyboard or controller activation focuses the credits back button",
- )
-
- var pointer_motion := InputEventMouseMotion.new()
- pointer_motion.position = Vector2(640.0, 360.0)
- title_screen._input(pointer_motion)
- await process_frame
- _expect(
- root.gui_get_focus_owner() == null,
- "pointer motion clears credits navigation focus",
- )
-
- var navigate := InputEventAction.new()
- navigate.action = &"ui_down"
- navigate.pressed = true
- title_screen._input(navigate)
- await process_frame
- _expect(
- root.gui_get_focus_owner() == back_button,
- "navigation input restores credits focus",
- )
-
- var cancel := InputEventAction.new()
- cancel.action = &"ui_cancel"
- cancel.pressed = true
- title_screen._input(cancel)
- await process_frame
- _expect(not credits_page.visible, "ui cancel closes credits")
- _expect(
- root.gui_get_focus_owner() == credits_button,
- "navigation return restores focus to the credits button",
- )
-
- var chillnfill_name := credits_page.get_node(
- "Paper/Margin/Layout/Columns/CreativeCredits/ChillnfillName"
- ) as Label
- var voyager_name := credits_page.get_node(
- "Paper/Margin/Layout/Columns/CreativeCredits/VoyagerName"
- ) as Label
- var endeavour_name := credits_page.get_node(
- "Paper/Margin/Layout/Columns/CreativeCredits/EndeavourName"
- ) as Label
- var chillnfill_credit := credits_page.get_node(
- "Paper/Margin/Layout/Columns/CreativeCredits/ChillnfillCredit"
- ) as Label
- var credits_paper := credits_page.get_node("Paper") as PanelContainer
- var audio_credits := credits_page.get_node(
- "Paper/Margin/Layout/Columns/AdditionalCredits/AudioCredits"
- ) as Label
- _expect(voyager_name.text == "Voyager", "in-game credits use Voyager")
- _expect(
- endeavour_name.text == "Endeavour",
- "in-game credits use Endeavour",
- )
- _expect(
- chillnfill_name.text == "chillnfill",
- "in-game credits name chillnfill",
- )
- _expect(
- "trees and bridges" in chillnfill_credit.text.to_lower(),
- "in-game credits describe the supplied model families",
- )
- _expect(
- "kat • animalese source recording" in audio_credits.text,
- "in-game credits name kat's animalese source contribution",
- )
- _expect(
- "kim • animalese source recording" in audio_credits.text,
- "in-game credits name kim's animalese source contribution",
- )
- _expect(
- credits_paper.get_global_rect().encloses(
- audio_credits.get_global_rect()
- ),
- "audio credits stay inside the authored credits panel",
- )
-
- title_screen.queue_free()
- root.size = original_size
- await process_frame
- await process_frame
- if _failures.is_empty():
- print("Title credits validation: PASS")
- quit(0)
- return
- for failure: String in _failures:
- push_error(failure)
- quit(1)
-
-
-func _expect(condition: bool, message: String) -> void:
- if not condition:
- _failures.append(message)
-
-
-func _save_capture(path: String) -> Error:
- var viewport_texture: Texture2D = root.get_texture()
- if viewport_texture == null:
- return ERR_UNAVAILABLE
- return viewport_texture.get_image().save_png(path)
diff --git a/tests/title_credits_validation.gd.uid b/tests/title_credits_validation.gd.uid
deleted file mode 100644
index 747e70c..0000000
--- a/tests/title_credits_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dimq4ehtlw64n
diff --git a/tests/tree_gathering_prototype_validation.gd b/tests/tree_gathering_prototype_validation.gd
deleted file mode 100644
index 2814e19..0000000
--- a/tests/tree_gathering_prototype_validation.gd
+++ /dev/null
@@ -1,166 +0,0 @@
-extends SceneTree
-
-const StarterIslandScene = preload(
- "res://world/regions/starter_island_region.tscn"
-)
-const Gatherables: GatherableCatalog = preload(
- "res://gathering/catalog/gatherable_catalog.tres"
-)
-const FishCatalog: FishPool = preload("res://fish/pools/fish_catalog.tres")
-const MeshSurfaceAnchorSamplerType = preload(
- "res://world/generation/mesh_surface_anchor_sampler.gd"
-)
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- _validate_beetle_data()
- await _validate_mesh_surface_sampler()
- await _validate_tree_anchors()
- _validate_anchored_presentation()
- _validate_three_dimensional_targeting()
- print("Tree gathering prototype validation: PASS")
- quit()
-
-
-func _validate_beetle_data() -> void:
- var beetle: GatherableData = Gatherables.get_entry(&"beetle_stag_common")
- assert(beetle != null and beetle.is_available())
- assert(beetle.catch_data == FishCatalog.get_fish_by_id(&"beetle_stag_common"))
- assert(beetle.catch_data.display_texture != null)
- assert(beetle.catch_data.collection_method == FishData.CollectionMethod.NET)
- assert(beetle.catch_data.collection_group == &"Beetles")
- assert(beetle.required_tool_id == &"crab_net")
- assert(beetle.spawn_anchor_set_id == &"starter_reachable_tree_trunks")
- assert(beetle.population == 3)
- assert(is_equal_approx(beetle.spawn_anchor_occupancy_ratio, 0.35))
- assert(beetle.maximum_anchor_population == 64)
- assert(beetle.target_population_for_anchor_count(8) == 3)
- assert(beetle.target_population_for_anchor_count(40) == 14)
- assert(beetle.target_population_for_anchor_count(400) == 64)
- assert(is_equal_approx(beetle.sprite_pixel_size, 0.0075))
- assert(beetle.is_stationary_spawn())
- assert(not beetle.can_be_scared())
-
-
-func _validate_mesh_surface_sampler() -> void:
- var prop := Node3D.new()
- # The accessibility band is local to the planted prop, so the same tree on
- # a raised cliff remains reachable from that cliff's walkable surface.
- prop.position = Vector3(3.0, 12.0, -4.0)
- root.add_child(prop)
- var visual := MeshInstance3D.new()
- var box := BoxMesh.new()
- box.size = Vector3(2.0, 4.0, 2.0)
- var wood := StandardMaterial3D.new()
- wood.resource_name = "wood"
- box.material = wood
- visual.mesh = box
- visual.position.y = 2.0
- prop.add_child(visual)
- await process_frame
- var random := RandomNumberGenerator.new()
- random.seed = 115
- var sample: Dictionary = MeshSurfaceAnchorSamplerType.sample_vertical_surface(
- prop,
- prop,
- PackedStringArray(["wood"]),
- 0.7,
- 1.5,
- 0.35,
- 0.025,
- random,
- )
- assert(not sample.is_empty())
- var surface_position: Vector3 = sample["surface_position"]
- var anchor_position: Vector3 = sample["position"]
- assert(surface_position.y >= 0.7 and surface_position.y <= 1.5)
- var world_anchor_position := prop.to_global(anchor_position)
- assert(
- world_anchor_position.y >= 12.7
- and world_anchor_position.y <= 13.5
- )
- assert(is_equal_approx(
- maxf(absf(surface_position.x), absf(surface_position.z)),
- 1.0,
- ))
- assert(is_equal_approx(
- anchor_position.distance_to(surface_position),
- 0.025,
- ))
- assert(MeshSurfaceAnchorSamplerType.sample_vertical_surface(
- prop,
- prop,
- PackedStringArray(["leaf"]),
- 0.7,
- 1.5,
- 0.35,
- 0.025,
- random,
- ).is_empty())
- prop.queue_free()
-
-
-func _validate_tree_anchors() -> void:
- var region := StarterIslandScene.instantiate() as WorldRegion
- root.add_child(region)
- await process_frame
- var anchor_set: GatherableAnchorSet3D = region.get_gatherable_anchor_set(
- &"starter_reachable_tree_trunks"
- )
- assert(anchor_set != null)
- var positions: PackedVector3Array = anchor_set.get_spawn_positions()
- assert(positions.size() == 8)
- for position: Vector3 in positions:
- assert(position.is_finite())
- assert(position.y >= 4.3 and position.y <= 4.5)
- region.queue_free()
-
-
-func _validate_anchored_presentation() -> void:
- var beetle: GatherableData = Gatherables.get_entry(&"beetle_stag_common")
- var presentation := WorldGatherable.new()
- root.add_child(presentation)
- presentation.configure("beetle-visual", beetle, Vector3.ZERO, 0.0)
- var sprite := presentation.get_node("GatherableSprite") as Sprite3D
- assert(sprite != null)
- assert(is_zero_approx(sprite.position.y))
- assert(is_equal_approx(sprite.pixel_size, 0.0075))
- assert(not sprite.shaded)
- assert(sprite.billboard == BaseMaterial3D.BILLBOARD_ENABLED)
- assert(sprite.texture_filter == BaseMaterial3D.TEXTURE_FILTER_NEAREST)
- presentation.queue_free()
-
-
-func _validate_three_dimensional_targeting() -> void:
- var service := NetworkWorldSpawnService.new()
- var beetle: GatherableData = Gatherables.get_entry(&"beetle_stag_common")
- service.set(
- "_entities",
- {
- "beetle-test": {
- "entity_id": "beetle-test",
- "type_id": &"beetle_stag_common",
- "data": beetle,
- "position": Vector3(2.0, 4.4, 3.0),
- "locked": false,
- },
- },
- )
- assert(
- service.find_capture_target(
- Vector3(2.0, 4.4, 3.0),
- &"crab_net",
- )
- == "beetle-test"
- )
- assert(
- service.find_capture_target(
- Vector3(2.0, 3.0, 3.0),
- &"crab_net",
- ).is_empty()
- )
- service.free()
diff --git a/tests/tree_gathering_prototype_validation.gd.uid b/tests/tree_gathering_prototype_validation.gd.uid
deleted file mode 100644
index defb119..0000000
--- a/tests/tree_gathering_prototype_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://d0g1xubbi48n
diff --git a/tests/ui_scaling_runtime_validation.gd b/tests/ui_scaling_runtime_validation.gd
deleted file mode 100644
index 6e6182a..0000000
--- a/tests/ui_scaling_runtime_validation.gd
+++ /dev/null
@@ -1,307 +0,0 @@
-extends SceneTree
-
-const UIPixelationPresenterType = preload(
- "res://ui/ui_pixelation_presenter.gd"
-)
-const UIReferencePresentationType = preload(
- "res://ui/ui_reference_presentation.gd"
-)
-const FullscreenMenuPresentationType = preload(
- "res://ui/fullscreen_menu_presentation.gd"
-)
-const GameUIScene := preload("res://ui/game_ui.tscn")
-const TARGET_SIZES: Array[Vector2i] = [
- Vector2i(1280, 720),
- Vector2i(1920, 1080),
- Vector2i(2560, 1440),
- Vector2i(3840, 2160),
- Vector2i(640, 480),
- Vector2i(1024, 768),
- Vector2i(1729, 973),
- Vector2i(2560, 1080),
- Vector2i(3440, 1440),
-]
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var original_size: Vector2i = root.size
- var presenter := UIPixelationPresenterType.new()
- presenter.name = "UIPresentation"
- presenter.stretch = true
- var ui_viewport := SubViewport.new()
- ui_viewport.name = "UIViewport"
- var game_ui := GameUIScene.instantiate() as GameUI
- var ui_root := game_ui.get_node("UIRoot") as Control
- var canonical_stage := game_ui.get_node(
- "UIRoot/CanonicalStage"
- ) as Control
- var chat_ui := game_ui.get_node("UIRoot/ChatUI") as ChatUI
- var player_menu := game_ui.get_node(
- "UIRoot/CanonicalStage/PlayerMenu"
- ) as PlayerMenu
- var player_menu_profile_stage := player_menu.get_node(
- "ResponsivePlayerMenuStage"
- ) as Control
- var hotbar := game_ui.get_node(
- "UIRoot/CanonicalStage/Hotbar"
- ) as HotbarUI
- assert(game_ui.get_node_or_null(
- "UIRoot/CanonicalStage/GameplayTransientHUD/EffectStatus"
- ) == null)
- var screen_fade := game_ui.get_node("UIRoot/ScreenFade") as Control
- var title_screen := game_ui.get_node("UIRoot/TitleScreen") as TitleScreen
- var title_content_stage := title_screen.get_node(
- "ResponsiveTitleStage"
- ) as Control
- var title_profile_root := title_screen.get_node(
- "ResponsiveTitleStage/TitlePresentationScaleRoot"
- ) as Control
- var pause_menu := game_ui.get_node(
- "UIRoot/CanonicalStage/PauseMenu"
- ) as PauseMenu
- var pause_profile_stage := pause_menu.get_node(
- "ResponsivePauseStage"
- ) as Control
- ui_viewport.add_child(game_ui)
- presenter.add_child(ui_viewport)
- root.add_child(presenter)
- await process_frame
- chat_ui.set_available(true)
- var speech_layer := chat_ui.get("_speech_layer") as Control
- assert(speech_layer != null and speech_layer.visible)
- game_ui.call("_on_player_menu_visibility_changed", true)
- assert(not chat_ui.is_world_speech_visible())
- assert(not speech_layer.visible)
- game_ui.call("_on_player_menu_visibility_changed", false)
- assert(chat_ui.is_world_speech_visible())
- assert(speech_layer.visible)
- chat_ui.set_available(true)
- chat_ui.call(
- "_set_presentation_state",
- ChatUI.PresentationState.EXPANDED,
- false,
- false,
- )
- print(
- "UI viewport embedding: root=%s subviewport=%s"
- % [root.gui_embed_subwindows, ui_viewport.gui_embed_subwindows]
- )
-
- for target_size: Vector2i in TARGET_SIZES:
- root.size = target_size
- await process_frame
- await process_frame
- var actual_size: Vector2i = root.size
- var display_size := Vector2(actual_size)
- var expected_scale: float = UIReferencePresentationType.get_scale(
- display_size
- )
- var expected_stage_offset: Vector2 = UIReferencePresentationType.get_offset(
- display_size
- )
- var expected_profile_scale: float = (
- FullscreenMenuPresentationType.get_profile_scale(display_size)
- )
- var expected_profile_position: Vector2 = (
- FullscreenMenuPresentationType.get_profile_position(display_size)
- )
- assert(presenter.position.is_equal_approx(Vector2.ZERO))
- assert(ui_root.position.is_equal_approx(Vector2.ZERO))
- var expected_visible_size: Vector2 = (
- UIReferencePresentationType.get_visible_reference_size(display_size)
- )
- assert(ui_root.size.is_equal_approx(expected_visible_size))
- assert(canonical_stage.size.is_equal_approx(
- UIReferencePresentationType.REFERENCE_SIZE
- ))
- assert(canonical_stage.position.is_equal_approx(
- UIReferencePresentationType.get_stage_position(display_size)
- ))
- assert(chat_ui.size.is_equal_approx(expected_visible_size))
- assert(screen_fade.size.is_equal_approx(expected_visible_size))
- assert(player_menu.position.is_equal_approx(Vector2.ZERO))
- assert(player_menu.size.is_equal_approx(
- UIReferencePresentationType.REFERENCE_SIZE
- ))
- assert(player_menu_profile_stage.scale.is_equal_approx(
- Vector2.ONE * expected_profile_scale
- ))
- assert(player_menu_profile_stage.position.is_equal_approx(
- expected_profile_position
- ))
- assert(hotbar.position.is_equal_approx(Vector2(
- 0.0,
- canonical_stage.position.y,
- )))
- assert(hotbar.size.is_equal_approx(
- UIReferencePresentationType.REFERENCE_SIZE
- ))
- assert(title_screen.size.is_equal_approx(expected_visible_size))
- assert(title_content_stage.size.is_equal_approx(
- UIReferencePresentationType.REFERENCE_SIZE
- ))
- assert(title_content_stage.position.is_equal_approx(
- canonical_stage.position
- ))
- assert(title_profile_root.scale.is_equal_approx(
- Vector2.ONE * expected_profile_scale
- ))
- assert(title_profile_root.position.is_equal_approx(
- expected_profile_position
- ))
- assert(pause_profile_stage.scale.is_equal_approx(
- Vector2.ONE * expected_profile_scale
- ))
- assert(pause_profile_stage.position.is_equal_approx(
- expected_profile_position
- ))
- assert(is_equal_approx(presenter.scale.x, presenter.scale.y))
- assert(is_equal_approx(ui_root.scale.x, ui_root.scale.y))
- var effective_scale: float = presenter.scale.x * ui_root.scale.x
- assert(is_equal_approx(effective_scale, expected_scale))
- assert((
- canonical_stage.position * effective_scale
- ).distance_to(expected_stage_offset) <= 0.01)
- var profile_source_rect: Rect2 = (
- FullscreenMenuPresentationType.get_source_rect(display_size)
- )
- var profile_screen_start: Vector2 = expected_stage_offset + (
- expected_profile_position
- + profile_source_rect.position * expected_profile_scale
- ) * expected_scale
- var profile_screen_size: Vector2 = (
- profile_source_rect.size
- * expected_profile_scale
- * expected_scale
- )
- var compact_target_scale: float = minf(
- display_size.x / profile_source_rect.size.x,
- display_size.y / profile_source_rect.size.y,
- )
- var expected_profile_screen_size: Vector2 = (
- profile_source_rect.size * compact_target_scale
- )
- var expected_profile_screen_start: Vector2 = (
- display_size - expected_profile_screen_size
- ) * 0.5
- assert(profile_screen_start.distance_to(
- expected_profile_screen_start
- ) <= 0.01)
- assert(profile_screen_size.distance_to(
- expected_profile_screen_size
- ) <= 0.01)
- var expected_render_height: int = mini(
- roundi(UIReferencePresentationType.REFERENCE_SIZE.y * expected_scale),
- PlayerSettings.get_ui_render_height(
- PlayerSettings.DEFAULT_UI_PIXEL_SIZE,
- roundi(UIReferencePresentationType.REFERENCE_SIZE.y),
- ),
- )
- var expected_render_scale: float = (
- float(expected_render_height)
- / UIReferencePresentationType.REFERENCE_SIZE.y
- )
- var expected_viewport_size := Vector2i(
- roundi(expected_visible_size.x * expected_render_scale),
- roundi(expected_visible_size.y * expected_render_scale),
- )
- assert(presenter.get_ui_viewport_size() == expected_viewport_size)
- assert(presenter.size.is_equal_approx(Vector2(expected_viewport_size)))
- var chat_panel := chat_ui.get_node("ChatPanel") as Control
- var clock_panel := chat_ui.get_node("WorldClockPanel") as Control
- var calendar_panel := chat_ui.get_node(
- "WorldCalendarPanel"
- ) as Control
- var status_effect_column := chat_ui.get_node(
- "StatusEffectColumn"
- ) as Control
- var weather_icon := chat_ui.get_node("WorldWeatherIcon") as Control
- assert(chat_panel != null)
- assert(clock_panel != null)
- assert(calendar_panel != null)
- assert(status_effect_column != null)
- assert(weather_icon != null)
- assert(chat_panel.visible)
- assert(is_equal_approx(chat_panel.position.x, 0.0))
- assert(is_equal_approx(
- (chat_panel.position.y + chat_panel.size.y) * effective_scale,
- display_size.y - ChatUI.BOTTOM_MARGIN * effective_scale,
- ))
- assert(clock_panel.position.is_equal_approx(Vector2(
- ChatUI.CLOCK_EDGE_MARGIN,
- ChatUI.CLOCK_EDGE_MARGIN,
- )))
- assert(calendar_panel.position.is_equal_approx(Vector2(
- ChatUI.CLOCK_EDGE_MARGIN,
- ChatUI.CLOCK_EDGE_MARGIN
- + ChatUI.CLOCK_SIZE.y
- + ChatUI.CALENDAR_TOP_GAP,
- )))
- assert(calendar_panel.size.is_equal_approx(ChatUI.CALENDAR_SIZE))
- assert(
- chat_panel.position.y
- >= calendar_panel.position.y
- + calendar_panel.size.y
- + ChatUI.EXPANDED_CHAT_TOP_GAP
- - 0.01
- )
- assert(is_equal_approx(
- status_effect_column.position.x,
- calendar_panel.position.x
- + (
- ChatUI.CALENDAR_SIZE.x
- - ChatUI.STATUS_EFFECT_ICON_SIZE.x
- ) * 0.5,
- ))
- assert(is_equal_approx(
- status_effect_column.position.y,
- calendar_panel.position.y
- + ChatUI.CALENDAR_SIZE.y
- + ChatUI.STATUS_EFFECT_TOP_GAP,
- ))
- assert(is_equal_approx(
- weather_icon.position.y, ChatUI.CLOCK_EDGE_MARGIN
- ))
- assert(weather_icon.position.x > clock_panel.position.x)
- print(
- (
- "UI resize: requested=%s actual=%s viewport=%s container_scale=%.6f "
- + "root_scale=%.6f effective_scale=%.6f stage_offset=%s "
- + "chat_x=%.3f"
- )
- % [
- target_size,
- actual_size,
- presenter.get_ui_viewport_size(),
- presenter.scale.x,
- ui_root.scale.x,
- effective_scale,
- expected_stage_offset,
- chat_panel.position.x * effective_scale,
- ]
- )
- if OS.has_environment("straywild_UI_SCALE_CAPTURE"):
- await process_frame
- var capture: Image = root.get_texture().get_image()
- var capture_path: String = (
- "%s-requested-%dx%d-actual-%dx%d.png"
- ) % [
- OS.get_environment("straywild_UI_SCALE_CAPTURE"),
- target_size.x,
- target_size.y,
- actual_size.x,
- actual_size.y,
- ]
- assert(capture.save_png(capture_path) == OK)
-
- presenter.queue_free()
- root.size = original_size
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- print("UI scaling runtime validation: PASS")
- quit()
diff --git a/tests/ui_scaling_runtime_validation.gd.uid b/tests/ui_scaling_runtime_validation.gd.uid
deleted file mode 100644
index 0b6079a..0000000
--- a/tests/ui_scaling_runtime_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://d1x8t4mf2kqv6
diff --git a/tests/unified_inventory_validation.gd b/tests/unified_inventory_validation.gd
deleted file mode 100644
index 6928969..0000000
--- a/tests/unified_inventory_validation.gd
+++ /dev/null
@@ -1,288 +0,0 @@
-extends SceneTree
-
-const ItemCatalogResource: ItemCatalog = preload(
- "res://items/catalog/item_catalog.tres"
-)
-const Bluegill: FishData = preload(
- "res://fish/species/bluegill/bluegill.tres"
-)
-const MainShopBuyer: FishBuyerProfile = preload(
- "res://economy/buyers/main_fishing_shop.tres"
-)
-const FishCatalogResource: FishPool = preload(
- "res://fish/pools/fish_catalog.tres"
-)
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var root := Node.new()
- get_root().add_child(root)
- var bag := PlayerBag.new()
- var catches := FishInventory.new()
- var capacity := PlayerCoolerCapacity.new()
- var layout := PlayerInventoryLayout.new()
- var hotbar := PlayerHotbar.new()
- for node: Node in [bag, catches, capacity, layout, hotbar]:
- root.add_child(node)
- bag.setup(ItemCatalogResource)
- layout.setup(bag, catches, ItemCatalogResource, capacity)
- bag.set_inventory_layout(layout)
- catches.set_inventory_layout(layout)
- hotbar.setup(bag, ItemCatalogResource, catches, layout)
- assert(PlayerInventoryLayout.INVENTORY_CAPACITIES == [9, 18, 27, 36])
- assert(PlayerCoolerCapacity.CAPACITIES == [9, 18, 27, 36, 45, 54, 63, 72])
- var basic_cube: ItemData = ItemCatalogResource.get_item_by_id(&"basic_cube")
- assert(basic_cube != null)
- assert(basic_cube.icon != null)
- assert(basic_cube.hotbar_allowed)
- assert(bag.add_item(&"basic_cube"))
- assert(layout.is_item_in_inventory(&"basic_cube"))
- assert(hotbar.assign_item(0, &"basic_cube"))
- assert(hotbar.get_item_id(0) == &"basic_cube")
- assert(
- layout.get_container(
- PlayerInventoryLayout.EntryKind.ITEM,
- &"basic_cube",
- ) == PlayerInventoryLayout.InventoryContainer.HOTBAR
- )
- assert(hotbar.clear_slot(0))
- assert(layout.is_item_in_inventory(&"basic_cube"))
- assert(bag.remove_item(&"basic_cube"))
- var packed_decor_slot: int = hotbar.find_first_empty_slot()
- assert(packed_decor_slot == 0)
- assert(layout.prepare_new_item_placement(
- &"basic_cube",
- PlayerInventoryLayout.InventoryContainer.HOTBAR,
- packed_decor_slot,
- ))
- assert(bag.add_item(&"basic_cube"))
- assert(
- layout.get_container(
- PlayerInventoryLayout.EntryKind.ITEM,
- &"basic_cube",
- ) == PlayerInventoryLayout.InventoryContainer.HOTBAR
- )
- assert(hotbar.assign_item(packed_decor_slot, &"basic_cube"))
- assert(hotbar.select_slot(packed_decor_slot))
- assert(hotbar.get_selected_item_id() == &"basic_cube")
- assert(bag.remove_item(&"basic_cube"))
-
- assert(bag.add_item(&"coffee", 3))
- assert(layout.get_inventory_count() == 1)
- assert(layout.get_inventory_capacity() == 9)
- assert(layout.get_hotbar_count() == 0)
- assert(hotbar.assign_item(2, &"coffee"))
- assert(layout.get_inventory_count() == 0)
- assert(layout.get_hotbar_count() == 1)
- assert(
- layout.get_container(PlayerInventoryLayout.EntryKind.ITEM, &"coffee")
- == PlayerInventoryLayout.InventoryContainer.HOTBAR
- )
- assert(hotbar.clear_slot(2))
- assert(layout.get_inventory_count() == 1)
- assert(layout.get_hotbar_count() == 0)
-
- var fish_catch := FishCatch.new()
- fish_catch.fish = Bluegill
- fish_catch.fish_id = Bluegill.id
- fish_catch.catch_id = &"bluegill:unified_inventory_test"
- fish_catch.weight_lb = Bluegill.get_minimum_weight()
- fish_catch.display_scale = Bluegill.get_display_scale_for_weight(
- fish_catch.weight_lb
- )
- fish_catch.sale_value = Bluegill.get_sale_value_for_weight(
- fish_catch.weight_lb
- )
- fish_catch.quality = FishQuality.Tier.IMPRESSIVE
- assert(catches.add_catch(fish_catch))
- assert(layout.get_inventory_count() == 2)
- assert(layout.move_entry_to_first_free(
- PlayerInventoryLayout.EntryKind.CATCH,
- fish_catch.catch_id,
- PlayerInventoryLayout.InventoryContainer.STORAGE,
- ))
- assert(layout.get_inventory_count() == 1)
- assert(layout.get_storage_count() == 1)
- assert(layout.get_storage_capacity() == 9)
- var inventory_grid := GeneralInventoryGrid.new()
- root.add_child(inventory_grid)
- inventory_grid.set_slot_presentation(Vector2(78.0, 78.0), 10)
- inventory_grid.setup(
- layout,
- bag,
- catches,
- hotbar,
- ItemCatalogResource,
- PlayerInventoryLayout.InventoryContainer.INVENTORY,
- )
- var all_inventory_slots: Array = inventory_grid.get("_slots")
- assert(all_inventory_slots.size() == 36)
- assert(inventory_grid.get_slots().size() == 9)
- for slot_index: int in all_inventory_slots.size():
- var slot := all_inventory_slots[slot_index] as GeneralInventorySlot
- assert(slot.custom_minimum_size == Vector2(78.0, 78.0))
- assert(slot.disabled == (slot_index >= 9))
- assert(slot.tooltip_text.is_empty())
- var normal := slot.get_theme_stylebox("normal") as StyleBoxFlat
- assert(normal != null and normal.corner_radius_top_left == 39)
- var locked_slot := all_inventory_slots[9] as GeneralInventorySlot
- var locked_icon := locked_slot.get("_icon") as TextureRect
- assert(locked_icon != null)
- assert(is_equal_approx(locked_icon.size.x, 24.0))
- assert(is_equal_approx(locked_icon.size.y, 24.0))
- assert(is_equal_approx(locked_icon.modulate.a, 0.18))
- var staged_slot := all_inventory_slots[0] as GeneralInventorySlot
- staged_slot.set_staged(true)
- assert(
- (staged_slot.get_theme_stylebox("normal") as StyleBoxFlat).bg_color
- == Color(UtilityPageStyle.OCEAN_SELECTED, 0.92)
- )
- staged_slot.set_staged(false)
- var quality_slot := GeneralInventorySlot.new()
- root.add_child(quality_slot)
- quality_slot.set_presentation_size(Vector2(78.0, 78.0))
- quality_slot.configure(
- 0,
- PlayerInventoryLayout.InventoryContainer.STORAGE,
- false,
- layout,
- bag,
- catches,
- hotbar,
- ItemCatalogResource,
- )
- assert(quality_slot.entry_identity == fish_catch.catch_id)
- assert(
- (quality_slot.get_theme_stylebox("normal") as StyleBoxFlat).bg_color
- == GeneralInventorySlot.quality_background_color(
- FishQuality.Tier.IMPRESSIVE,
- GeneralInventorySlot.QualityEmphasis.NORMAL,
- )
- )
- quality_slot.set_staged(true)
- assert(
- (quality_slot.get_theme_stylebox("normal") as StyleBoxFlat).bg_color
- == GeneralInventorySlot.quality_background_color(
- FishQuality.Tier.IMPRESSIVE,
- GeneralInventorySlot.QualityEmphasis.SELECTED,
- )
- )
- var distinct_quality_colors: Dictionary[Color, bool] = {}
- for quality: int in FishQuality.TIER_COUNT:
- var quality_color := GeneralInventorySlot.quality_background_color(
- quality,
- GeneralInventorySlot.QualityEmphasis.NORMAL,
- )
- assert(not distinct_quality_colors.has(quality_color))
- distinct_quality_colors[quality_color] = true
- assert(distinct_quality_colors.size() == FishQuality.TIER_COUNT)
- var sale_tray_slot := ShopSaleTraySlot.new()
- root.add_child(sale_tray_slot)
- sale_tray_slot.configure(
- "catch:%s" % fish_catch.catch_id,
- fish_catch.fish.display_texture,
- fish_catch.fish.display_name,
- 1,
- fish_catch.quality,
- )
- assert(
- sale_tray_slot.custom_minimum_size
- == GeneralInventoryGrid.DEFAULT_SLOT_SIZE
- )
- var sale_tray_style := sale_tray_slot.get_theme_stylebox(
- "normal"
- ) as StyleBoxFlat
- assert(sale_tray_style != null)
- assert(sale_tray_style.corner_radius_top_left == 26)
- var expected_tray_quality_color := (
- GeneralInventorySlot.quality_background_color(
- FishQuality.Tier.IMPRESSIVE,
- GeneralInventorySlot.QualityEmphasis.NORMAL,
- )
- )
- expected_tray_quality_color.a = 0.96
- assert(sale_tray_style.bg_color == expected_tray_quality_color)
- var wallet := PlayerWallet.new()
- root.add_child(wallet)
- assert(wallet.restore_balance(15000))
- assert(layout.get_next_backpack_cost() == 1500)
- assert(layout.purchase_backpack(wallet))
- assert(layout.get_inventory_capacity() == 18)
- assert(inventory_grid.get_slots().size() == 18)
- assert(layout.get_next_backpack_cost() == 4500)
- assert(layout.purchase_backpack(wallet))
- assert(layout.get_inventory_capacity() == 27)
- assert(inventory_grid.get_slots().size() == 27)
- assert(layout.get_next_backpack_cost() == 9000)
- assert(layout.purchase_backpack(wallet))
- assert(layout.get_inventory_capacity() == 36)
- assert(inventory_grid.get_slots().size() == 36)
- assert(layout.get_next_backpack_cost() == -1)
- var storage_grid := GeneralInventoryGrid.new()
- root.add_child(storage_grid)
- storage_grid.setup(
- layout,
- bag,
- catches,
- hotbar,
- ItemCatalogResource,
- PlayerInventoryLayout.InventoryContainer.STORAGE,
- )
- var all_storage_slots: Array = storage_grid.get("_slots")
- assert(all_storage_slots.size() == 72)
- assert(storage_grid.get_slots().size() == 9)
- assert(
- (all_storage_slots[9] as GeneralInventorySlot).accessibility_name
- == "locked storage slot 10"
- )
- var storage_wallet := PlayerWallet.new()
- root.add_child(storage_wallet)
- assert(storage_wallet.restore_balance(9500))
- for expected_capacity: int in [18, 27, 36, 45, 54, 63, 72]:
- assert(capacity.purchase(storage_wallet))
- assert(layout.get_storage_capacity() == expected_capacity)
- assert(storage_grid.get_slots().size() == expected_capacity)
- assert(capacity.get_next_capacity() == -1)
- assert(capacity.get_next_cost() == -1)
-
- var saved := layout.to_save_data()
- assert(layout.move_entry_to_first_free(
- PlayerInventoryLayout.EntryKind.ITEM,
- &"coffee",
- PlayerInventoryLayout.InventoryContainer.STORAGE,
- ))
- assert(layout.restore_from_save_data(saved))
- assert(layout.is_item_in_inventory(&"coffee"))
- assert(
- layout.get_container(
- PlayerInventoryLayout.EntryKind.CATCH,
- fish_catch.catch_id,
- ) == PlayerInventoryLayout.InventoryContainer.STORAGE
- )
-
- var network_sale := NetworkSaleService.new()
- var session := NetworkSession.new()
- root.add_child(session)
- root.add_child(network_sale)
- network_sale.set("_session", session)
- network_sale.set("_item_catalog", ItemCatalogResource)
- network_sale.set("_fish_catalog", FishCatalogResource)
- var result: Dictionary = network_sale.call(
- "_build_authoritative_result",
- 1,
- "mixed_sale_test",
- [],
- [{"item_id": "coffee", "quantity": 2}],
- MainShopBuyer,
- )
- assert(bool(result.get("accepted", false)))
- assert(int(result.get("payout", -1)) == 20)
- assert((result.get("items", []) as Array).size() == 1)
-
- print("Unified inventory validation: PASS")
- root.free()
- quit()
diff --git a/tests/unified_inventory_validation.gd.uid b/tests/unified_inventory_validation.gd.uid
deleted file mode 100644
index baaf9c3..0000000
--- a/tests/unified_inventory_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://osuf2xa2bxht
diff --git a/tests/world_layout_multiplayer_validation.gd b/tests/world_layout_multiplayer_validation.gd
deleted file mode 100644
index 51e7be6..0000000
--- a/tests/world_layout_multiplayer_validation.gd
+++ /dev/null
@@ -1,126 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-const TEST_PORT: int = 18155
-const TEST_SEED: int = 86421357
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var arguments: PackedStringArray = OS.get_cmdline_user_args()
- if arguments.has("host"):
- await _run_host()
- return
- if arguments.has("client"):
- await _run_client()
- return
- push_error("World layout multiplayer validation needs host or client mode.")
- quit(1)
-
-
-func _run_host() -> void:
- var main: Node = await _create_initialized_main()
- var session := main.get_node("%NetworkSession") as NetworkSession
- var save_manager := main.get("_save_manager") as PlayerSaveManager
- assert(bool(main.call(
- "_apply_world",
- WorldLayout.STARTER_ISLAND,
- TEST_SEED,
- true,
- )))
- assert(bool(main.call(
- "_prepare_host_world",
- WorldLayout.STARTER_ISLAND,
- TEST_SEED,
- )))
- assert(save_manager.initialize_new_game(
- TEST_SEED,
- WorldLayout.STARTER_ISLAND,
- ))
- assert(session.start_private_host(TEST_PORT))
- main.call("_enter_gameplay")
- assert(session.set_host_open(true))
- var remote_peer_id: int = await _wait_for_remote_peer(session)
- assert(remote_peer_id > 1)
- assert(session.peer_supports_capability(
- remote_peer_id,
- NetworkProtocol.WORLD_LAYOUT_CAPABILITY,
- ))
- var disconnect_deadline: int = Time.get_ticks_msec() + 12000
- while (
- Time.get_ticks_msec() < disconnect_deadline
- and session.is_authenticated_peer(remote_peer_id)
- ):
- await process_frame
- assert(not session.is_authenticated_peer(remote_peer_id))
- print("World layout multiplayer host validation: PASS")
- await _cleanup(main, session)
-
-
-func _run_client() -> void:
- var main: Node = await _create_initialized_main()
- main.call(
- "_on_title_join_game_requested",
- "127.0.0.1:%d" % TEST_PORT,
- )
- var session := main.get_node("%NetworkSession") as NetworkSession
- var deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
- main.call("_confirm_server_trust")
- if session.is_joined_client() and bool(main.get("_gameplay_started")):
- break
- assert(session.is_joined_client())
- assert(session.supports_server_capability(
- NetworkProtocol.WORLD_LAYOUT_CAPABILITY
- ))
- var metadata: Dictionary = session.get_last_server_metadata()
- assert(str(metadata.get("world_layout", "")) == String(
- WorldLayout.STARTER_ISLAND
- ))
- assert(int(metadata.get("world_seed", 0)) == TEST_SEED)
- var world := main.get("_test_world") as TestWorld
- assert(world.get_world_layout() == WorldLayout.STARTER_ISLAND)
- assert(world.get_generation_seed() == TEST_SEED)
- assert(world.get_node_or_null("Regions/StarterIslandRegion") != null)
- assert(world.get_fishing_shop() != null)
- assert(world.get_player_storage() != null)
- print("World layout multiplayer client validation: PASS")
- await _cleanup(main, session)
-
-
-func _create_initialized_main() -> Node:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- return main
-
-
-func _wait_for_remote_peer(session: NetworkSession) -> int:
- var deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- for peer_id: int in session.get_authenticated_peer_ids():
- if peer_id != session.get_local_peer_id():
- return peer_id
- return 0
-
-
-func _cleanup(main: Node, session: NetworkSession) -> void:
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
diff --git a/tests/world_layout_multiplayer_validation.gd.uid b/tests/world_layout_multiplayer_validation.gd.uid
deleted file mode 100644
index e666cb2..0000000
--- a/tests/world_layout_multiplayer_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bg52pbgv5ykjc
diff --git a/tests/world_layout_validation.gd b/tests/world_layout_validation.gd
deleted file mode 100644
index e6eec39..0000000
--- a/tests/world_layout_validation.gd
+++ /dev/null
@@ -1,159 +0,0 @@
-extends SceneTree
-
-const TestWorldScene: PackedScene = preload("res://world/test_world.tscn")
-const SetupPageScene: PackedScene = preload(
- "res://ui/new_game_setup_page.tscn"
-)
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- root.size = Vector2i(1280, 720)
- _validate_layout_values()
- await _validate_setup_page()
- await _validate_world_switching()
- _validate_network_metadata()
- print("World layout validation: PASS")
- quit()
-
-
-func _validate_layout_values() -> void:
- assert(WorldLayout.is_valid(WorldLayout.GENERATED))
- assert(WorldLayout.is_valid(String(WorldLayout.STARTER_ISLAND)))
- assert(not WorldLayout.is_valid(&"unknown"))
- assert(WorldLayout.normalized("unknown") == WorldLayout.GENERATED)
- assert(WorldLayout.display_name(WorldLayout.STARTER_ISLAND) == "starter island")
-
-
-func _validate_setup_page() -> void:
- assert(NewGameSetupPage.parse_seed_text(" 24680 ") == 24680)
- assert(NewGameSetupPage.parse_seed_text("0") == 0)
- assert(NewGameSetupPage.parse_seed_text("2147483647") == 0)
- assert(NewGameSetupPage.parse_seed_text("") == 0)
- var text_seed: int = NewGameSetupPage.parse_seed_text("voyager island")
- assert(text_seed > 0 and text_seed <= PlayerSaveManager.MAX_WORLD_SEED)
- assert(NewGameSetupPage.parse_seed_text(" voyager island ") == text_seed)
- assert(NewGameSetupPage.parse_seed_text("voyager island") == text_seed)
- assert(NewGameSetupPage.parse_seed_text("Voyager Island") != text_seed)
- var page := SetupPageScene.instantiate() as NewGameSetupPage
- root.add_child(page)
- await process_frame
- page.open_page(true)
- await process_frame
- assert(page.visible)
- var paper := page.get_node("%Paper") as PanelContainer
- assert(paper.get_combined_minimum_size().x <= paper.size.x)
- assert(paper.get_combined_minimum_size().y <= paper.size.y)
- assert(page.get_selected_world_layout() == WorldLayout.GENERATED)
- assert(page.get_selected_world_seed() > 0)
- assert((page.get_node("%OverwriteWarning") as Label).visible)
- var generated_button := page.get_node("%GeneratedButton") as Button
- var starter_button := page.get_node("%StarterButton") as Button
- var random_button := page.get_node("%RandomSeedButton") as Button
- var custom_button := page.get_node("%CustomSeedButton") as Button
- var seed_edit := page.get_node("%SeedEdit") as LineEdit
- var start_button := page.get_node("%StartButton") as Button
- var back_button := page.get_node("%BackButton") as Button
- assert(generated_button.get_node(generated_button.focus_neighbor_right) == starter_button)
- assert(generated_button.get_node(generated_button.focus_neighbor_bottom) == random_button)
- assert(random_button.get_node(random_button.focus_neighbor_right) == custom_button)
- assert(start_button.get_node(start_button.focus_neighbor_right) == back_button)
- page.call("_choose_custom_seed")
- assert(seed_edit.editable)
- assert(custom_button.get_node(custom_button.focus_neighbor_bottom) == seed_edit)
- assert(seed_edit.get_node(seed_edit.focus_neighbor_bottom) == start_button)
- page.call("_set_world_layout", WorldLayout.STARTER_ISLAND)
- assert(not (page.get_node("%SeedSection") as Control).visible)
- var request: Dictionary = {}
- page.start_requested.connect(func(layout: StringName, seed: int) -> void:
- request["layout"] = layout
- request["seed"] = seed
- )
- page.call("_request_start")
- assert(request.get("layout") == WorldLayout.STARTER_ISLAND)
- assert(int(request.get("seed", 0)) > 0)
- page.queue_free()
- await process_frame
-
-
-func _validate_world_switching() -> void:
- var world := TestWorldScene.instantiate() as TestWorld
- root.add_child(world)
- await process_frame
- assert(world.get_world_layout() == WorldLayout.GENERATED)
- var initial_generator := world.get_node(
- "Regions/GeneratedWorldRegion/Terrain/TerrainChunkGenerator"
- ) as TerrainChunkGenerator
- assert(initial_generator != null)
- assert(initial_generator.get_generated_chunks_root() == null)
- assert(world.get_fishing_shop() != null)
- assert(world.get_player_storage() != null)
- _validate_world_boundary_clearance(world)
- assert(world.activate_world(WorldLayout.STARTER_ISLAND, 13579))
- assert(world.get_world_layout() == WorldLayout.STARTER_ISLAND)
- assert(world.get_generation_seed() == 13579)
- assert(world.get_node_or_null("Regions/StarterIslandRegion") != null)
- assert(world.get_node_or_null("Regions/GeneratedWorldRegion") == null)
- assert(world.get_fishing_shop() != null)
- assert(world.get_player_storage() != null)
- assert(not world.get_fishable_water_regions().is_empty())
- var starter_safe_points := world.get_safe_respawn_points()
- assert(not starter_safe_points.is_empty())
- var authored_safe_point: SafeRespawnPoint = starter_safe_points.back()
- assert(
- world.get_water_recovery_position(
- authored_safe_point.global_position
- ).is_equal_approx(authored_safe_point.global_position)
- )
- assert(world.activate_world(WorldLayout.GENERATED, 24680))
- assert(world.get_world_layout() == WorldLayout.GENERATED)
- assert(world.get_generation_seed() == 24680)
- assert(world.get_node_or_null("Regions/GeneratedWorldRegion") != null)
- assert(world.get_node_or_null("Regions/StarterIslandRegion") == null)
- world.queue_free()
- await process_frame
-
-
-func _validate_world_boundary_clearance(world: TestWorld) -> void:
- var active_region := world.get("_active_region") as WorldRegion
- assert(active_region != null)
- var half := active_region.get_playable_half_extents()
- var north := world.get_node("WorldBounds/North") as StaticBody3D
- var south := world.get_node("WorldBounds/South") as StaticBody3D
- var west := world.get_node("WorldBounds/West") as StaticBody3D
- var east := world.get_node("WorldBounds/East") as StaticBody3D
- var fishing_defaults := FishingSpot.new()
- assert(
- TestWorld.WORLD_BOUNDARY_SHORELINE_CLEARANCE
- > fishing_defaults.maximum_cast_distance + 1.0
- )
- fishing_defaults.free()
- assert(north.position.z - half.y >= 18.0)
- assert(-south.position.z - half.y >= 18.0)
- assert(-west.position.x - half.x >= 18.0)
- assert(east.position.x - half.x >= 18.0)
-
-
-func _validate_network_metadata() -> void:
- var hello: Dictionary = NetworkProtocol.make_server_hello(
- true,
- NetworkProtocol.RejectionCode.NONE,
- "session",
- 2,
- 1,
- 16,
- "room",
- 97531,
- WorldLayout.STARTER_ISLAND,
- )
- assert(str(hello.get("world_layout", "")) == String(
- WorldLayout.STARTER_ISLAND
- ))
- assert(int(hello.get("world_seed", 0)) == 97531)
- assert(
- NetworkProtocol.WORLD_LAYOUT_CAPABILITY
- in PackedStringArray(hello.get("capability_flags", []))
- )
diff --git a/tests/world_layout_validation.gd.uid b/tests/world_layout_validation.gd.uid
deleted file mode 100644
index 9002c58..0000000
--- a/tests/world_layout_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://bgxgyqq73sdpi
diff --git a/tests/world_spawn_multiplayer_validation.gd b/tests/world_spawn_multiplayer_validation.gd
deleted file mode 100644
index 45be081..0000000
--- a/tests/world_spawn_multiplayer_validation.gd
+++ /dev/null
@@ -1,239 +0,0 @@
-extends SceneTree
-
-const MainScene: PackedScene = preload("res://main/main.tscn")
-const TEST_PORT: int = 18170
-const EXPECTED_POPULATION: int = 8
-const EXPECTED_BY_TYPE: Dictionary = {
- &"crab_brown": 2,
- &"clam_manila": 3,
- &"beetle_stag_common": 3,
-}
-const WINTER_POPULATION: int = 5
-const WINTER_BY_TYPE: Dictionary = {
- &"crab_brown": 2,
- &"clam_manila": 3,
-}
-const SUMMER_DATE_ID: String = "2026-08-18"
-const WINTER_DATE_ID: String = "2026-12-18"
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var arguments: PackedStringArray = OS.get_cmdline_user_args()
- if arguments.has("host"):
- await _run_host()
- return
- if arguments.has("client"):
- await _run_client()
- return
- push_error("World spawn multiplayer validation needs host or client mode.")
- quit(1)
-
-
-func _run_host() -> void:
- var main: Node = await _create_initialized_main()
- var session := main.get_node("%NetworkSession") as NetworkSession
- var service: Node = main.get_node("%NetworkWorldSpawnService")
- var world_time := main.get_node("%WorldTimeService") as WorldTimeService
- assert(bool(main.call(
- "_apply_world", WorldLayout.STARTER_ISLAND, 1, true
- )))
- assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
- assert(session.start_dedicated_host(TEST_PORT, 8, "127.0.0.1"))
- assert(world_time.synchronize_calendar_time(12.0, SUMMER_DATE_ID))
- assert(world_time.set_authoritative_time(12.0))
- assert(session.set_host_open(true))
- await _wait_for_population(service, EXPECTED_POPULATION)
- _freeze_timed_entries(service)
- _validate_population(service, true)
-
- var remote_peer_id: int = 0
- var join_deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < join_deadline and remote_peer_id == 0:
- await process_frame
- for peer_id: int in session.get_authenticated_peer_ids():
- if peer_id != session.get_local_peer_id():
- remote_peer_id = peer_id
- break
- assert(remote_peer_id > 1)
- assert(session.peer_supports_capability(
- remote_peer_id,
- NetworkProtocol.WORLD_SPAWN_CAPABILITY,
- ))
- await create_timer(0.75).timeout
- assert(world_time.synchronize_calendar_time(12.0, WINTER_DATE_ID))
- await _wait_for_population(service, WINTER_POPULATION)
- _validate_population(service, true, WINTER_BY_TYPE)
- await create_timer(0.75).timeout
- assert(world_time.synchronize_calendar_time(12.0, SUMMER_DATE_ID))
- await _wait_for_population(service, EXPECTED_POPULATION)
- _validate_population(service, true)
-
- var disconnect_deadline: int = Time.get_ticks_msec() + 12000
- while (
- Time.get_ticks_msec() < disconnect_deadline
- and session.is_authenticated_peer(remote_peer_id)
- ):
- await process_frame
- assert(not session.is_authenticated_peer(remote_peer_id))
- _validate_population(service, true)
- _validate_respawn_budget(service)
- print("World spawn multiplayer host validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _run_client() -> void:
- var main: Node = await _create_initialized_main()
- main.call(
- "_on_title_join_game_requested",
- "127.0.0.1:%d" % TEST_PORT,
- )
- var session := main.get_node("%NetworkSession") as NetworkSession
- var service: Node = main.get_node("%NetworkWorldSpawnService")
- var world_time := main.get_node("%WorldTimeService") as WorldTimeService
- var join_deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < join_deadline:
- await process_frame
- if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
- main.call("_confirm_server_trust")
- if session.is_joined_client() and bool(main.get("_gameplay_started")):
- break
- assert(session.is_joined_client())
- assert(session.supports_server_capability(
- NetworkProtocol.WORLD_SPAWN_CAPABILITY
- ))
- await _wait_for_population(service, EXPECTED_POPULATION)
- _validate_population(service, false)
- await _wait_for_population(service, WINTER_POPULATION)
- _validate_population(service, false, WINTER_BY_TYPE)
- assert(world_time.get_calendar_date_id() == WINTER_DATE_ID)
- await _wait_for_population(service, EXPECTED_POPULATION)
- _validate_population(service, false)
- assert(world_time.get_calendar_date_id() == SUMMER_DATE_ID)
- print("World spawn multiplayer client validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _wait_for_population(service: Node, expected_population: int) -> void:
- var deadline: int = Time.get_ticks_msec() + 12000
- while Time.get_ticks_msec() < deadline:
- await process_frame
- if (service.get("_entities") as Dictionary).size() == expected_population:
- return
- assert(false, "Timed out waiting for the world spawn population.")
-
-
-func _validate_population(
- service: Node,
- expect_authoritative_state: bool,
- expected_by_type: Dictionary = EXPECTED_BY_TYPE,
-) -> void:
- var entities: Dictionary = service.get("_entities")
- var presentations: Dictionary = service.get("_presentations")
- var expected_population: int = 0
- for amount: int in expected_by_type.values():
- expected_population += amount
- assert(entities.size() == expected_population)
- assert(presentations.size() == expected_population)
- var counts: Dictionary = {}
- for state: Dictionary in entities.values():
- var type_id := state.get("type_id") as StringName
- counts[type_id] = int(counts.get(type_id, 0)) + 1
- var position: Variant = state.get("position")
- assert(typeof(position) == TYPE_VECTOR3)
- assert((position as Vector3).is_finite())
- var entry := state.get("data") as GatherableData
- assert(entry != null)
- assert(
- (position as Vector3).y
- >= entry.minimum_surface_y - 0.001
- )
- if expect_authoritative_state:
- var quality: int = int(state.get("quality", -1))
- assert(FishQuality.is_valid(quality))
- if entry.is_stationary_spawn():
- assert(entry.get_movement_speed_for_quality(quality) <= 0.0)
- assert(not entry.can_be_scared())
- else:
- assert(entry.get_movement_speed_for_quality(quality) > 0.0)
- assert(entry.get_scare_radius_for_quality(quality) > 0.0)
- assert(counts == expected_by_type)
-
-
-func _freeze_timed_entries(service: Node) -> void:
- for state: Dictionary in (service.get("_entities") as Dictionary).values():
- var entry := state.get("data") as GatherableData
- if entry != null and entry.active_lifetime_seconds > 0.0:
- state["expires_at"] = INF
-
-
-func _validate_respawn_budget(service: Node) -> void:
- var entities: Dictionary = service.get("_entities")
- var crab_ids: Array[String] = []
- for entity_id: String in entities:
- var state: Dictionary = entities[entity_id]
- if state.get("type_id") == &"crab_brown":
- crab_ids.append(entity_id)
- assert(crab_ids.size() == 2)
- for entity_id: String in crab_ids:
- service.call(
- "_despawn_entity",
- entity_id,
- &"captured",
- false,
- true,
- )
- assert((service.get("_entities") as Dictionary).size() == 6)
-
- var now: float = Time.get_ticks_msec() / 1000.0
- var respawns: Array = service.get("_respawns")
- assert(respawns.size() == crab_ids.size())
- for index: int in respawns.size():
- var respawn: Dictionary = respawns[index]
- var delay: float = float(respawn.get("due", 0.0)) - now
- assert(delay >= 479.0 and delay <= 721.0)
- respawn["due"] = now - 1.0
- respawns[index] = respawn
-
- service.call("_update_respawns")
- assert((service.get("_entities") as Dictionary).size() == 7)
- assert((service.get("_respawns") as Array).size() == 1)
- var next_by_type: Dictionary = service.get("_next_respawn_by_type")
- var next_allowed: float = float(next_by_type.get(&"crab_brown", 0.0))
- assert(next_allowed - now >= 179.0)
-
- service.call("_update_respawns")
- assert((service.get("_entities") as Dictionary).size() == 7)
- assert((service.get("_respawns") as Array).size() == 1)
- next_by_type[&"crab_brown"] = now - 1.0
- service.call("_update_respawns")
- assert((service.get("_entities") as Dictionary).size() == 8)
- assert((service.get("_respawns") as Array).is_empty())
- _validate_population(service, true)
-
-
-func _create_initialized_main() -> Node:
- root.size = Vector2i(1280, 720)
- var main: Node = MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- return main
diff --git a/tests/world_spawn_multiplayer_validation.gd.uid b/tests/world_spawn_multiplayer_validation.gd.uid
deleted file mode 100644
index ea4f1de..0000000
--- a/tests/world_spawn_multiplayer_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://wumg5ev7jr6e
diff --git a/tests/world_spawn_protocol_validation.gd b/tests/world_spawn_protocol_validation.gd
deleted file mode 100644
index 0840f2d..0000000
--- a/tests/world_spawn_protocol_validation.gd
+++ /dev/null
@@ -1,286 +0,0 @@
-extends SceneTree
-
-const Gatherables: GatherableCatalog = preload(
- "res://gathering/catalog/gatherable_catalog.tres"
-)
-const FishCatalog: FishPool = preload("res://fish/pools/fish_catalog.tres")
-const GatheringControllerType = preload(
- "res://gathering/gathering_controller.gd"
-)
-const WorldGatherableType = preload("res://gathering/world_gatherable.gd")
-
-const CRAB_PIXEL_SIZE: float = 0.0005
-const REDUCED_CATCH_RING_RADIUS: float = 0.175
-const NET_STRIKE_MARKER_DISTANCE: float = 0.85
-const CalendarSeasonType = preload("res://world/calendar_season.gd")
-
-
-func _initialize() -> void:
- call_deferred(&"_run")
-
-
-func _run() -> void:
- assert(NetworkProtocol.PROTOCOL_VERSION == 12)
- assert(
- NetworkWorldSpawnProtocol.CAPABILITY
- == NetworkProtocol.WORLD_SPAWN_CAPABILITY
- )
- assert(NetworkWorldSpawnProtocol.SNAPSHOT_ENTITIES_PER_ENVELOPE <= 4)
- _validate_catalog_statuses()
- await _validate_billboard_presentation()
- _validate_envelopes()
- print("World spawn protocol validation: PASS")
- quit()
-
-
-func _validate_catalog_statuses() -> void:
- var brown: GatherableData = Gatherables.get_entry(&"crab_brown")
- assert(brown != null and brown.is_available())
- assert(brown.catch_data.active)
- assert(brown.catch_data.collection_method == FishData.CollectionMethod.NET)
- assert(brown.catch_data.logbook_section == FishData.LogbookSection.SHELLFISH)
- assert(brown.population == 2)
- assert(is_equal_approx(brown.charge_duration, 2.0))
- assert(
- is_equal_approx(
- brown.sprite_pixel_size,
- CRAB_PIXEL_SIZE,
- )
- )
- _validate_quality_behavior(brown)
- assert(is_equal_approx(brown.capture_respawn_min_seconds, 480.0))
- assert(is_equal_approx(brown.capture_respawn_max_seconds, 720.0))
- assert(is_equal_approx(brown.scare_respawn_min_seconds, 45.0))
- assert(is_equal_approx(brown.scare_respawn_max_seconds, 90.0))
- assert(is_equal_approx(brown.minimum_respawn_spacing_seconds, 180.0))
- assert(brown.required_tool_id == &"crab_net")
- assert(is_equal_approx(brown.minimum_surface_y, -0.44))
- assert(FishCatalog.get_fish_by_id(&"crab_brown") == brown.catch_data)
- assert(not brown.catch_data.is_fishable())
- var clam: GatherableData = Gatherables.get_entry(&"clam_manila")
- assert(clam != null and clam.is_available())
- assert(clam.catch_data.collection_method == FishData.CollectionMethod.DIGGING)
- assert(clam.catch_data.logbook_section == FishData.LogbookSection.SHELLFISH)
- assert(clam.required_tool_id == &"standard_shovel")
- assert(clam.diggable_area_id == &"starter_beach")
- assert(clam.presentation_mode == GatherableData.PresentationMode.WATER_SPURT)
- assert(clam.is_stationary_hotspot())
- assert(not clam.requires_sneaking)
- assert(not clam.can_be_scared())
- assert(is_equal_approx(clam.active_lifetime_seconds, 10.0))
- assert(FishCatalog.get_fish_by_id(&"clam_manila") == clam.catch_data)
- assert(not clam.catch_data.is_fishable())
- var beetle: GatherableData = Gatherables.get_entry(&"beetle_stag_common")
- assert(beetle != null and beetle.is_available())
- assert(beetle.is_available(CalendarSeasonType.Season.SUMMER))
- assert(not beetle.is_available(CalendarSeasonType.Season.WINTER))
- assert(beetle.catch_data.collection_method == FishData.CollectionMethod.NET)
- assert(beetle.required_tool_id == &"crab_net")
- assert(beetle.spawn_anchor_set_id == &"starter_reachable_tree_trunks")
- assert(is_equal_approx(beetle.spawn_anchor_occupancy_ratio, 0.35))
- assert(beetle.maximum_anchor_population == 64)
- assert(beetle.is_stationary_spawn())
- assert(not beetle.is_stationary_hotspot())
- assert(not beetle.requires_sneaking)
- assert(not beetle.can_be_scared())
- assert(FishCatalog.get_fish_by_id(&"beetle_stag_common") == beetle.catch_data)
- assert(not beetle.catch_data.is_fishable())
-
- for type_id: StringName in [
- &"crab_ghost",
- &"crab_blue",
- &"crab_dungeness",
- ]:
- var entry: GatherableData = Gatherables.get_entry(type_id)
- assert(entry != null)
- assert(entry.is_valid())
- assert(not entry.catch_data.active)
- assert(not entry.is_available())
- assert(
- is_equal_approx(
- entry.sprite_pixel_size,
- CRAB_PIXEL_SIZE,
- )
- )
- assert(entry.catch_data.collection_method == FishData.CollectionMethod.NET)
- assert(
- entry.catch_data.logbook_section
- == FishData.LogbookSection.SHELLFISH
- )
-
- var available: Array[GatherableData] = Gatherables.get_available_entries()
- assert(available.size() == 3)
- assert(available.has(brown))
- assert(available.has(clam))
- assert(available.has(beetle))
- var winter_available: Array[GatherableData] = (
- Gatherables.get_available_entries(CalendarSeasonType.Season.WINTER)
- )
- assert(winter_available.has(brown))
- assert(winter_available.has(clam))
- assert(not winter_available.has(beetle))
- var rng := RandomNumberGenerator.new()
- rng.seed = 24680
- var captured_delay: float = brown.get_respawn_delay(&"captured", rng)
- var scared_delay: float = brown.get_respawn_delay(&"scared", rng)
- assert(captured_delay >= 480.0 and captured_delay <= 720.0)
- assert(scared_delay >= 45.0 and scared_delay <= 90.0)
-
- var gathering_controller := GatheringControllerType.new()
- assert(
- is_equal_approx(
- gathering_controller.marker_radius,
- REDUCED_CATCH_RING_RADIUS,
- )
- )
- assert(
- is_equal_approx(
- gathering_controller.marker_distance,
- NET_STRIKE_MARKER_DISTANCE,
- )
- )
- gathering_controller.free()
-
-
-func _validate_billboard_presentation() -> void:
- var gatherable := WorldGatherableType.new()
- gatherable.call("_ensure_visual")
- var sprite := gatherable.get_node("GatherableSprite") as Sprite3D
- assert(sprite != null)
- assert(sprite.billboard == BaseMaterial3D.BILLBOARD_ENABLED)
- assert(sprite.texture_filter == BaseMaterial3D.TEXTURE_FILTER_NEAREST)
- assert(not sprite.shaded)
- gatherable.free()
-
- var unconfigured_hotspot := WorldGatherableType.new()
- unconfigured_hotspot.call("_ensure_water_spurt_visual")
- assert(
- unconfigured_hotspot.get_node_or_null(
- "WaterSpurt/WaterDroplets"
- ) == null
- )
- unconfigured_hotspot.free()
-
- var clam: GatherableData = Gatherables.get_entry(&"clam_manila")
- assert(clam != null)
- var hotspot := WorldGatherableType.new()
- root.add_child(hotspot)
- var configured_position := Vector3(24.0, 0.3, -11.0)
- hotspot.configure(
- "world:clam-presentation-regression",
- clam,
- configured_position,
- 0.0,
- )
- assert(hotspot.global_position.is_equal_approx(configured_position))
- var hole := hotspot.get_node("WaterSpurt/BurrowMark") as MeshInstance3D
- assert(hole != null)
- assert(hole.cast_shadow == GeometryInstance3D.SHADOW_CASTING_SETTING_OFF)
- var hole_material := hole.mesh.surface_get_material(0) as StandardMaterial3D
- assert(hole_material != null)
- assert(hole_material.shading_mode == BaseMaterial3D.SHADING_MODE_UNSHADED)
- assert(hole_material.transparency == BaseMaterial3D.TRANSPARENCY_DISABLED)
- assert(is_equal_approx(hole_material.albedo_color.a, 1.0))
- var particles := hotspot.get_node(
- "WaterSpurt/WaterDroplets"
- ) as CPUParticles3D
- assert(particles != null and particles.emitting)
- assert(particles.local_coords)
- assert(particles.global_position.is_equal_approx(
- configured_position + Vector3.UP * 0.035
- ))
- hotspot.set_process(false)
- await create_timer(0.6).timeout
- await process_frame
- assert(hotspot.get_node_or_null("WaterSpurt/WaterDroplets") == null)
- hotspot.queue_free()
-
-
-func _validate_quality_behavior(entry: GatherableData) -> void:
- var prior_speed: float = -1.0
- var prior_scare_radius: float = -1.0
- for quality: int in FishQuality.TIER_COUNT:
- var movement_speed: float = entry.get_movement_speed_for_quality(
- quality
- )
- var scare_radius: float = entry.get_scare_radius_for_quality(quality)
- assert(movement_speed > prior_speed)
- assert(scare_radius > prior_scare_radius)
- prior_speed = movement_speed
- prior_scare_radius = scare_radius
- assert(
- is_equal_approx(
- entry.get_movement_speed_for_quality(FishQuality.Tier.BORING),
- entry.movement_speed,
- )
- )
- assert(
- is_equal_approx(
- entry.get_movement_speed_for_quality(FishQuality.Tier.SHINY),
- entry.movement_speed * 2.0,
- )
- )
- assert(
- is_equal_approx(
- entry.get_scare_radius_for_quality(FishQuality.Tier.SHINY),
- entry.scare_radius * 1.7,
- )
- )
- var service := NetworkWorldSpawnService.new()
- var spawned_catch := service.call(
- "_create_catch_for_state",
- entry,
- {"quality": FishQuality.Tier.EXCEPTIONAL},
- ) as FishCatch
- assert(spawned_catch != null and spawned_catch.is_valid())
- assert(spawned_catch.quality == FishQuality.Tier.EXCEPTIONAL)
- assert(
- spawned_catch.sale_value
- == FishQuality.apply_sale_value(
- entry.catch_data.get_sale_value_for_weight(
- spawned_catch.weight_lb
- ),
- FishQuality.Tier.EXCEPTIONAL,
- )
- )
- spawned_catch = null
- service.free()
-
-
-func _validate_envelopes() -> void:
- var entity: Dictionary = {
- "entity_id": "world:sample",
- "type_id": "future_shellfish",
- "position": [1.0, 2.0, 3.0],
- "yaw": 0.5,
- "revision": 7,
- }
- assert(NetworkWorldSpawnProtocol.validate_entity_state(entity))
- var future_entity: Dictionary = entity.duplicate(true)
- future_entity["future_state"] = {"buried": false}
- assert(NetworkWorldSpawnProtocol.validate_entity_state(future_entity))
-
- var envelope: Dictionary = NetworkWorldSpawnProtocol.make_envelope(
- "session-test",
- 12,
- &"future_spawn_type",
- {"entity": entity, "future_payload": [1, 2, 3]},
- )
- assert(NetworkWorldSpawnProtocol.validate_envelope(envelope))
-
- var malformed_position: Dictionary = entity.duplicate(true)
- malformed_position["position"] = [1.0, "not-a-number", 3.0]
- assert(
- not NetworkWorldSpawnProtocol.validate_entity_state(
- malformed_position
- )
- )
- assert(
- not NetworkWorldSpawnProtocol.array_to_vector3(
- malformed_position["position"]
- ).is_finite()
- )
-
- var invalid_envelope: Dictionary = envelope.duplicate(true)
- invalid_envelope["envelope_version"] = 0
- assert(not NetworkWorldSpawnProtocol.validate_envelope(invalid_envelope))
diff --git a/tests/world_spawn_protocol_validation.gd.uid b/tests/world_spawn_protocol_validation.gd.uid
deleted file mode 100644
index e03c707..0000000
--- a/tests/world_spawn_protocol_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://clu8j64mh4x54
diff --git a/tests/world_time_multiplayer_validation.gd b/tests/world_time_multiplayer_validation.gd
deleted file mode 100644
index 635ad4f..0000000
--- a/tests/world_time_multiplayer_validation.gd
+++ /dev/null
@@ -1,241 +0,0 @@
-extends SceneTree
-
-const MainScene = preload("res://main/main.tscn")
-const TEST_PORT: int = 17983
-const INITIAL_HOST_TIME: float = 19.75
-const UPDATED_HOST_TIME: float = WorldTimeService.DUSK_END_HOUR
-const TIME_TOLERANCE_HOURS: float = 0.05
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- var arguments: PackedStringArray = OS.get_cmdline_user_args()
- if arguments.has("host"):
- await _run_host()
- return
- if arguments.has("client"):
- await _run_client()
- return
- push_error("World time multiplayer validation needs host or client mode.")
- quit(1)
-
-
-func _run_host() -> void:
- var main: Node = await _create_initialized_main()
- var session := main.get_node("%NetworkSession") as NetworkSession
- var world_time := main.get_node("%WorldTimeService") as WorldTimeService
- var world_weather := (
- main.get_node("%WorldWeatherService") as WorldWeatherService
- )
- assert(bool(main.call(
- "_apply_world", WorldLayout.STARTER_ISLAND, 1, true
- )))
- assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1))
- assert(session.start_private_host(TEST_PORT))
- assert(world_time.set_authoritative_time(INITIAL_HOST_TIME))
- assert(world_weather.set_authoritative_weather(
- WorldWeatherService.Weather.RAINY
- ))
- assert(session.set_host_open(true))
-
- var remote_peer_id: int = 0
- var join_deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < join_deadline and remote_peer_id == 0:
- await process_frame
- for peer_id: int in session.get_authenticated_peer_ids():
- if peer_id != session.get_local_peer_id():
- remote_peer_id = peer_id
- break
- assert(remote_peer_id > 1)
- assert(session.peer_supports_capability(
- remote_peer_id, NetworkProtocol.WORLD_TIME_CAPABILITY
- ))
- assert(session.peer_supports_capability(
- remote_peer_id, NetworkProtocol.WORLD_WEATHER_CAPABILITY
- ))
- assert(world_time.get_phase() == WorldTimeService.Phase.DUSK)
- assert(WorldTimeService.is_valid_calendar_date_id(
- world_time.get_calendar_date_id()
- ))
- await create_timer(1.0).timeout
- assert(world_time.set_authoritative_time(UPDATED_HOST_TIME))
- assert(world_weather.set_authoritative_weather(
- WorldWeatherService.Weather.FOGGY
- ))
- var disconnect_deadline: int = Time.get_ticks_msec() + 12000
- while (
- Time.get_ticks_msec() < disconnect_deadline
- and session.is_authenticated_peer(remote_peer_id)
- ):
- await process_frame
- assert(not session.is_authenticated_peer(remote_peer_id))
- print("World time multiplayer host validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _run_client() -> void:
- var main: Node = await _create_initialized_main()
- main.call(
- "_on_title_join_game_requested",
- "127.0.0.1:%d" % TEST_PORT,
- )
- var session := main.get_node("%NetworkSession") as NetworkSession
- var world_time := main.get_node("%WorldTimeService") as WorldTimeService
- var world_weather := (
- main.get_node("%WorldWeatherService") as WorldWeatherService
- )
- var join_deadline: int = Time.get_ticks_msec() + 20000
- while Time.get_ticks_msec() < join_deadline:
- await process_frame
- if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
- main.call("_confirm_server_trust")
- if session.is_joined_client() and bool(main.get("_gameplay_started")):
- break
- assert(session.is_joined_client())
- assert(session.supports_server_capability(
- NetworkProtocol.WORLD_TIME_CAPABILITY
- ))
- assert(session.supports_server_capability(
- NetworkProtocol.WORLD_WEATHER_CAPABILITY
- ))
- var initial_deadline: int = Time.get_ticks_msec() + 8000
- while (
- Time.get_ticks_msec() < initial_deadline
- and _wrapped_time_difference(
- world_time.get_time_hours(), INITIAL_HOST_TIME
- ) > TIME_TOLERANCE_HOURS
- ):
- await process_frame
- assert(_wrapped_time_difference(
- world_time.get_time_hours(), INITIAL_HOST_TIME
- ) <= TIME_TOLERANCE_HOURS)
- assert(world_time.get_phase() == WorldTimeService.Phase.DUSK)
- var expected_date_id: String = WorldTimeService.date_id_from_datetime(
- Time.get_datetime_dict_from_system(false)
- )
- assert(world_time.get_calendar_date_id() == expected_date_id)
- var weather_deadline: int = Time.get_ticks_msec() + 8000
- while (
- Time.get_ticks_msec() < weather_deadline
- and not world_weather.is_raining()
- ):
- await process_frame
- assert(world_weather.is_raining())
- var game_ui := main.get_node("%GameUI") as CanvasLayer
- var chat_ui := game_ui.get_node("%ChatUI") as Control
- var clock_panel := chat_ui.get_node("WorldClockPanel") as PanelContainer
- var clock_label := clock_panel.get_node("WorldClockLabel") as Label
- var calendar_panel := (
- chat_ui.get_node("WorldCalendarPanel") as PanelContainer
- )
- var calendar_row := (
- calendar_panel.get_node("WorldCalendarRow") as HBoxContainer
- )
- var calendar_label := (
- calendar_row.get_node("WorldCalendarLabel") as Label
- )
- var calendar_balance := (
- calendar_row.get_node("WorldCalendarBalance") as CurrencyAmount
- )
- var weather_icon := chat_ui.get_node("WorldWeatherIcon") as WeatherIcon
- var chat_panel := chat_ui.get_node("ChatPanel") as PanelContainer
- assert(clock_panel.visible)
- assert(clock_label.text == world_time.get_clock_text())
- assert(clock_label.text.ends_with(" pm"))
- assert(calendar_panel.visible)
- assert(calendar_label.text == world_time.get_calendar_text())
- assert(not calendar_label.text.is_empty())
- assert(
- calendar_label.horizontal_alignment
- == HORIZONTAL_ALIGNMENT_LEFT
- )
- assert(calendar_label.size_flags_horizontal == Control.SIZE_EXPAND_FILL)
- var player := chat_ui.get("_player") as Player
- assert(calendar_balance.amount == player.wallet.get_balance())
- assert(player.wallet.credit(1))
- assert(calendar_balance.amount == player.wallet.get_balance())
- assert(weather_icon.visible)
- assert(weather_icon.get_weather() == WorldWeatherService.Weather.RAINY)
- assert(is_equal_approx(clock_panel.position.y, 10.0))
- assert(is_equal_approx(weather_icon.position.y, 10.0))
- assert(is_equal_approx(
- calendar_panel.position.y,
- clock_panel.position.y
- + clock_panel.size.y
- + ChatUI.CALENDAR_TOP_GAP,
- ))
- assert(calendar_panel.size.is_equal_approx(ChatUI.CALENDAR_SIZE))
- assert(clock_panel.position.y + clock_panel.size.y < chat_panel.position.y)
- assert(not chat_ui.has_method("_handle_chat_command"))
-
- var update_deadline: int = Time.get_ticks_msec() + 10000
- while (
- Time.get_ticks_msec() < update_deadline
- and _wrapped_time_difference(
- world_time.get_time_hours(), UPDATED_HOST_TIME
- ) > TIME_TOLERANCE_HOURS
- ):
- await process_frame
- assert(_wrapped_time_difference(
- world_time.get_time_hours(), UPDATED_HOST_TIME
- ) <= TIME_TOLERANCE_HOURS)
- assert(world_time.get_phase() == WorldTimeService.Phase.NIGHT)
- assert(clock_label.text == world_time.get_clock_text())
- var fog_deadline: int = Time.get_ticks_msec() + 8000
- while (
- Time.get_ticks_msec() < fog_deadline
- and not world_weather.is_foggy()
- ):
- await process_frame
- assert(world_weather.is_foggy())
- assert(weather_icon.get_weather() == WorldWeatherService.Weather.FOGGY)
- chat_ui.call("set_dock_right", true)
- await process_frame
- assert(clock_panel.position.x > 1000.0)
- assert(weather_icon.position.x < clock_panel.position.x)
- assert(is_equal_approx(
- calendar_panel.position.x, weather_icon.position.x
- ))
- assert(is_equal_approx(clock_panel.position.y, 10.0))
- chat_ui.call("set_dock_right", false)
- await process_frame
- assert(clock_panel.position.x < 20.0)
- assert(weather_icon.position.x > clock_panel.position.x)
- assert(is_equal_approx(
- calendar_panel.position.x, clock_panel.position.x
- ))
- assert(is_equal_approx(clock_panel.position.y, 10.0))
- print("World time multiplayer client validation: PASS")
- session.disconnect_session("")
- main.queue_free()
- for _frame: int in 4:
- await process_frame
- await create_timer(0.1).timeout
- quit()
-
-
-func _create_initialized_main() -> Node:
- root.size = Vector2i(1280, 720)
- var main := MainScene.instantiate()
- root.add_child(main)
- for _frame: int in 4:
- await process_frame
- if not bool(main.get("_application_initialized")):
- main.call("_activate_selected_data_path", "", true)
- for _frame: int in 8:
- await process_frame
- assert(bool(main.get("_application_initialized")))
- return main
-
-
-func _wrapped_time_difference(left: float, right: float) -> float:
- var direct: float = absf(left - right)
- return minf(direct, WorldTimeService.HOURS_PER_DAY - direct)
diff --git a/tests/world_time_multiplayer_validation.gd.uid b/tests/world_time_multiplayer_validation.gd.uid
deleted file mode 100644
index 3e17cd7..0000000
--- a/tests/world_time_multiplayer_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://6fal0btp6sgr
diff --git a/tests/world_time_validation.gd b/tests/world_time_validation.gd
deleted file mode 100644
index 3caea2b..0000000
--- a/tests/world_time_validation.gd
+++ /dev/null
@@ -1,573 +0,0 @@
-extends SceneTree
-
-const WorldTimeServiceType = preload("res://world/world_time_service.gd")
-const WorldTimeVisualControllerType = preload(
- "res://world/world_time_visual_controller.gd"
-)
-const NetworkWorldTimeServiceType = preload(
- "res://network/network_world_time_service.gd"
-)
-const FishingContextType = preload("res://fishing/fishing_context.gd")
-const FishDataType = preload("res://fish/fish_data.gd")
-const FishPoolType = preload("res://fish/fish_pool.gd")
-const FishingSpotType = preload("res://fishing/fishing_spot.gd")
-const FishableWaterRegionType = preload(
- "res://world/fishable_water_region.gd"
-)
-const WeatherIconType = preload("res://ui/weather_icon.gd")
-const CalendarSeasonType = preload("res://world/calendar_season.gd")
-
-const Catalog: FishPoolType = preload("res://fish/pools/fish_catalog.tres")
-const SaltWaterMaterial: ShaderMaterial = preload(
- "res://world/materials/stylized_water.tres"
-)
-const FreshWaterMaterial: ShaderMaterial = preload(
- "res://world/materials/stylized_water_fresh.tres"
-)
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- _validate_clock_boundaries_and_duration()
- _validate_system_clock_authority()
- _validate_persistent_host_clock()
- _validate_fishing_availability()
- _validate_fishing_spot_context()
- _validate_network_snapshot_bounds()
- _validate_environment_presentation()
- _validate_weather_clock_icon()
- print("World time validation: PASS")
- quit()
-
-
-func _validate_clock_boundaries_and_duration() -> void:
- assert(WorldTimeServiceType.REAL_SECONDS_PER_CYCLE == 86400.0)
- assert(is_equal_approx(
- WorldTimeServiceType.HOURS_PER_REAL_SECOND,
- 1.0 / 3600.0,
- ))
- assert(is_equal_approx(
- WorldTimeServiceType.time_hours_from_datetime({
- "hour": 17, "minute": 30, "second": 18,
- }),
- 17.505,
- ))
- assert(
- WorldTimeServiceType.date_id_from_datetime({
- "year": 2026, "month": 8, "day": 18,
- }) == "2026-08-18"
- )
- assert(WorldTimeServiceType.is_valid_calendar_date_id("2026-08-18"))
- assert(WorldTimeServiceType.is_valid_calendar_date_id("2028-02-29"))
- assert(not WorldTimeServiceType.is_valid_calendar_date_id("2026-02-29"))
- assert(not WorldTimeServiceType.is_valid_calendar_date_id("2026-13-01"))
- assert(not WorldTimeServiceType.is_valid_calendar_date_id("2026-8-18"))
- assert(
- WorldTimeServiceType.format_calendar_date("2026-08-18")
- == "tuesday, august 18"
- )
- assert(
- CalendarSeasonType.from_date_id("2026-03-01")
- == CalendarSeasonType.Season.SPRING
- )
- assert(
- CalendarSeasonType.from_date_id("2026-06-01")
- == CalendarSeasonType.Season.SUMMER
- )
- assert(
- CalendarSeasonType.from_date_id("2026-09-01")
- == CalendarSeasonType.Season.FALL
- )
- assert(
- CalendarSeasonType.from_date_id("2026-12-01")
- == CalendarSeasonType.Season.WINTER
- )
- assert(
- CalendarSeasonType.from_date_id("invalid")
- == CalendarSeasonType.UNKNOWN
- )
- assert(
- WorldTimeServiceType.calendar_cycle_id_from_datetime({
- "year": 2026, "month": 8, "day": 18,
- "hour": 7, "minute": 59, "second": 0,
- }, WorldTimeServiceType.DAY_START_HOUR) == "2026-08-17"
- )
- assert(
- WorldTimeServiceType.calendar_cycle_id_from_datetime({
- "year": 2026, "month": 8, "day": 18,
- "hour": 8, "minute": 0, "second": 0,
- }, WorldTimeServiceType.DAY_START_HOUR) == "2026-08-18"
- )
- assert(
- WorldTimeServiceType.phase_for_hour(7.49)
- == WorldTimeServiceType.Phase.NIGHT
- )
- assert(
- WorldTimeServiceType.phase_for_hour(7.5)
- == WorldTimeServiceType.Phase.DAWN
- )
- assert(
- WorldTimeServiceType.phase_for_hour(8.49)
- == WorldTimeServiceType.Phase.DAWN
- )
- assert(
- WorldTimeServiceType.phase_for_hour(8.5)
- == WorldTimeServiceType.Phase.DAY
- )
- assert(
- WorldTimeServiceType.phase_for_hour(19.5)
- == WorldTimeServiceType.Phase.DUSK
- )
- assert(
- WorldTimeServiceType.phase_for_hour(20.5)
- == WorldTimeServiceType.Phase.NIGHT
- )
- assert(WorldTimeServiceType.format_clock_time(0.0) == "12:00 am")
- assert(WorldTimeServiceType.format_clock_time(8.0) == "8:00 am")
- assert(WorldTimeServiceType.format_clock_time(20.5) == "8:30 pm")
-
- var clock := WorldTimeServiceType.new()
- root.add_child(clock)
- clock.begin_test_session(8.0, "2026-08-18")
- assert(clock.get_calendar_date_id() == "2026-08-18")
- assert(clock.get_calendar_text() == "tuesday, august 18")
- assert(clock.get_season() == CalendarSeasonType.Season.SUMMER)
- assert(clock.get_season_text() == "summer")
- clock.advance_time(12.0 * 60.0 * 60.0)
- assert(is_equal_approx(clock.get_time_hours(), 20.0))
- assert(clock.is_night_period())
- assert(clock.is_transition())
- clock.advance_time(12.0 * 60.0 * 60.0)
- assert(is_equal_approx(clock.get_time_hours(), 8.0))
- assert(not clock.is_night_period())
- assert(clock.is_transition())
-
- # Small high-refresh deltas must still advance the displayed minute. Using
- # is_equal_approx() per frame used to suppress every clock notification.
- var emitted_times: Array[float] = []
- clock.time_changed.connect(
- func(time_hours: float, _phase: WorldTimeService.Phase) -> void:
- emitted_times.append(time_hours)
- )
- clock.begin_test_session(12.0 + 31.0 / 60.0)
- emitted_times.clear()
- for _frame: int in 7201:
- clock.advance_time(1.0 / 60.0)
- assert(clock.get_clock_text() == "12:33 pm")
- assert(emitted_times.size() >= 2)
- clock.queue_free()
-
-
-func _validate_persistent_host_clock() -> void:
- var clock := WorldTimeServiceType.new()
- root.add_child(clock)
- assert(clock.restore_persistent_time_hours(18.75))
- clock.begin_test_session(18.75)
- assert(is_equal_approx(clock.get_time_hours(), 18.75))
- clock.synchronize_time(19.25)
- assert(is_equal_approx(clock.get_persistent_time_hours(), 19.25))
- clock.synchronize_time(6.5)
- assert(is_equal_approx(clock.get_time_hours(), 6.5))
- assert(is_equal_approx(clock.get_persistent_time_hours(), 6.5))
- assert(not clock.restore_persistent_time_hours(-1.0))
- assert(not clock.restore_persistent_time_hours(24.0))
- clock.queue_free()
-
-
-func _validate_system_clock_authority() -> void:
- var clock := WorldTimeServiceType.new()
- root.add_child(clock)
- clock.begin_authoritative_session()
- var system_datetime: Dictionary = Time.get_datetime_dict_from_system(false)
- var system_hour: float = WorldTimeServiceType.time_hours_from_datetime(
- system_datetime
- )
- assert(clock.is_using_system_clock())
- assert(_wrapped_time_difference(
- clock.get_time_hours(), system_hour
- ) < 2.0 / 3600.0)
- assert(
- clock.get_calendar_date_id()
- == WorldTimeServiceType.date_id_from_datetime(system_datetime)
- )
- assert(clock.set_authoritative_time(12.0))
- assert(not clock.is_using_system_clock())
- clock.clear_editor_time_override()
- assert(clock.is_using_system_clock())
- clock.queue_free()
-
-
-func _validate_fishing_availability() -> void:
- var day_context := FishingContextType.new()
- # Cover both freshwater habitat families so this test isolates time-of-day
- # gating from the separate pond-versus-river availability rules.
- day_context.location_tags = [&"starter_pond", &"river"]
- day_context.is_night = false
- var night_context := FishingContextType.new()
- night_context.location_tags = [&"starter_pond", &"river"]
- night_context.is_night = true
- var transition_context := FishingContextType.new()
- transition_context.location_tags = [&"starter_pond", &"river"]
- transition_context.is_night = true
- transition_context.is_day_night_transition = true
-
- for fish_id: StringName in [&"bluegill", &"sunfish"]:
- var day_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
- assert(day_fish.availability.is_available(day_context))
- assert(not day_fish.availability.is_available(night_context))
- assert(day_fish.availability.is_available(transition_context))
- for fish_id: StringName in [
- &"catfish_blue",
- &"catfish_channel",
- &"catfish_flathead",
- &"catfish_white",
- ]:
- var night_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
- assert(not night_fish.availability.is_available(day_context))
- assert(night_fish.availability.is_available(night_context))
- assert(night_fish.availability.is_available(transition_context))
- for fish_id: StringName in [
- &"bass",
- &"carp",
- &"tuna_albacore",
- &"tuna_bigeye",
- &"tuna_bluefin",
- &"tuna_skipjack",
- &"tuna_yellowfin",
- &"goby_round",
- &"salmon_atlantic",
- &"salmon_chum",
- &"salmon_coho",
- &"salmon_pink",
- &"salmon_sockeye",
- ]:
- var all_time_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
- assert(all_time_fish.availability.is_available(day_context))
- assert(all_time_fish.availability.is_available(night_context))
- assert(all_time_fish.availability.is_available(transition_context))
-
-
-func _validate_fishing_spot_context() -> void:
- var clock := WorldTimeServiceType.new()
- clock.begin_test_session(20.25, "2026-12-18")
- var fishing_spot := FishingSpotType.new()
- fishing_spot.set("_world_time", clock)
- var region := FishableWaterRegionType.new()
- region.location_tags = [&"starter_pond"]
- region.water_type = WaterType.Type.FRESH_WATER
- var dusk_context: FishingContext = fishing_spot.build_network_context(region)
- assert(dusk_context.is_night)
- assert(dusk_context.is_day_night_transition)
- assert(dusk_context.season == CalendarSeasonType.Season.WINTER)
- clock.synchronize_time(14.0)
- var day_context: FishingContext = fishing_spot.build_network_context(region)
- assert(not day_context.is_night)
- assert(not day_context.is_day_night_transition)
- region.free()
- fishing_spot.free()
- clock.free()
-
-
-func _validate_network_snapshot_bounds() -> void:
- assert(NetworkWorldTimeServiceType.validate_snapshot({
- "session_id": "session",
- "time_hours": 8.25,
- "calendar_date_id": "2026-08-18",
- "sequence": 1,
- }))
- assert(not NetworkWorldTimeServiceType.validate_snapshot({
- "session_id": "session",
- "time_hours": 24.0,
- "calendar_date_id": "2026-08-18",
- "sequence": 1,
- }))
- assert(not NetworkWorldTimeServiceType.validate_snapshot({
- "session_id": "",
- "time_hours": 8.0,
- "calendar_date_id": "2026-08-18",
- "sequence": 1,
- }))
- assert(not NetworkWorldTimeServiceType.validate_snapshot({
- "session_id": "session",
- "time_hours": 8.0,
- "calendar_date_id": "2026-02-29",
- "sequence": 1,
- }))
-
-
-func _validate_environment_presentation() -> void:
- var world_root := Node3D.new()
- root.add_child(world_root)
- var world_environment := WorldEnvironment.new()
- var environment := Environment.new()
- var sky := Sky.new()
- var sky_material := ShaderMaterial.new()
- sky_material.shader = preload(
- "res://world/environment/straywild_sky.gdshader"
- )
- sky.sky_material = sky_material
- environment.sky = sky
- environment.adjustment_enabled = true
- environment.fog_enabled = true
- world_environment.environment = environment
- world_root.add_child(world_environment)
- var sun := DirectionalLight3D.new()
- world_root.add_child(sun)
- var clock := WorldTimeServiceType.new()
- world_root.add_child(clock)
- var visuals := WorldTimeVisualControllerType.new()
- world_root.add_child(visuals)
- visuals.setup(clock, world_environment, sun)
- visuals.apply_time_immediately(12.0)
- var runtime_environment: Environment = world_environment.environment
- var runtime_sky_material := (
- runtime_environment.sky.sky_material as ShaderMaterial
- )
- var day_horizon_value: Variant = runtime_sky_material.get_shader_parameter(
- "sky_horizon_color"
- )
- assert(day_horizon_value is Color)
- var day_horizon: Color = day_horizon_value as Color
- var day_sun_visibility: float = float(
- runtime_sky_material.get_shader_parameter("sun_visibility")
- )
- var day_sun_direction: Vector3 = (
- runtime_sky_material.get_shader_parameter("sun_direction") as Vector3
- )
- assert(day_sun_visibility > 0.99)
- assert(day_sun_direction.y > 0.85)
- assert(
- float(runtime_sky_material.get_shader_parameter("moon_visibility"))
- < 0.01
- )
- assert(
- float(runtime_sky_material.get_shader_parameter("star_visibility"))
- < 0.01
- )
- var day_ambient_energy: float = runtime_environment.ambient_light_energy
- assert(not runtime_environment.fog_enabled)
- assert(is_equal_approx(runtime_environment.fog_sky_affect, 0.35))
- var water_shader: Shader = preload(
- "res://world/materials/stylized_water.gdshader"
- )
- var sky_shader: Shader = preload(
- "res://world/environment/straywild_sky.gdshader"
- )
- assert("uniform float fog_horizon_occlusion" in sky_shader.code)
- assert("fog_horizon_color.rgb" in sky_shader.code)
- assert("uniform float star_visibility" in sky_shader.code)
- assert("float procedural_star_field" in sky_shader.code)
- assert("upper_sky_fade" in sky_shader.code)
- assert("fog_disabled" in water_shader.code)
- assert("surface_view_position = view_vertex.xyz" in water_shader.code)
- assert("surface_view_distance = length(surface_view_position)" in water_shader.code)
- assert(
- "max(weather_fog_end, weather_fog_begin + 1.0),\n"
- + "\t\tsurface_view_distance"
- in water_shader.code
- )
- assert("weather_fog_near_amount" in water_shader.code)
- assert("float weather_fog_alpha = mix(" in water_shader.code)
- assert("ALPHA = clamp(water_alpha, 0.1, weather_fog_alpha)" in water_shader.code)
- assert(
- "water_alpha *= mix(0.82, 1.0, contact_stability);"
- in water_shader.code
- )
- assert(is_zero_approx(float(
- SaltWaterMaterial.get_shader_parameter("weather_fog_amount")
- )))
- assert(is_equal_approx(float(
- SaltWaterMaterial.get_shader_parameter("surface_highlight_night_floor")
- ), WorldTimeVisualController.DEFAULT_SURFACE_HIGHLIGHT_FLOOR))
- var day_water_brightness := float(
- SaltWaterMaterial.get_shader_parameter("environment_brightness")
- )
- assert(day_water_brightness > 0.99)
- assert(is_equal_approx(
- float(FreshWaterMaterial.get_shader_parameter("environment_brightness")),
- day_water_brightness,
- ))
- visuals.apply_time_immediately(0.0)
- var night_horizon: Color = runtime_sky_material.get_shader_parameter(
- "sky_horizon_color"
- ) as Color
- assert(night_horizon != day_horizon)
- assert(
- float(runtime_sky_material.get_shader_parameter("sun_visibility"))
- < 0.01
- )
- var night_moon_visibility: float = float(
- runtime_sky_material.get_shader_parameter("moon_visibility")
- )
- var night_moon_direction: Vector3 = (
- runtime_sky_material.get_shader_parameter("moon_direction") as Vector3
- )
- var night_sun_direction: Vector3 = (
- runtime_sky_material.get_shader_parameter("sun_direction") as Vector3
- )
- assert(night_moon_visibility > 0.99)
- assert(
- float(runtime_sky_material.get_shader_parameter("star_visibility"))
- > 0.99
- )
- assert((
- runtime_sky_material.get_shader_parameter("star_color") as Color
- ).is_equal_approx(WorldTimeVisualController.STAR_COLOR))
- assert(is_equal_approx(
- float(runtime_sky_material.get_shader_parameter("star_strength")),
- WorldTimeVisualController.STAR_STRENGTH,
- ))
- assert(night_moon_direction.y > 0.85)
- assert(night_moon_direction.is_equal_approx(-night_sun_direction))
- assert(runtime_environment.ambient_light_energy < day_ambient_energy)
- assert(is_zero_approx(float(
- runtime_sky_material.get_shader_parameter("fog_horizon_occlusion")
- )))
- var night_water_brightness := float(
- SaltWaterMaterial.get_shader_parameter("environment_brightness")
- )
- assert(night_water_brightness < day_water_brightness * 0.4)
- assert(is_equal_approx(
- float(FreshWaterMaterial.get_shader_parameter("environment_brightness")),
- night_water_brightness,
- ))
- var night_water_tint := (
- SaltWaterMaterial.get_shader_parameter("environment_tint") as Color
- )
- assert(night_water_tint.get_luminance() < 0.5)
- var night_background_energy := runtime_environment.background_energy_multiplier
- var night_ambient_energy := runtime_environment.ambient_light_energy
- var night_fog_light_energy := runtime_environment.fog_light_energy
- var night_adjustment_brightness := runtime_environment.adjustment_brightness
- var night_adjustment_saturation := runtime_environment.adjustment_saturation
- visuals.apply_weather_immediately(WorldWeatherService.Weather.FOGGY)
- visuals.apply_time_immediately(0.0)
- assert(runtime_environment.fog_enabled)
- assert(float(
- SaltWaterMaterial.get_shader_parameter("weather_fog_amount")
- ) > 0.99)
- assert(is_equal_approx(float(
- SaltWaterMaterial.get_shader_parameter("weather_fog_near_amount")
- ), WorldTimeVisualController.FOGGY_NIGHT_WATER_NEAR_FOG_AMOUNT))
- assert(is_equal_approx(float(
- SaltWaterMaterial.get_shader_parameter("surface_highlight_night_floor")
- ), WorldTimeVisualController.FOGGY_NIGHT_SURFACE_HIGHLIGHT_FLOOR))
- assert(is_equal_approx(float(
- runtime_sky_material.get_shader_parameter("fog_horizon_occlusion")
- ), 1.0))
- assert(is_zero_approx(float(
- runtime_sky_material.get_shader_parameter("star_visibility")
- )))
- assert((
- runtime_sky_material.get_shader_parameter("fog_horizon_color") as Color
- ).is_equal_approx(WorldTimeVisualController.NIGHT_FOG))
- var night_rendered_fog_color := (
- runtime_environment.fog_light_color.srgb_to_linear()
- )
- night_rendered_fog_color.r *= runtime_environment.fog_light_energy
- night_rendered_fog_color.g *= runtime_environment.fog_light_energy
- night_rendered_fog_color.b *= runtime_environment.fog_light_energy
- night_rendered_fog_color.a = 1.0
- night_rendered_fog_color = night_rendered_fog_color.linear_to_srgb()
- var night_water_fog_color := (
- SaltWaterMaterial.get_shader_parameter("weather_fog_color") as Color
- )
- var corrected_night_fog_color := night_rendered_fog_color
- corrected_night_fog_color.r *= (
- WorldTimeVisualController.FOGGY_DARK_WATER_FOG_CORRECTION.r
- )
- corrected_night_fog_color.g *= (
- WorldTimeVisualController.FOGGY_DARK_WATER_FOG_CORRECTION.g
- )
- corrected_night_fog_color.b *= (
- WorldTimeVisualController.FOGGY_DARK_WATER_FOG_CORRECTION.b
- )
- assert(night_water_fog_color.is_equal_approx(corrected_night_fog_color))
- var foggy_sky_horizon := (
- runtime_sky_material.get_shader_parameter("sky_horizon_color") as Color
- )
- assert(foggy_sky_horizon.is_equal_approx(night_horizon))
- assert(is_equal_approx(runtime_environment.fog_sky_affect, 1.0))
- assert(is_zero_approx(runtime_environment.fog_aerial_perspective))
- assert(is_equal_approx(runtime_environment.fog_depth_begin, 3.0))
- assert(is_equal_approx(runtime_environment.fog_depth_end, 18.0))
- assert(is_equal_approx(
- float(SaltWaterMaterial.get_shader_parameter("environment_brightness")),
- night_water_brightness,
- ))
- assert(is_equal_approx(
- runtime_environment.background_energy_multiplier,
- night_background_energy,
- ))
- assert(is_equal_approx(
- runtime_environment.ambient_light_energy,
- night_ambient_energy,
- ))
- assert(is_equal_approx(
- runtime_environment.fog_light_energy,
- night_fog_light_energy,
- ))
- assert(is_equal_approx(
- runtime_environment.adjustment_brightness,
- night_adjustment_brightness,
- ))
- assert(is_equal_approx(
- runtime_environment.adjustment_saturation,
- night_adjustment_saturation,
- ))
- visuals.apply_time_immediately(8.0)
- assert(is_equal_approx(float(
- SaltWaterMaterial.get_shader_parameter("weather_fog_near_amount")
- ), WorldTimeVisualController.FOGGY_NIGHT_WATER_NEAR_FOG_AMOUNT))
- assert(is_equal_approx(float(
- runtime_sky_material.get_shader_parameter("fog_horizon_occlusion")
- ), 1.0))
- visuals.apply_time_immediately(20.0)
- assert(is_zero_approx(float(
- SaltWaterMaterial.get_shader_parameter("weather_fog_near_amount")
- )))
- assert(is_zero_approx(float(
- runtime_sky_material.get_shader_parameter("fog_horizon_occlusion")
- )))
- visuals.apply_weather_immediately(WorldWeatherService.Weather.SUNNY)
- visuals.apply_time_immediately(20.0)
- assert(not runtime_environment.fog_enabled)
- assert(is_zero_approx(float(
- SaltWaterMaterial.get_shader_parameter("weather_fog_amount")
- )))
- assert(is_zero_approx(float(
- SaltWaterMaterial.get_shader_parameter("weather_fog_near_amount")
- )))
- assert(is_equal_approx(float(
- SaltWaterMaterial.get_shader_parameter("surface_highlight_night_floor")
- ), WorldTimeVisualController.DEFAULT_SURFACE_HIGHLIGHT_FLOOR))
- assert(is_zero_approx(float(
- runtime_sky_material.get_shader_parameter("fog_horizon_occlusion")
- )))
- var dusk_star_visibility := float(
- runtime_sky_material.get_shader_parameter("star_visibility")
- )
- assert(dusk_star_visibility > 0.45 and dusk_star_visibility < 0.55)
- var dusk_horizon: Color = runtime_sky_material.get_shader_parameter(
- "sky_horizon_color"
- ) as Color
- assert(dusk_horizon.r > day_horizon.r)
- world_root.queue_free()
-
-
-func _validate_weather_clock_icon() -> void:
- var weather_icon := WeatherIconType.new()
- root.add_child(weather_icon)
- assert(weather_icon.tooltip_text == "clear")
- assert(not weather_icon.is_nighttime())
- weather_icon.set_nighttime(true)
- assert(weather_icon.is_nighttime())
- weather_icon.set_weather(WorldWeatherService.Weather.CLOUDY)
- assert(weather_icon.tooltip_text == "cloudy")
- weather_icon.queue_free()
-
-
-static func _wrapped_time_difference(left: float, right: float) -> float:
- var difference: float = absf(left - right)
- return minf(difference, WorldTimeServiceType.HOURS_PER_DAY - difference)
diff --git a/tests/world_time_validation.gd.uid b/tests/world_time_validation.gd.uid
deleted file mode 100644
index b84e544..0000000
--- a/tests/world_time_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://buiitafgrgm4b
diff --git a/tests/world_weather_validation.gd b/tests/world_weather_validation.gd
deleted file mode 100644
index 7bc5114..0000000
--- a/tests/world_weather_validation.gd
+++ /dev/null
@@ -1,641 +0,0 @@
-extends SceneTree
-
-const WorldWeatherServiceType = preload(
- "res://world/world_weather_service.gd"
-)
-const NetworkWorldWeatherServiceType = preload(
- "res://network/network_world_weather_service.gd"
-)
-const WorldTimeServiceType = preload("res://world/world_time_service.gd")
-const WorldTimeVisualControllerType = preload(
- "res://world/world_time_visual_controller.gd"
-)
-const PrecipitationOcclusionType = preload(
- "res://world/environment/precipitation_occlusion.gd"
-)
-const FishingContextType = preload("res://fishing/fishing_context.gd")
-const FishingSpotType = preload("res://fishing/fishing_spot.gd")
-const FishAvailabilityType = preload("res://fish/fish_availability.gd")
-const FishableWaterRegionType = preload(
- "res://world/fishable_water_region.gd"
-)
-
-
-func _initialize() -> void:
- call_deferred("_run")
-
-
-func _run() -> void:
- _validate_weather_scheduler()
- _validate_authoritative_weather_override()
- _validate_weather_persistence()
- _validate_legacy_forecast_save_migration()
- _validate_snapshot_bounds()
- _validate_fishing_weather_seams()
- _validate_fishing_spot_context()
- _validate_weather_presentation()
- print("World weather validation: PASS")
- quit()
-
-
-func _validate_weather_scheduler() -> void:
- var weather := WorldWeatherServiceType.new()
- root.add_child(weather)
- weather.begin_authoritative_session(20260802)
- assert(weather.get_weather() == WorldWeatherServiceType.Weather.SUNNY)
- assert(_duration_is_valid(weather))
- weather.advance_weather(weather.get_seconds_remaining() + 0.01)
- assert(weather.get_weather() == WorldWeatherServiceType.Weather.CLOUDY)
- assert(_duration_is_valid(weather))
- weather.advance_weather(weather.get_seconds_remaining() + 0.01)
- assert(weather.get_weather() in [
- WorldWeatherServiceType.Weather.SUNNY,
- WorldWeatherServiceType.Weather.RAINY,
- WorldWeatherServiceType.Weather.FOGGY,
- ])
- assert(_duration_is_valid(weather))
- weather.apply_authoritative_snapshot(
- WorldWeatherServiceType.Weather.RAINY, 120.0
- )
- assert(weather.is_raining())
- assert(not weather.is_foggy())
- weather.apply_authoritative_snapshot(
- WorldWeatherServiceType.Weather.FOGGY, 120.0
- )
- assert(weather.is_foggy())
- assert(not weather.is_raining())
- weather.queue_free()
-
-
-func _duration_is_valid(weather: WorldWeatherServiceType) -> bool:
- var seconds: float = weather.get_seconds_remaining()
- return is_equal_approx(
- seconds, WorldWeatherServiceType.WEATHER_PERIOD_SECONDS
- )
-
-
-func _validate_authoritative_weather_override() -> void:
- var weather := WorldWeatherServiceType.new()
- root.add_child(weather)
- assert(not weather.set_authoritative_weather(
- WorldWeatherServiceType.Weather.FOGGY
- ))
- weather.begin_authoritative_session(20260812)
- assert(weather.set_authoritative_weather(
- WorldWeatherServiceType.Weather.FOGGY
- ))
- assert(weather.is_foggy())
- assert(_duration_is_valid(weather))
- weather.end_session()
- assert(not weather.set_authoritative_weather(
- WorldWeatherServiceType.Weather.RAINY
- ))
-
- var clock := WorldTimeServiceType.new()
- root.add_child(clock)
- clock.begin_test_session(WorldTimeServiceType.DAY_START_HOUR)
- var schedule: Array[Dictionary] = []
- for index: int in WorldWeatherServiceType.DAILY_PLAN_SEGMENT_COUNT:
- schedule.append({
- "start_hour": fposmod(
- WorldTimeServiceType.DAY_START_HOUR
- + float(index)
- * WorldWeatherServiceType.DAILY_PLAN_SEGMENT_HOURS,
- WorldTimeServiceType.HOURS_PER_DAY,
- ),
- "weather": int(WorldWeatherServiceType.Weather.SUNNY),
- })
- assert(weather.configure_daily_plan("command-test", schedule, clock))
- weather.begin_authoritative_session(20260812)
- assert(weather.set_authoritative_weather(
- WorldWeatherServiceType.Weather.RAINY
- ))
- weather.advance_weather(1.0)
- assert(weather.is_raining())
- clock.synchronize_time(
- WorldTimeServiceType.DAY_START_HOUR
- + WorldWeatherServiceType.DAILY_PLAN_SEGMENT_HOURS
- )
- weather.advance_weather(0.01)
- assert(weather.get_weather() == WorldWeatherServiceType.Weather.SUNNY)
- weather.queue_free()
- clock.queue_free()
-
-
-func _validate_weather_persistence() -> void:
- var weather := WorldWeatherServiceType.new()
- root.add_child(weather)
- weather.set_persistence_tracking_enabled(true)
- weather.begin_authoritative_session(20260802)
- weather.apply_authoritative_snapshot(
- WorldWeatherServiceType.Weather.RAINY,
- 200.0,
- )
- weather.advance_weather(12.5)
- assert(weather.has_persistent_state())
- assert(
- weather.get_persistent_weather()
- == WorldWeatherServiceType.Weather.RAINY
- )
- assert(is_equal_approx(
- weather.get_persistent_seconds_remaining(),
- 187.5,
- ))
- weather.set_persistence_tracking_enabled(false)
- weather.begin_remote_session()
- weather.apply_authoritative_snapshot(
- WorldWeatherServiceType.Weather.FOGGY,
- 45.0,
- )
- assert(weather.is_foggy())
- assert(
- weather.get_persistent_weather()
- == WorldWeatherServiceType.Weather.RAINY
- )
- assert(is_equal_approx(
- weather.get_persistent_seconds_remaining(),
- 187.5,
- ))
- weather.set_persistence_tracking_enabled(true)
- weather.begin_authoritative_session(20260803)
- assert(weather.get_weather() == WorldWeatherServiceType.DEFAULT_WEATHER)
- assert(is_equal_approx(
- weather.get_seconds_remaining(),
- WorldWeatherServiceType.WEATHER_PERIOD_SECONDS,
- ))
- assert(not weather.restore_persistent_state(
- WorldWeatherServiceType.Weather.RAINY,
- WorldWeatherServiceType.MAX_PERSISTED_SECONDS + 1.0,
- ))
- weather.queue_free()
-
-
-func _validate_snapshot_bounds() -> void:
- assert(NetworkWorldWeatherServiceType.validate_snapshot({
- "session_id": "session",
- "weather": int(WorldWeatherServiceType.Weather.FOGGY),
- "seconds_remaining": 300.0,
- "sequence": 2,
- }))
- assert(not NetworkWorldWeatherServiceType.validate_snapshot({
- "session_id": "session",
- "weather": 99,
- "seconds_remaining": 300.0,
- "sequence": 2,
- }))
- assert(not NetworkWorldWeatherServiceType.validate_snapshot({
- "session_id": "session",
- "weather": int(WorldWeatherServiceType.Weather.RAINY),
- "seconds_remaining": 3601.0,
- "sequence": 2,
- }))
-
-
-func _validate_legacy_forecast_save_migration() -> void:
- var legacy_schedule: Array[Dictionary] = []
- for index: int in JobCatalog.LEGACY_WEATHER_SEGMENT_COUNT:
- legacy_schedule.append({
- "start_hour": fposmod(
- WorldTimeService.DAY_START_HOUR
- + float(index) * JobCatalog.LEGACY_WEATHER_SEGMENT_HOURS,
- WorldTimeService.HOURS_PER_DAY,
- ),
- "weather": int(WorldWeatherService.Weather.SUNNY),
- })
- var legacy_job: Dictionary = {
- "id": "legacy-catch",
- "title": "legacy catch",
- "description": "catch one fish",
- "kind": int(JobCatalog.Kind.CATCH_TOTAL),
- "target": 1,
- "fish_coin": 1,
- "experience": 1,
- }
- var save_data: Dictionary = PlayerJobService.default_save_data()
- save_data["host_board"] = {
- "plan_id": "legacy-plan",
- "cycle": 4,
- "schedule_anchor_index": 0,
- "jobs": [legacy_job],
- "weather_schedule": legacy_schedule,
- }
- save_data["active_plan_id"] = "legacy-plan"
- assert(PlayerJobService.validate_save_data(save_data))
- var jobs := PlayerJobService.new()
- assert(jobs.restore_from_save_data(save_data))
- var migrated: Dictionary = jobs.to_save_data()
- assert((migrated.get("host_board", {}) as Dictionary).is_empty())
- assert(str(migrated.get("active_plan_id", "")).is_empty())
- jobs.free()
-
-
-func _validate_fishing_weather_seams() -> void:
- var clear_context := FishingContextType.new()
- var rain_context := FishingContextType.new()
- rain_context.is_raining = true
- var fog_context := FishingContextType.new()
- fog_context.is_foggy = true
- fog_context.is_night = true
- var rain_only := FishAvailabilityType.new()
- rain_only.require_rain = true
- assert(not rain_only.is_available(clear_context))
- assert(rain_only.is_available(rain_context))
- var fog_only := FishAvailabilityType.new()
- fog_only.allow_day = false
- fog_only.require_fog = true
- assert(not fog_only.is_available(clear_context))
- assert(fog_only.is_available(fog_context))
- var no_fog := FishAvailabilityType.new()
- no_fog.forbid_fog = true
- assert(no_fog.is_available(clear_context))
- assert(not no_fog.is_available(fog_context))
-
-
-func _validate_fishing_spot_context() -> void:
- var weather := WorldWeatherServiceType.new()
- weather.begin_remote_session()
- weather.apply_authoritative_snapshot(
- WorldWeatherServiceType.Weather.FOGGY, 200.0
- )
- var fishing_spot := FishingSpotType.new()
- fishing_spot.set("_world_weather", weather)
- var region := FishableWaterRegionType.new()
- region.location_tags = [&"starter_pond"]
- var context: FishingContext = fishing_spot.build_network_context(region)
- assert(context.is_foggy)
- assert(not context.is_raining)
- weather.apply_authoritative_snapshot(
- WorldWeatherServiceType.Weather.RAINY, 200.0
- )
- context = fishing_spot.build_network_context(region)
- assert(context.is_raining)
- assert(not context.is_foggy)
- region.free()
- fishing_spot.free()
- weather.free()
-
-
-func _validate_weather_presentation() -> void:
- var world_root := Node3D.new()
- root.add_child(world_root)
- var world_environment := WorldEnvironment.new()
- var environment := Environment.new()
- var sky := Sky.new()
- var sky_material := ShaderMaterial.new()
- sky_material.shader = preload(
- "res://world/environment/straywild_sky.gdshader"
- )
- sky.sky_material = sky_material
- environment.sky = sky
- environment.adjustment_enabled = true
- environment.fog_enabled = true
- world_environment.environment = environment
- world_root.add_child(world_environment)
- var sun := DirectionalLight3D.new()
- world_root.add_child(sun)
- var clock := WorldTimeServiceType.new()
- world_root.add_child(clock)
- clock.begin_test_session(14.0)
- var weather := WorldWeatherServiceType.new()
- world_root.add_child(weather)
- weather.begin_remote_session()
- var rain_target := Node3D.new()
- world_root.add_child(rain_target)
- var rain_camera := Camera3D.new()
- world_root.add_child(rain_camera)
- rain_target.global_position = Vector3(-18.0, 2.0, 14.0)
- rain_camera.global_position = Vector3(24.0, 9.0, -18.0)
- var visuals := WorldTimeVisualControllerType.new()
- world_root.add_child(visuals)
- visuals.setup(
- clock,
- world_environment,
- sun,
- weather,
- rain_target,
- func() -> Camera3D: return rain_camera,
- )
- var runtime_environment: Environment = world_environment.environment
- var runtime_sky_material := (
- runtime_environment.sky.sky_material as ShaderMaterial
- )
- clock.set_authoritative_time(0.0)
- visuals.apply_time_immediately(0.0)
- assert(float(
- runtime_sky_material.get_shader_parameter("star_visibility")
- ) > 0.99)
- visuals.apply_weather_immediately(
- WorldWeatherServiceType.Weather.CLOUDY
- )
- assert(is_equal_approx(float(
- runtime_sky_material.get_shader_parameter("star_visibility")
- ), 0.18))
- visuals.apply_weather_immediately(
- WorldWeatherServiceType.Weather.RAINY
- )
- assert(is_equal_approx(float(
- runtime_sky_material.get_shader_parameter("star_visibility")
- ), 0.03))
- visuals.apply_weather_immediately(
- WorldWeatherServiceType.Weather.FOGGY
- )
- assert(is_zero_approx(float(
- runtime_sky_material.get_shader_parameter("star_visibility")
- )))
- visuals.apply_weather_immediately(
- WorldWeatherServiceType.Weather.SUNNY
- )
- clock.set_authoritative_time(14.0)
- visuals.apply_time_immediately(14.0)
- var clear_background_energy: float = (
- runtime_environment.background_energy_multiplier
- )
- var clear_ambient_energy: float = runtime_environment.ambient_light_energy
- var clear_fog_light_energy: float = runtime_environment.fog_light_energy
- var clear_brightness: float = runtime_environment.adjustment_brightness
- var clear_saturation: float = runtime_environment.adjustment_saturation
- var clear_sun_energy: float = sun.light_energy
- var clear_water_brightness: float = float(
- WorldTimeVisualControllerType.SALT_WATER_MATERIAL.get_shader_parameter(
- "environment_brightness"
- )
- )
- assert(not runtime_environment.fog_enabled)
- assert(is_zero_approx(float(
- WorldTimeVisualControllerType.SALT_WATER_MATERIAL.get_shader_parameter(
- "weather_fog_amount"
- )
- )))
- assert(is_equal_approx(runtime_environment.fog_sky_affect, 0.35))
- assert(is_zero_approx(float(
- runtime_sky_material.get_shader_parameter("cloud_opacity")
- )))
- var storm_clouds := visuals.get_node("LocalStormClouds") as Node3D
- assert(storm_clouds != null)
- assert(storm_clouds.call("get_patch_count") == 81)
- var cloud_field := storm_clouds.get_node("CloudField") as MultiMeshInstance3D
- assert(cloud_field != null)
- assert(cloud_field.multimesh != null)
- assert(cloud_field.multimesh.instance_count == 81)
- assert(cloud_field.multimesh.mesh is ArrayMesh)
- var cloud_shader: Shader = preload(
- "res://world/environment/local_storm_cloud.gdshader"
- )
- assert("cull_disabled" in cloud_shader.code)
- assert(LocalStormCloudLayer.WRAP_RADIUS >= 315.0)
- assert(is_equal_approx(LocalStormCloudLayer.MAXIMUM_OPACITY, 1.0))
- assert(
- LocalStormCloudLayer.DISTANCE_FADE_END
- < LocalStormCloudLayer.WRAP_RADIUS
- )
- assert(is_zero_approx(float(storm_clouds.call("get_storm_amount"))))
- visuals.apply_weather_immediately(
- WorldWeatherServiceType.Weather.FOGGY
- )
- assert(runtime_environment.fog_enabled)
- assert(float(
- WorldTimeVisualControllerType.SALT_WATER_MATERIAL.get_shader_parameter(
- "weather_fog_amount"
- )
- ) > 0.99)
- assert(is_zero_approx(float(
- WorldTimeVisualControllerType.SALT_WATER_MATERIAL.get_shader_parameter(
- "weather_fog_near_amount"
- )
- )))
- var water_fog_color := (
- WorldTimeVisualControllerType.SALT_WATER_MATERIAL.get_shader_parameter(
- "weather_fog_color"
- ) as Color
- )
- var rendered_fog_color := (
- runtime_environment.fog_light_color.srgb_to_linear()
- )
- rendered_fog_color.r *= runtime_environment.fog_light_energy
- rendered_fog_color.g *= runtime_environment.fog_light_energy
- rendered_fog_color.b *= runtime_environment.fog_light_energy
- rendered_fog_color.a = 1.0
- rendered_fog_color = rendered_fog_color.linear_to_srgb()
- assert(water_fog_color.is_equal_approx(rendered_fog_color))
- assert(is_equal_approx(float(
- WorldTimeVisualControllerType.SALT_WATER_MATERIAL.get_shader_parameter(
- "surface_highlight_night_floor"
- )
- ), WorldTimeVisualControllerType.DEFAULT_SURFACE_HIGHLIGHT_FLOOR))
- assert(runtime_environment.fog_depth_begin <= 4.01)
- assert(is_equal_approx(runtime_environment.fog_depth_end, 18.0))
- assert(is_equal_approx(runtime_environment.fog_sky_affect, 1.0))
- assert(is_zero_approx(runtime_environment.fog_aerial_perspective))
- assert(is_zero_approx(float(
- runtime_sky_material.get_shader_parameter("cloud_opacity")
- )))
- assert(is_equal_approx(
- runtime_environment.background_energy_multiplier,
- clear_background_energy
- * WorldTimeVisualControllerType.FOG_DAYLIGHT_SCENE_BRIGHTNESS,
- ))
- assert(is_equal_approx(
- runtime_environment.ambient_light_energy,
- clear_ambient_energy
- * WorldTimeVisualControllerType.FOG_DAYLIGHT_SCENE_BRIGHTNESS,
- ))
- assert(is_equal_approx(
- runtime_environment.fog_light_energy,
- clear_fog_light_energy
- * WorldTimeVisualControllerType.FOG_DAYLIGHT_FOG_LIGHT_BRIGHTNESS,
- ))
- assert(is_equal_approx(
- runtime_environment.adjustment_brightness,
- clear_brightness
- * WorldTimeVisualControllerType.FOG_DAYLIGHT_POST_BRIGHTNESS,
- ))
- assert(is_equal_approx(sun.light_energy, clear_sun_energy * 0.30))
- assert(is_equal_approx(
- float(
- WorldTimeVisualControllerType.SALT_WATER_MATERIAL.get_shader_parameter(
- "environment_brightness"
- )
- ),
- clear_water_brightness
- * WorldTimeVisualControllerType.FOG_DAYLIGHT_WATER_BRIGHTNESS,
- ))
- assert(is_equal_approx(
- runtime_environment.adjustment_saturation,
- clear_saturation,
- ))
- visuals.apply_weather_immediately(
- WorldWeatherServiceType.Weather.RAINY
- )
- assert(runtime_environment.fog_enabled)
- assert(float(
- WorldTimeVisualControllerType.SALT_WATER_MATERIAL.get_shader_parameter(
- "weather_fog_amount"
- )
- ) > 0.99)
- assert(is_zero_approx(float(
- WorldTimeVisualControllerType.SALT_WATER_MATERIAL.get_shader_parameter(
- "weather_fog_near_amount"
- )
- )))
- var rainy_water_fog_color := (
- WorldTimeVisualControllerType.SALT_WATER_MATERIAL.get_shader_parameter(
- "weather_fog_color"
- ) as Color
- )
- assert(
- rainy_water_fog_color.is_equal_approx(
- WorldTimeVisualControllerType.RAIN_WATER_FOG_COLOR
- )
- )
- assert(is_equal_approx(float(
- WorldTimeVisualControllerType.SALT_WATER_MATERIAL.get_shader_parameter(
- "surface_highlight_night_floor"
- )
- ), WorldTimeVisualControllerType.DEFAULT_SURFACE_HIGHLIGHT_FLOOR))
- assert(is_equal_approx(runtime_environment.fog_sky_affect, 0.68))
- assert(
- float(runtime_sky_material.get_shader_parameter("cloud_coverage"))
- > 0.98
- )
- assert(
- float(runtime_sky_material.get_shader_parameter("cloud_opacity"))
- > 0.99
- )
- assert(float(storm_clouds.call("get_storm_amount")) > 0.87)
- assert(storm_clouds.visible)
- assert(is_equal_approx(
- runtime_environment.adjustment_brightness,
- clear_brightness
- * 0.92
- * WorldTimeVisualControllerType.RAIN_DAYLIGHT_POST_BRIGHTNESS,
- ))
- var rain := visuals.get_node("LocalRain") as GPUParticles3D
- assert(rain != null)
- assert(rain.global_position.is_equal_approx(
- rain_camera.global_position
- + WorldTimeVisualControllerType.RAIN_EMITTER_OFFSET
- ))
- rain_camera.global_position = Vector3(-31.0, 15.0, 42.0)
- visuals.call("_update_rain_position")
- assert(rain.global_position.is_equal_approx(
- rain_camera.global_position
- + WorldTimeVisualControllerType.RAIN_EMITTER_OFFSET
- ))
- assert(rain.emitting)
- assert(rain.amount_ratio > 0.99)
- assert(rain.amount == WorldTimeVisualControllerType.RAIN_PARTICLE_AMOUNT)
- assert(
- WorldTimeVisualControllerType.RAIN_EMITTER_OFFSET.y
- >= WorldTimeVisualControllerType.RAIN_SUPPORTED_CANOPY_HEIGHT
- + WorldTimeVisualControllerType.RAIN_CANOPY_CLEARANCE
- )
- assert(is_equal_approx(
- rain.lifetime,
- WorldTimeVisualControllerType.RAIN_LIFETIME_SECONDS,
- ))
- assert(rain.visibility_aabb == (
- WorldTimeVisualControllerType.RAIN_VISIBILITY_AABB
- ))
- var rain_material := rain.process_material as ParticleProcessMaterial
- assert(rain_material != null)
- assert(
- rain_material.collision_mode
- == ParticleProcessMaterial.COLLISION_HIDE_ON_CONTACT
- )
- assert(is_equal_approx(
- rain.collision_base_size,
- WorldTimeVisualControllerType.RAIN_COLLISION_BASE_SIZE,
- ))
- assert(rain_material.emission_box_extents.is_equal_approx(
- WorldTimeVisualControllerType.RAIN_EMISSION_EXTENTS
- ))
- assert(is_equal_approx(
- rain_material.initial_velocity_min,
- WorldTimeVisualControllerType.RAIN_VELOCITY_MIN,
- ))
- assert(is_equal_approx(
- rain_material.initial_velocity_max,
- WorldTimeVisualControllerType.RAIN_VELOCITY_MAX,
- ))
- assert(rain_material.gravity.is_equal_approx(
- WorldTimeVisualControllerType.RAIN_GRAVITY
- ))
- var rain_mesh := rain.draw_pass_1 as BoxMesh
- assert(rain_mesh != null)
- assert(rain_mesh.size.is_equal_approx(
- WorldTimeVisualControllerType.RAIN_DROP_SIZE
- ))
- var rain_collision := visuals.get_node(
- "LocalRainCanopyCollision"
- ) as GPUParticlesCollisionHeightField3D
- assert(rain_collision != null)
- assert(rain_collision.follow_camera_enabled)
- assert(
- rain_collision.resolution
- == GPUParticlesCollisionHeightField3D.RESOLUTION_256
- )
- assert(
- rain_collision.update_mode
- == GPUParticlesCollisionHeightField3D.UPDATE_MODE_WHEN_MOVED
- )
- assert(rain_collision.size.is_equal_approx(
- WorldTimeVisualControllerType.RAIN_COLLISION_SIZE
- ))
- assert(
- rain_collision.size.y * 0.5
- > WorldTimeVisualControllerType.RAIN_EMITTER_OFFSET.y
- + WorldTimeVisualControllerType.RAIN_EMISSION_EXTENTS.y
- )
- assert(
- rain_collision.heightfield_mask
- == PrecipitationOcclusionType.RENDER_LAYER_MASK
- )
- visuals.call(
- "_on_weather_changed",
- WorldWeatherServiceType.Weather.SUNNY,
- 60.0,
- )
- visuals.call(
- "_process",
- WorldTimeVisualControllerType.WEATHER_TRANSITION_SECONDS * 0.5,
- )
- var halfway_water_fog: float = float(
- WorldTimeVisualControllerType.SALT_WATER_MATERIAL.get_shader_parameter(
- "weather_fog_amount"
- )
- )
- assert(halfway_water_fog > 0.49 and halfway_water_fog < 0.51)
- assert(is_equal_approx(
- float(
- WorldTimeVisualControllerType.FRESH_WATER_MATERIAL.get_shader_parameter(
- "weather_fog_amount"
- )
- ),
- halfway_water_fog,
- ))
- assert(runtime_environment.fog_enabled)
- visuals.call(
- "_process",
- WorldTimeVisualControllerType.WEATHER_TRANSITION_SECONDS * 0.5,
- )
- assert(is_zero_approx(float(
- WorldTimeVisualControllerType.SALT_WATER_MATERIAL.get_shader_parameter(
- "weather_fog_amount"
- )
- )))
- assert(not runtime_environment.fog_enabled)
- visuals.apply_weather_immediately(
- WorldWeatherServiceType.Weather.CLOUDY
- )
- assert(not runtime_environment.fog_enabled)
- assert(is_zero_approx(float(
- WorldTimeVisualControllerType.SALT_WATER_MATERIAL.get_shader_parameter(
- "weather_fog_amount"
- )
- )))
- visuals.apply_weather_immediately(
- WorldWeatherServiceType.Weather.SUNNY
- )
- assert(not runtime_environment.fog_enabled)
- assert(not rain.emitting)
- world_root.queue_free()
diff --git a/tests/world_weather_validation.gd.uid b/tests/world_weather_validation.gd.uid
deleted file mode 100644
index 8694186..0000000
--- a/tests/world_weather_validation.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://cdbmbag58okjb
diff --git a/tools/art/export_creature_art.py b/tools/art/export_creature_art.py
deleted file mode 100644
index 758be87..0000000
--- a/tools/art/export_creature_art.py
+++ /dev/null
@@ -1,256 +0,0 @@
-#!/usr/bin/env python3
-"""Export cataloged creature artwork with consistent transparent margins."""
-
-from __future__ import annotations
-
-import argparse
-import hashlib
-import re
-import sys
-import zipfile
-import xml.etree.ElementTree as ET
-from pathlib import Path
-
-from PIL import Image
-
-
-TABLE_NAMESPACE = "urn:oasis:names:tc:opendocument:xmlns:table:1.0"
-TABLE_ATTRIBUTE = "{%s}" % TABLE_NAMESPACE
-NAMESPACES = {"table": TABLE_NAMESPACE}
-VALID_EXPORT_SIZES = {64, 128, 256, 512}
-SAFE_AREA_RATIO = 0.875
-KNOWN_SOURCE_ALIASES = {
- # The delivered filename predates the catalog's authoritative species ID.
- "bowfish": "bowfin",
-}
-
-
-def parse_arguments() -> argparse.Namespace:
- parser = argparse.ArgumentParser(
- description=(
- "Export PNG creature art according to the catalog's recommended "
- "canvas size, using alpha-bound trimming and nearest-neighbor scaling."
- )
- )
- parser.add_argument("--tracker", required=True, type=Path)
- parser.add_argument("--source", required=True, type=Path)
- parser.add_argument("--output", required=True, type=Path)
- parser.add_argument(
- "--skip",
- action="append",
- default=[],
- help="Catalog creature ID or source filename stem to skip; repeat as needed.",
- )
- parser.add_argument(
- "--dry-run",
- action="store_true",
- help="Validate and report the batch without writing output files.",
- )
- return parser.parse_args()
-
-
-def normalize_name(value: str) -> str:
- normalized = value.strip().lower().replace("&", " and ")
- normalized = re.sub(r"[^a-z0-9]+", "_", normalized)
- return normalized.strip("_")
-
-
-def expanded_row_values(row: ET.Element) -> list[str]:
- values: list[str] = []
- for cell in row.findall("table:table-cell", NAMESPACES):
- repeat = int(
- cell.attrib.get(
- TABLE_ATTRIBUTE + "number-columns-repeated",
- "1",
- )
- )
- values.extend(["".join(cell.itertext()).strip()] * repeat)
- return values
-
-
-def load_catalog(tracker_path: Path) -> list[dict[str, str]]:
- with zipfile.ZipFile(tracker_path) as archive:
- root = ET.fromstring(archive.read("content.xml"))
- table = root.find(".//table:table", NAMESPACES)
- if table is None:
- raise ValueError("tracker does not contain a table")
- rows = table.findall("table:table-row", NAMESPACES)
- if not rows:
- raise ValueError("tracker table is empty")
- headers = expanded_row_values(rows[0])
- required_headers = {
- "catalog_number",
- "display_name",
- "id",
- "recommended_export_canvas_px",
- }
- missing_headers = required_headers.difference(headers)
- if missing_headers:
- raise ValueError(
- "tracker is missing required columns: "
- + ", ".join(sorted(missing_headers))
- )
- records: list[dict[str, str]] = []
- for row in rows[1:]:
- values = expanded_row_values(row)
- if not any(values):
- continue
- record = dict(zip(headers, values))
- if record.get("id", "").strip():
- records.append(record)
- return records
-
-
-def catalog_lookup(records: list[dict[str, str]]) -> dict[str, dict[str, str]]:
- lookup: dict[str, dict[str, str]] = {}
- for record in records:
- keys = {
- normalize_name(record["catalog_number"]),
- normalize_name(record["id"]),
- normalize_name(record["display_name"]),
- }
- for key in keys:
- previous = lookup.get(key)
- if previous is not None and previous["id"] != record["id"]:
- raise ValueError(
- f"ambiguous normalized catalog name {key!r}: "
- f"{previous['id']} and {record['id']}"
- )
- lookup[key] = record
- return lookup
-
-
-def resolve_record(
- source_path: Path,
- lookup: dict[str, dict[str, str]],
-) -> dict[str, str]:
- source_key = normalize_name(source_path.stem)
- catalog_key = KNOWN_SOURCE_ALIASES.get(source_key, source_key)
- record = lookup.get(catalog_key)
- if record is None:
- raise ValueError(
- f"cannot map source {source_path.name!r} to one catalog creature"
- )
- return record
-
-
-def normalize_artwork(source_path: Path, canvas_size: int) -> tuple[Image.Image, tuple[int, int, int, int], tuple[int, int]]:
- with Image.open(source_path) as source_image:
- artwork = source_image.convert("RGBA")
- alpha_bounds = artwork.getchannel("A").getbbox()
- if alpha_bounds is None:
- raise ValueError(f"source {source_path.name!r} is fully transparent")
- visible = artwork.crop(alpha_bounds)
- safe_long_side = round(canvas_size * SAFE_AREA_RATIO)
- scale = safe_long_side / max(visible.width, visible.height)
- scaled_size = (
- max(1, round(visible.width * scale)),
- max(1, round(visible.height * scale)),
- )
- visible = visible.resize(scaled_size, Image.Resampling.NEAREST)
- canvas = Image.new("RGBA", (canvas_size, canvas_size), (0, 0, 0, 0))
- offset = (
- (canvas_size - scaled_size[0]) // 2,
- (canvas_size - scaled_size[1]) // 2,
- )
- canvas.alpha_composite(visible, offset)
- return canvas, alpha_bounds, scaled_size
-
-
-def sha256(path: Path) -> str:
- digest = hashlib.sha256()
- with path.open("rb") as source:
- for block in iter(lambda: source.read(1024 * 1024), b""):
- digest.update(block)
- return digest.hexdigest()
-
-
-def main() -> int:
- arguments = parse_arguments()
- records = load_catalog(arguments.tracker)
- lookup = catalog_lookup(records)
- skip_keys = {normalize_name(value) for value in arguments.skip}
- source_paths = sorted(arguments.source.glob("*.png"))
- if not source_paths:
- raise ValueError(f"no PNG files found under {arguments.source}")
-
- resolved_by_output: dict[str, tuple[Path, dict[str, str]]] = {}
- for source_path in source_paths:
- record = resolve_record(source_path, lookup)
- source_key = normalize_name(source_path.stem)
- creature_key = normalize_name(record["id"])
- if source_key in skip_keys or creature_key in skip_keys:
- print(f"SKIP {source_path.name} -> {record['id']}")
- continue
- output_name = f"{record['catalog_number']}.png"
- previous = resolved_by_output.get(output_name)
- if previous is not None:
- previous_path, _previous_record = previous
- catalog_key = normalize_name(record["catalog_number"])
- previous_is_numbered = (
- normalize_name(previous_path.stem) == catalog_key
- )
- current_is_numbered = source_key == catalog_key
- if current_is_numbered and not previous_is_numbered:
- print(
- "SUPERSEDE "
- f"{previous_path.name} with numbered source "
- f"{source_path.name} -> {record['id']}"
- )
- resolved_by_output[output_name] = (source_path, record)
- continue
- if previous_is_numbered and not current_is_numbered:
- print(
- "SKIP "
- f"{source_path.name}; numbered source "
- f"{previous_path.name} takes precedence -> {record['id']}"
- )
- continue
- raise ValueError(
- f"duplicate sources for output filename {output_name}: "
- f"{previous_path.name} and {source_path.name}"
- )
- resolved_by_output[output_name] = (source_path, record)
-
- resolved = list(resolved_by_output.values())
-
- if not arguments.dry_run:
- arguments.output.mkdir(parents=True, exist_ok=True)
-
- for source_path, record in resolved:
- canvas_size = int(record["recommended_export_canvas_px"])
- if canvas_size not in VALID_EXPORT_SIZES:
- raise ValueError(
- f"{record['id']} has invalid export size {canvas_size}"
- )
- image, alpha_bounds, scaled_size = normalize_artwork(
- source_path,
- canvas_size,
- )
- output_path = arguments.output / f"{record['catalog_number']}.png"
- if not arguments.dry_run:
- image.save(output_path, format="PNG", optimize=False, compress_level=6)
- output_hash = sha256(output_path)
- else:
- output_hash = "dry-run"
- print(
- "EXPORT "
- f"{source_path.name} -> {output_path.name} "
- f"id={record['id']} canvas={canvas_size}x{canvas_size} "
- f"alpha_bounds={alpha_bounds} visible={scaled_size[0]}x{scaled_size[1]} "
- f"source_sha256={sha256(source_path)} output_sha256={output_hash}"
- )
-
- print(
- f"Complete: {len(resolved)} export(s), "
- f"{len(source_paths) - len(resolved)} skipped."
- )
- return 0
-
-
-if __name__ == "__main__":
- try:
- raise SystemExit(main())
- except (OSError, ValueError, zipfile.BadZipFile) as error:
- print(f"ERROR: {error}", file=sys.stderr)
- raise SystemExit(1)
diff --git a/tools/blender/export_terrain_chunks.py b/tools/blender/export_terrain_chunks.py
deleted file mode 100644
index d4a278e..0000000
--- a/tools/blender/export_terrain_chunks.py
+++ /dev/null
@@ -1,873 +0,0 @@
-#!/usr/bin/env python3
-"""Export convention-named terrain chunk collections to individual GLBs.
-
-Run through Blender rather than a standalone Python interpreter:
-
- blender --background terrain_chunks.blend \
- --python tools/blender/export_terrain_chunks.py -- \
- --output /path/to/terrain_chunks
-
-Collections use ``chunk_####_description`` and contain one primary terrain
-mesh named ``chunk_####``. Additional production objects may live in the same
-collection. Reusable procedural props use individual ``prop_description``
-mesh objects. Their child empties are exported with them so authored sockets
-remain attached to the prop. Props may be arranged anywhere in the source file
-because each root object's authored origin becomes the exported runtime anchor.
-A same-named ``prop_description`` collection remains supported for multi-object
-props. Unrelated objects and collections are ignored.
-"""
-
-from __future__ import annotations
-
-import argparse
-import hashlib
-import json
-import math
-import re
-import struct
-import sys
-from dataclasses import dataclass
-from pathlib import Path
-from typing import Iterable
-
-import bpy
-from mathutils import Vector
-
-
-CHUNK_SIZE_METERS = 10.0
-# Match the runtime boundary analyzer's five-millimeter authoring tolerance.
-DEFAULT_TOLERANCE = 0.005
-COLLECTION_PATTERN = re.compile(
- r"^chunk_(?P\d{4})(?:_(?P